fix conflicts
2
.gitattributes
vendored
@ -1 +1 @@
|
||||
* text=auto
|
||||
* text=auto eol=lf
|
18
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@ -44,11 +44,21 @@ body:
|
||||
placeholder: v0.8.8
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Platform
|
||||
description: The OS platform of the computer where you are running Wave
|
||||
options:
|
||||
- macOS
|
||||
- Linux
|
||||
- Windows
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: OS
|
||||
description: The name and version of the operating system of the computer where you are running Wave
|
||||
placeholder: macOS 15.0
|
||||
label: OS Version/Distribution
|
||||
description: The version of the operating system of the computer where you are running Wave
|
||||
placeholder: Ubuntu 24.04
|
||||
validations:
|
||||
required: false
|
||||
- type: dropdown
|
||||
@ -59,7 +69,7 @@ body:
|
||||
- arm64
|
||||
- x64
|
||||
validations:
|
||||
required: false
|
||||
required: true
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
3
.github/dependabot.yml
vendored
@ -24,6 +24,9 @@ updates:
|
||||
electron:
|
||||
patterns:
|
||||
- "*electron*"
|
||||
docusaurus:
|
||||
patterns:
|
||||
- "*docusaurus*"
|
||||
- package-ecosystem: "gomod"
|
||||
directory: "/"
|
||||
schedule:
|
||||
|
44
.github/workflows/build-helper.yml
vendored
@ -3,7 +3,7 @@
|
||||
# For more information on the Windows Code Signing, see https://docs.digicert.com/en/digicert-keylocker/ci-cd-integrations/plugins/github-custom-action-for-keypair-signing.html and https://docs.digicert.com/en/digicert-keylocker/signing-tools/sign-authenticode-with-electron-builder-using-ksp-integration.html
|
||||
|
||||
name: Build Helper
|
||||
run-name: Build ${{ github.ref_name }}${{ github.event_name == 'workflow_dispatch' && ' - Manual'}}
|
||||
run-name: Build ${{ github.ref_name }}${{ github.event_name == 'workflow_dispatch' && ' - Manual' || '' }}
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
@ -11,12 +11,9 @@ on:
|
||||
workflow_dispatch:
|
||||
env:
|
||||
GO_VERSION: "1.22"
|
||||
NODE_VERSION: "20"
|
||||
STATIC_DOCSITE_PATH: docsite
|
||||
NODE_VERSION: 22
|
||||
jobs:
|
||||
runbuild:
|
||||
permissions:
|
||||
contents: write
|
||||
build-app:
|
||||
outputs:
|
||||
version: ${{ steps.set-version.outputs.WAVETERM_VERSION }}
|
||||
strategy:
|
||||
@ -109,22 +106,12 @@ jobs:
|
||||
smctl windows certsync
|
||||
shell: cmd
|
||||
|
||||
- name: Download waveterm-docs static site
|
||||
uses: dawidd6/action-download-artifact@v6
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
workflow: build-embedded.yml
|
||||
repo: wavetermdev/waveterm-docs
|
||||
name: static-site
|
||||
path: ${{env.STATIC_DOCSITE_PATH}}
|
||||
|
||||
# Build and upload packages
|
||||
- name: Build (Linux)
|
||||
if: matrix.platform == 'linux'
|
||||
run: task package
|
||||
env:
|
||||
USE_SYSTEM_FPM: true # Ensure that the installed version of FPM is used rather than the bundled one.
|
||||
STATIC_DOCSITE_PATH: ${{env.STATIC_DOCSITE_PATH}}
|
||||
SNAPCRAFT_BUILD_ENVIRONMENT: host
|
||||
- name: Build (Darwin)
|
||||
if: matrix.platform == 'darwin'
|
||||
@ -147,6 +134,7 @@ jobs:
|
||||
STATIC_DOCSITE_PATH: ${{env.STATIC_DOCSITE_PATH}}
|
||||
shell: powershell # electron-builder's Windows code signing package has some compatibility issues with pwsh, so we need to use Windows Powershell
|
||||
|
||||
# Upload artifacts to the S3 staging and to the workflow output for the draft release job
|
||||
- name: Upload to S3 staging
|
||||
if: github.event_name != 'workflow_dispatch'
|
||||
run: task artifacts:upload
|
||||
@ -154,8 +142,24 @@ jobs:
|
||||
AWS_ACCESS_KEY_ID: "${{ secrets.ARTIFACTS_KEY_ID }}"
|
||||
AWS_SECRET_ACCESS_KEY: "${{ secrets.ARTIFACTS_KEY_SECRET }}"
|
||||
AWS_DEFAULT_REGION: us-west-2
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.runner }}
|
||||
path: make
|
||||
create-release:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-app
|
||||
permissions:
|
||||
contents: write
|
||||
if: ${{ github.event_name != 'workflow_dispatch' }}
|
||||
steps:
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: make
|
||||
merge-multiple: true
|
||||
- name: Create draft release
|
||||
if: github.event_name != 'workflow_dispatch'
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
prerelease: ${{ contains(github.ref_name, '-beta') }}
|
||||
@ -173,9 +177,3 @@ jobs:
|
||||
make/*.snap
|
||||
make/*.flatpak
|
||||
make/*.AppImage
|
||||
- name: Upload build artifacts to workflow (manual runs only)
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{matrix.runner}}
|
||||
path: make
|
||||
|
2
.github/workflows/bump-version.yml
vendored
@ -23,7 +23,7 @@ on:
|
||||
type: boolean
|
||||
default: true
|
||||
env:
|
||||
NODE_VERSION: "20"
|
||||
NODE_VERSION: 22
|
||||
jobs:
|
||||
bump-version:
|
||||
runs-on: ubuntu-latest
|
||||
|
2
.github/workflows/codeql.yml
vendored
@ -20,7 +20,7 @@ on:
|
||||
- cron: "36 5 * * 5"
|
||||
|
||||
env:
|
||||
NODE_VERSION: "20"
|
||||
NODE_VERSION: 22
|
||||
GO_VERSION: "1.22.5"
|
||||
|
||||
jobs:
|
||||
|
72
.github/workflows/deploy-docsite.yml
vendored
Normal file
@ -0,0 +1,72 @@
|
||||
name: Docsite and Storybook CI/CD
|
||||
|
||||
run-name: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && 'Build and Deploy' || 'Test Build' }} Docsite and Storybook
|
||||
|
||||
env:
|
||||
NODE_VERSION: 22
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
# Also run any time a PR is opened targeting the docs or storybook resources
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "docs/**"
|
||||
- "storybook/**"
|
||||
- "**/*.story.*"
|
||||
- "**/*.stories.*"
|
||||
- ".github/workflows/deploy-docsite.yml"
|
||||
- "Taskfile.yml"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build Docsite
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{env.NODE_VERSION}}
|
||||
- name: Install Task
|
||||
uses: arduino/setup-task@v2
|
||||
with:
|
||||
version: 3.x
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Install yarn
|
||||
run: |
|
||||
corepack enable
|
||||
yarn install
|
||||
- name: Build docsite
|
||||
run: task docsite:build:public
|
||||
- name: Upload Build Artifact
|
||||
# Only upload the build artifact when pushed to the main branch
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: docs/build
|
||||
deploy:
|
||||
name: Deploy to GitHub Pages
|
||||
# Only deploy when pushed to the main branch
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
needs: build
|
||||
# Grant GITHUB_TOKEN the permissions required to make a Pages deployment
|
||||
permissions:
|
||||
pages: write # to deploy to Pages
|
||||
id-token: write # to verify the deployment originates from an appropriate source
|
||||
|
||||
# Deploy to the github-pages environment
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
42
.github/workflows/deploy-storybook.yml
vendored
@ -1,42 +0,0 @@
|
||||
# Workflow name
|
||||
name: Build and Publish Storybook to GitHub Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
env:
|
||||
NODE_VERSION: "20"
|
||||
|
||||
# List of jobs
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
# Job steps
|
||||
steps:
|
||||
# Manual Checkout
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# Set up Node
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{env.NODE_VERSION}}
|
||||
|
||||
- name: Install yarn
|
||||
run: |
|
||||
corepack enable
|
||||
yarn install
|
||||
|
||||
#👇 Add Storybook build and deploy to GitHub Pages as a step in the workflow
|
||||
- uses: bitovi/github-actions-storybook-to-github-pages@v1.0.3
|
||||
with:
|
||||
install_command: yarn # default: npm ci
|
||||
build_command: yarn build-storybook # default: npm run build-storybook
|
||||
path: storybook-static # default: dist/storybook
|
||||
checkout: false # default: true
|
26
.github/workflows/merge-gatekeeper.yml
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
---
|
||||
name: Merge Gatekeeper
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
|
||||
jobs:
|
||||
merge-gatekeeper:
|
||||
runs-on: ubuntu-latest
|
||||
# Restrict permissions of the GITHUB_TOKEN.
|
||||
# Docs: https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs
|
||||
permissions:
|
||||
checks: read
|
||||
statuses: read
|
||||
steps:
|
||||
- name: Run Merge Gatekeeper
|
||||
# NOTE: v1 is updated to reflect the latest v1.x.y. Please use any tag/branch that suits your needs:
|
||||
# https://github.com/upsidr/merge-gatekeeper/tags
|
||||
# https://github.com/upsidr/merge-gatekeeper/branches
|
||||
uses: upsidr/merge-gatekeeper@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
ignored: Test Onboarding, Analyze (go), Analyze (javascript-typescript), License Compliance
|
38
.github/workflows/publish-release.yml
vendored
@ -7,6 +7,7 @@ on:
|
||||
types: [published]
|
||||
jobs:
|
||||
publish:
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/') }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@ -15,11 +16,46 @@ jobs:
|
||||
with:
|
||||
version: 3.x
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Install Snapcraft
|
||||
run: sudo snap install snapcraft --classic
|
||||
shell: bash
|
||||
- name: Publish from staging
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
run: "task artifacts:publish:${{ github.ref_name }}"
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: "${{ secrets.PUBLISHER_KEY_ID }}"
|
||||
AWS_SECRET_ACCESS_KEY: "${{ secrets.PUBLISHER_KEY_SECRET }}"
|
||||
AWS_DEFAULT_REGION: us-west-2
|
||||
shell: bash
|
||||
- name: Download Snap from Release
|
||||
uses: robinraju/release-downloader@v1
|
||||
with:
|
||||
tag: ${{github.ref_name}}
|
||||
fileName: "*.snap"
|
||||
- name: Publish to Snapcraft
|
||||
run: "task artifacts:snap:publish:${{ github.ref_name }}"
|
||||
env:
|
||||
SNAPCRAFT_STORE_CREDENTIALS: "${{secrets.SNAPCRAFT_LOGIN_CREDS}}"
|
||||
shell: bash
|
||||
bump-winget:
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/') && !contains(github.ref_name, 'beta') }}
|
||||
needs: [publish]
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Task
|
||||
uses: arduino/setup-task@v2
|
||||
with:
|
||||
version: 3.x
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Install winget
|
||||
uses: Cyberboss/install-winget@v1
|
||||
with:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Install wingetcreate
|
||||
run: winget install -e --silent --accept-package-agreements --accept-source-agreements wingetcreate
|
||||
shell: pwsh
|
||||
- name: Submit WinGet version bump
|
||||
run: "task artifacts:winget:publish:${{ github.ref_name }}"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.WINGET_BUMP_PAT }}
|
||||
shell: pwsh
|
||||
|
267
.github/workflows/testdriver.yml
vendored
@ -1,156 +1,165 @@
|
||||
name: TestDriver.ai
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- "v[0-9]+.[0-9]+.[0-9]+*"
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
schedule:
|
||||
- cron: 0 21 * * *
|
||||
workflow_dispatch: null
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- "v[0-9]+.[0-9]+.[0-9]+*"
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
paths-ignore:
|
||||
- "docs/**"
|
||||
- ".storybook/**"
|
||||
- ".vscode/**"
|
||||
- ".editorconfig"
|
||||
- ".gitignore"
|
||||
- ".prettierrc"
|
||||
- ".eslintrc.js"
|
||||
- "**/*.md"
|
||||
schedule:
|
||||
- cron: 0 21 * * *
|
||||
workflow_dispatch: null
|
||||
|
||||
env:
|
||||
GO_VERSION: "1.22"
|
||||
NODE_VERSION: "20"
|
||||
GO_VERSION: "1.22"
|
||||
NODE_VERSION: 22
|
||||
|
||||
permissions:
|
||||
contents: read # To allow the action to read repository contents
|
||||
pull-requests: write # To allow the action to create/update pull request comments
|
||||
contents: read # To allow the action to read repository contents
|
||||
pull-requests: write # To allow the action to create/update pull request comments
|
||||
|
||||
jobs:
|
||||
build_and_upload:
|
||||
name: Test Onboarding
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
build_and_upload:
|
||||
name: Test Onboarding
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# General build dependencies
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{env.GO_VERSION}}
|
||||
cache-dependency-path: |
|
||||
go.sum
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{env.NODE_VERSION}}
|
||||
- name: Install Yarn
|
||||
run: |
|
||||
corepack enable
|
||||
yarn install
|
||||
- name: Install Task
|
||||
uses: arduino/setup-task@v2
|
||||
with:
|
||||
version: 3.x
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# General build dependencies
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{env.GO_VERSION}}
|
||||
cache-dependency-path: |
|
||||
go.sum
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{env.NODE_VERSION}}
|
||||
- name: Install Yarn
|
||||
run: |
|
||||
corepack enable
|
||||
yarn install
|
||||
- name: Install Task
|
||||
uses: arduino/setup-task@v2
|
||||
with:
|
||||
version: 3.x
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build
|
||||
run: task package
|
||||
env:
|
||||
USE_SYSTEM_FPM: true # Ensure that the installed version of FPM is used rather than the bundled one.
|
||||
CSC_IDENTITY_AUTO_DISCOVERY: false # disable codesign
|
||||
shell: powershell # electron-builder's Windows code signing package has some compatibility issues with pwsh, so we need to use Windows Powershell
|
||||
- name: Build
|
||||
run: task package
|
||||
env:
|
||||
USE_SYSTEM_FPM: true # Ensure that the installed version of FPM is used rather than the bundled one.
|
||||
CSC_IDENTITY_AUTO_DISCOVERY: false # disable codesign
|
||||
shell: powershell # electron-builder's Windows code signing package has some compatibility issues with pwsh, so we need to use Windows Powershell
|
||||
|
||||
# Upload .exe as an artifact
|
||||
- name: Upload .exe artifact
|
||||
id: upload
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: windows-exe
|
||||
path: make/*.exe
|
||||
# Upload .exe as an artifact
|
||||
- name: Upload .exe artifact
|
||||
id: upload
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: windows-exe
|
||||
path: make/*.exe
|
||||
|
||||
- uses: testdriverai/action@main
|
||||
id: testdriver
|
||||
env:
|
||||
FORCE_COLOR: "3"
|
||||
with:
|
||||
key: ${{ secrets.DASHCAM_API }}
|
||||
prerun: |
|
||||
$headers = @{
|
||||
Authorization = "token ${{ secrets.GITHUB_TOKEN }}"
|
||||
}
|
||||
- uses: testdriverai/action@main
|
||||
id: testdriver
|
||||
env:
|
||||
FORCE_COLOR: "3"
|
||||
with:
|
||||
key: ${{ secrets.DASHCAM_API }}
|
||||
prerun: |
|
||||
$headers = @{
|
||||
Authorization = "token ${{ secrets.GITHUB_TOKEN }}"
|
||||
}
|
||||
|
||||
$downloadFolder = "./download"
|
||||
$artifactFileName = "waveterm.exe"
|
||||
$artifactFilePath = "$downloadFolder/$artifactFileName"
|
||||
$downloadFolder = "./download"
|
||||
$artifactFileName = "waveterm.exe"
|
||||
$artifactFilePath = "$downloadFolder/$artifactFileName"
|
||||
|
||||
Write-Host "Starting the artifact download process..."
|
||||
Write-Host "Starting the artifact download process..."
|
||||
|
||||
# Create the download directory if it doesn't exist
|
||||
if (-not (Test-Path -Path $downloadFolder)) {
|
||||
Write-Host "Creating download folder..."
|
||||
mkdir $downloadFolder
|
||||
} else {
|
||||
Write-Host "Download folder already exists."
|
||||
}
|
||||
# Create the download directory if it doesn't exist
|
||||
if (-not (Test-Path -Path $downloadFolder)) {
|
||||
Write-Host "Creating download folder..."
|
||||
mkdir $downloadFolder
|
||||
} else {
|
||||
Write-Host "Download folder already exists."
|
||||
}
|
||||
|
||||
# Fetch the artifact upload URL
|
||||
Write-Host "Fetching the artifact upload URL..."
|
||||
$artifactUrl = (Invoke-RestMethod -Uri "https://api.github.com/repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts" -Headers $headers).artifacts[0].archive_download_url
|
||||
# Fetch the artifact upload URL
|
||||
Write-Host "Fetching the artifact upload URL..."
|
||||
$artifactUrl = (Invoke-RestMethod -Uri "https://api.github.com/repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts" -Headers $headers).artifacts[0].archive_download_url
|
||||
|
||||
if ($artifactUrl) {
|
||||
Write-Host "Artifact URL successfully fetched: $artifactUrl"
|
||||
} else {
|
||||
Write-Error "Failed to fetch the artifact URL."
|
||||
exit 1
|
||||
}
|
||||
if ($artifactUrl) {
|
||||
Write-Host "Artifact URL successfully fetched: $artifactUrl"
|
||||
} else {
|
||||
Write-Error "Failed to fetch the artifact URL."
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Download the artifact (zipped file)
|
||||
Write-Host "Starting artifact download..."
|
||||
$artifactZipPath = "$env:TEMP\artifact.zip"
|
||||
try {
|
||||
Invoke-WebRequest -Uri $artifactUrl `
|
||||
-Headers $headers `
|
||||
-OutFile $artifactZipPath `
|
||||
-MaximumRedirection 5
|
||||
# Download the artifact (zipped file)
|
||||
Write-Host "Starting artifact download..."
|
||||
$artifactZipPath = "$env:TEMP\artifact.zip"
|
||||
try {
|
||||
Invoke-WebRequest -Uri $artifactUrl `
|
||||
-Headers $headers `
|
||||
-OutFile $artifactZipPath `
|
||||
-MaximumRedirection 5
|
||||
|
||||
Write-Host "Artifact downloaded successfully to $artifactZipPath"
|
||||
} catch {
|
||||
Write-Error "Error downloading artifact: $_"
|
||||
exit 1
|
||||
}
|
||||
Write-Host "Artifact downloaded successfully to $artifactZipPath"
|
||||
} catch {
|
||||
Write-Error "Error downloading artifact: $_"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Unzip the artifact
|
||||
$artifactUnzipPath = "$env:TEMP\artifact"
|
||||
Write-Host "Unzipping the artifact to $artifactUnzipPath..."
|
||||
try {
|
||||
Expand-Archive -Path $artifactZipPath -DestinationPath $artifactUnzipPath -Force
|
||||
Write-Host "Artifact unzipped successfully to $artifactUnzipPath"
|
||||
} catch {
|
||||
Write-Error "Failed to unzip the artifact: $_"
|
||||
exit 1
|
||||
}
|
||||
# Unzip the artifact
|
||||
$artifactUnzipPath = "$env:TEMP\artifact"
|
||||
Write-Host "Unzipping the artifact to $artifactUnzipPath..."
|
||||
try {
|
||||
Expand-Archive -Path $artifactZipPath -DestinationPath $artifactUnzipPath -Force
|
||||
Write-Host "Artifact unzipped successfully to $artifactUnzipPath"
|
||||
} catch {
|
||||
Write-Error "Failed to unzip the artifact: $_"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Find the installer or app executable
|
||||
$artifactInstallerPath = Get-ChildItem -Path $artifactUnzipPath -Filter *.exe -Recurse | Select-Object -First 1
|
||||
# Find the installer or app executable
|
||||
$artifactInstallerPath = Get-ChildItem -Path $artifactUnzipPath -Filter *.exe -Recurse | Select-Object -First 1
|
||||
|
||||
if ($artifactInstallerPath) {
|
||||
Write-Host "Executable file found: $($artifactInstallerPath.FullName)"
|
||||
} else {
|
||||
Write-Error "Executable file not found. Exiting."
|
||||
exit 1
|
||||
}
|
||||
if ($artifactInstallerPath) {
|
||||
Write-Host "Executable file found: $($artifactInstallerPath.FullName)"
|
||||
} else {
|
||||
Write-Error "Executable file not found. Exiting."
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Run the installer and log the result
|
||||
Write-Host "Running the installer: $($artifactInstallerPath.FullName)..."
|
||||
try {
|
||||
Start-Process -FilePath $artifactInstallerPath.FullName -Wait
|
||||
Write-Host "Installer ran successfully."
|
||||
} catch {
|
||||
Write-Error "Failed to run the installer: $_"
|
||||
exit 1
|
||||
}
|
||||
# Run the installer and log the result
|
||||
Write-Host "Running the installer: $($artifactInstallerPath.FullName)..."
|
||||
try {
|
||||
Start-Process -FilePath $artifactInstallerPath.FullName -Wait
|
||||
Write-Host "Installer ran successfully."
|
||||
} catch {
|
||||
Write-Error "Failed to run the installer: $_"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Optional: If the app executable is different from the installer, find and launch it
|
||||
$wavePath = Join-Path $env:USERPROFILE "AppData\Local\Programs\waveterm\Wave.exe"
|
||||
# Optional: If the app executable is different from the installer, find and launch it
|
||||
$wavePath = Join-Path $env:USERPROFILE "AppData\Local\Programs\waveterm\Wave.exe"
|
||||
|
||||
Write-Host "Launching the application: $($wavePath)"
|
||||
Start-Process -FilePath $wavePath
|
||||
Write-Host "Application launched."
|
||||
Write-Host "Launching the application: $($wavePath)"
|
||||
Start-Process -FilePath $wavePath
|
||||
Write-Host "Application launched."
|
||||
|
||||
prompt: |
|
||||
1. /run testdriver/onboarding.yml
|
||||
2. /generate desktop 20
|
||||
prompt: |
|
||||
1. /run testdriver/onboarding.yml
|
||||
2. /generate desktop 20
|
||||
|
2
.gitignore
vendored
@ -30,3 +30,5 @@ artifacts/
|
||||
storybook-static/
|
||||
|
||||
test-results.xml
|
||||
|
||||
docsite/
|
||||
|
@ -37,6 +37,46 @@ const config: StorybookConfig = {
|
||||
return mergedConfig;
|
||||
},
|
||||
|
||||
staticDirs: [{ from: "../assets", to: "/assets" }],
|
||||
staticDirs: [
|
||||
{ from: "../assets", to: "/assets" },
|
||||
{ from: "../public/fontawesome", to: "/fontawesome" },
|
||||
],
|
||||
managerHead: (head) => `
|
||||
${head}
|
||||
<link rel="shortcut icon" href="./assets/waveterm-logo-with-bg.ico" />
|
||||
<link rel="icon" type="image/png" href="./assets/waveterm-logo-with-bg.png" sizes="250x250" />
|
||||
<style>
|
||||
.sidebar-header img {
|
||||
max-width: 150px !important;
|
||||
max-height: 100px !important;
|
||||
}
|
||||
</style>`,
|
||||
previewHead: (head) => `
|
||||
${head}
|
||||
<link rel="stylesheet" href="./fontawesome/css/fontawesome.min.css" />
|
||||
<link rel="stylesheet" href="./fontawesome/css/brands.min.css" />
|
||||
<link rel="stylesheet" href="./fontawesome/css/solid.min.css" />
|
||||
<link rel="stylesheet" href="./fontawesome/css/sharp-solid.min.css" />
|
||||
<link rel="stylesheet" href="./fontawesome/css/sharp-regular.min.css" />
|
||||
<style>
|
||||
#storybook-docs {
|
||||
[id^="anchor--"],
|
||||
#stories {
|
||||
a {
|
||||
margin-left: -24px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #ffffff !important;
|
||||
}
|
||||
|
||||
html.dark {
|
||||
body {
|
||||
background-color: #222222 !important;
|
||||
}
|
||||
}
|
||||
</style>`,
|
||||
};
|
||||
export default config;
|
||||
|
@ -1,8 +0,0 @@
|
||||
<link rel="shortcut icon" href="/assets/waveterm-logo-with-bg.ico" />
|
||||
<link rel="icon" type="image/png" href="/assets/waveterm-logo-with-bg.png" sizes="250x250" />
|
||||
<style>
|
||||
.sidebar-header img {
|
||||
max-width: 150px !important;
|
||||
max-height: 100px !important;
|
||||
}
|
||||
</style>
|
@ -1,25 +0,0 @@
|
||||
<link rel="stylesheet" href="/fontawesome/css/fontawesome.min.css" />
|
||||
<link rel="stylesheet" href="/fontawesome/css/brands.min.css" />
|
||||
<link rel="stylesheet" href="/fontawesome/css/solid.min.css" />
|
||||
<link rel="stylesheet" href="/fontawesome/css/sharp-solid.min.css" />
|
||||
<link rel="stylesheet" href="/fontawesome/css/sharp-regular.min.css" />
|
||||
<style>
|
||||
#storybook-docs {
|
||||
[id^="anchor--"],
|
||||
#stories {
|
||||
a {
|
||||
margin-left: -24px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #ffffff !important;
|
||||
}
|
||||
|
||||
html.dark {
|
||||
body {
|
||||
background-color: #222222 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -3,9 +3,9 @@ import type { Preview } from "@storybook/react";
|
||||
import React from "react";
|
||||
import { DndProvider } from "react-dnd";
|
||||
import { HTML5Backend } from "react-dnd-html5-backend";
|
||||
import "../frontend/app/theme.less";
|
||||
import "../frontend/app/app.less";
|
||||
import "../frontend/app/reset.less";
|
||||
import "../frontend/app/theme.scss";
|
||||
import "../frontend/app/app.scss";
|
||||
import "../frontend/app/reset.scss";
|
||||
import "./global.css";
|
||||
import { light, dark } from "./theme";
|
||||
import { DocsContainer } from "@storybook/addon-docs";
|
||||
|
@ -3,15 +3,15 @@ import { create } from "@storybook/theming";
|
||||
export const light = create({
|
||||
base: "light",
|
||||
brandTitle: "Wave Terminal Storybook",
|
||||
brandUrl: "https://storybook.waveterm.dev",
|
||||
brandImage: "/assets/wave-light.png",
|
||||
brandUrl: "https://docs.waveterm.dev/storybook/",
|
||||
brandImage: "./assets/wave-light.png",
|
||||
brandTarget: "_self",
|
||||
});
|
||||
|
||||
export const dark = create({
|
||||
base: "dark",
|
||||
brandTitle: "Wave Terminal Storybook",
|
||||
brandUrl: "https://storybook.waveterm.dev",
|
||||
brandImage: "/assets/wave-dark.png",
|
||||
brandUrl: "https://docs.waveterm.dev/storybook/",
|
||||
brandImage: "./assets/wave-dark.png",
|
||||
brandTarget: "_self",
|
||||
});
|
||||
|
6
.vscode/settings.json
vendored
@ -36,5 +36,11 @@
|
||||
},
|
||||
"[go]": {
|
||||
"editor.defaultFormatter": "golang.go"
|
||||
},
|
||||
"[mdx]": {
|
||||
"editor.wordWrap": "on"
|
||||
},
|
||||
"[md]": {
|
||||
"editor.wordWrap": "on"
|
||||
}
|
||||
}
|
||||
|
21
BUILD.md
@ -35,6 +35,19 @@ Arch:
|
||||
sudo pacman -S zip zig
|
||||
```
|
||||
|
||||
##### For packaging
|
||||
|
||||
For packaging, the following additional packages are required:
|
||||
|
||||
- `fpm` — If you're on x64 you can skip this. If you're on ARM64, install fpm via [Gem](https://rubygems.org/gems/fpm)
|
||||
- `rpm` — If you're not on Fedora, install RPM via your package manager.
|
||||
- `snapd` — If your distro doesn't already include it, [install `snapd`](https://snapcraft.io/docs/installing-snapd)
|
||||
- `lxd` — [Installation instructions](https://canonical.com/lxd/install)
|
||||
- `snapcraft` — Run `sudo snap install snapcraft --classic`
|
||||
- `libarchive-tools` — Install via your package manager
|
||||
- `libopenjp2-tools` — Install via your package manager
|
||||
- `squashfs-tools` — Install via your package manager
|
||||
|
||||
#### Windows
|
||||
|
||||
You will need the GNU build toolchain installed in order for Go to work on Windows. In most cases, this requires installing MinGW-w64.
|
||||
@ -55,7 +68,7 @@ Download and install Go via your package manager or directly from the website: h
|
||||
|
||||
### NodeJS
|
||||
|
||||
Make sure you have a recent version of NodeJS installed (>= 20).
|
||||
Make sure you have a NodeJS 22 LTS installed.
|
||||
|
||||
See NodeJS's website for platform-specific instructions: https://nodejs.org/en/download
|
||||
|
||||
@ -118,6 +131,12 @@ Run the following command to generate a production build and package it. This le
|
||||
task package
|
||||
```
|
||||
|
||||
If you're on Linux ARM64, run the following:
|
||||
|
||||
```sh
|
||||
USE_SYSTEM_FPM=1 task package
|
||||
```
|
||||
|
||||
## Debugging
|
||||
|
||||
### Frontend logs
|
||||
|
@ -4,7 +4,7 @@ We welcome and value contributions to Wave Terminal! Wave is an open source proj
|
||||
|
||||
- Submit issues related to bugs or new feature requests
|
||||
- Fix outstanding [issues](https://github.com/wavetermdev/waveterm/issues) with the existing code
|
||||
- Contribute to [documentation](https://github.com/wavetermdev/waveterm-docs)
|
||||
- Contribute to [documentation](./docs)
|
||||
- Spread the word on social media (tag us on [LinkedIn](https://www.linkedin.com/company/wavetermdev), [Twitter/X](https://x.com/wavetermdev))
|
||||
- Or simply ⭐️ the repository to show your appreciation
|
||||
|
||||
@ -43,7 +43,7 @@ To build and run Wave locally, see instructions at [Building Wave Terminal](./BU
|
||||
|
||||
We are working to document all our UI components in [Storybook](https://storybook.js.org/docs) for easy reference and testing. If you would like to help us with this, we would be very grateful!
|
||||
|
||||
Our Storybook site is hosted [storybook.waveterm.dev](https://storybook.waveterm.dev).
|
||||
Our Storybook site is hosted [docs.waveterm.dev/storybook](https://docs.waveterm.dev/storybook).
|
||||
|
||||
### Create a Pull Request
|
||||
|
||||
|
13
README.md
@ -10,6 +10,7 @@
|
||||
# Wave Terminal
|
||||
|
||||
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fwavetermdev%2Fwaveterm.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Fwavetermdev%2Fwaveterm?ref=badge_shield)
|
||||
[![waveterm](https://snapcraft.io/waveterm/trending.svg?name=0)](https://snapcraft.io/waveterm)
|
||||
|
||||
Wave is an open-source terminal that can launch graphical widgets, controlled and integrated directly with the CLI. It includes a base terminal, directory browser, file previews (images, media, markdown), a graphical editor (for code/text files), a web browser, and integrated AI chat.
|
||||
|
||||
@ -21,19 +22,15 @@ Wave isn't just another terminal emulator; it's a rethink on how terminals are b
|
||||
|
||||
Wave Terminal works on macOS, Linux, and Windows.
|
||||
|
||||
Install Wave Terminal from: [www.waveterm.dev/download](https://www.waveterm.dev/download)
|
||||
Platform-specific installation instructions can be found [here](https://docs.waveterm.dev/gettingstarted).
|
||||
|
||||
Also available as a Homebrew Cask for macOS:
|
||||
|
||||
```bash
|
||||
brew install --cask wave
|
||||
```
|
||||
You can also install Wave Terminal directly from: [www.waveterm.dev/download](https://www.waveterm.dev/download).
|
||||
|
||||
### Minimum requirements
|
||||
|
||||
Wave Terminal and WSH run on the following platforms:
|
||||
|
||||
- macOS 10.15 or later (arm64, x64)
|
||||
- macOS 11 or later (arm64, x64)
|
||||
- Windows 10 1809 or later (x64)
|
||||
- Linux based on glibc-2.28 or later (Debian 10, RHEL 8, Ubuntu 20.04, etc.) (arm64, x64)
|
||||
|
||||
@ -58,7 +55,7 @@ Find more information in our [Contributions Guide](CONTRIBUTING.md), which inclu
|
||||
|
||||
- [Ways to contribute](CONTRIBUTING.md#contributing-to-wave-terminal)
|
||||
- [Contribution guidelines](CONTRIBUTING.md#before-you-start)
|
||||
- [Storybook](https://storybook.waveterm.dev)
|
||||
- [Storybook](https://docs.waveterm.dev/storybook)
|
||||
|
||||
### Activity
|
||||
|
||||
|
171
Taskfile.yml
@ -13,6 +13,7 @@ vars:
|
||||
DATE: '{{if eq OS "windows"}}powershell Get-Date -UFormat{{else}}date{{end}}'
|
||||
ARTIFACTS_BUCKET: waveterm-github-artifacts/staging-w2
|
||||
RELEASES_BUCKET: dl.waveterm.dev/releases-w2
|
||||
WINGET_PACKAGE: CommandLine.Wave
|
||||
|
||||
tasks:
|
||||
electron:dev:
|
||||
@ -20,6 +21,7 @@ tasks:
|
||||
cmd: yarn dev
|
||||
deps:
|
||||
- yarn
|
||||
- docsite:build:embedded
|
||||
- build:backend
|
||||
env:
|
||||
WCLOUD_ENDPOINT: "https://ot2e112zx5.execute-api.us-west-2.amazonaws.com/dev"
|
||||
@ -30,11 +32,64 @@ tasks:
|
||||
cmd: yarn start
|
||||
deps:
|
||||
- yarn
|
||||
- docsite:build:embedded
|
||||
- build:backend
|
||||
env:
|
||||
WCLOUD_ENDPOINT: "https://ot2e112zx5.execute-api.us-west-2.amazonaws.com/dev"
|
||||
WCLOUD_WS_ENDPOINT: "wss://5lfzlg5crl.execute-api.us-west-2.amazonaws.com/dev/"
|
||||
|
||||
storybook:
|
||||
desc: Start the Storybook server.
|
||||
cmd: yarn storybook
|
||||
deps:
|
||||
- yarn
|
||||
|
||||
storybook:build:
|
||||
desc: Build the Storybook static site.
|
||||
cmd: yarn build-storybook
|
||||
generates:
|
||||
- storybook-static/**/*
|
||||
deps:
|
||||
- yarn
|
||||
|
||||
docsite:start:
|
||||
desc: Start the docsite dev server.
|
||||
cmd: yarn start
|
||||
dir: docs
|
||||
deps:
|
||||
- yarn
|
||||
|
||||
docsite:build:public:
|
||||
desc: Build the full docsite.
|
||||
cmds:
|
||||
- cd docs && yarn build
|
||||
- task: copyfiles:'storybook-static':'docs/build/storybook'
|
||||
sources:
|
||||
- "docs/*"
|
||||
- "docs/src/**/*"
|
||||
- "docs/docs/**/*"
|
||||
- "docs/static/**/*"
|
||||
- storybook-static/**/*
|
||||
generates:
|
||||
- "docs/build/**/*"
|
||||
deps:
|
||||
- yarn
|
||||
- storybook:build
|
||||
|
||||
docsite:build:embedded:
|
||||
desc: Build the embedded docsite and copy it to dist/docsite
|
||||
sources:
|
||||
- "docs/*"
|
||||
- "docs/src/**/*"
|
||||
- "docs/docs/**/*"
|
||||
- "docs/static/**/*"
|
||||
generates:
|
||||
- "dist/docsite/**/*"
|
||||
cmds:
|
||||
- cd docs && yarn build-embedded
|
||||
- task: copyfiles:'docs/build/':'dist/docsite'
|
||||
deps:
|
||||
- yarn
|
||||
|
||||
package:
|
||||
desc: Package the application for the current platform.
|
||||
@ -44,6 +99,7 @@ tasks:
|
||||
- yarn build:prod && yarn electron-builder -c electron-builder.config.cjs -p never
|
||||
deps:
|
||||
- yarn
|
||||
- docsite:build:embedded
|
||||
- build:backend
|
||||
|
||||
build:backend:
|
||||
@ -54,16 +110,22 @@ tasks:
|
||||
|
||||
build:server:
|
||||
desc: Build the wavesrv component.
|
||||
cmds:
|
||||
- task: build:server:linux
|
||||
- task: build:server:macos
|
||||
- task: build:server:windows
|
||||
deps:
|
||||
- go:mod:tidy
|
||||
- generate
|
||||
- build:server:linux
|
||||
- build:server:macos
|
||||
- build:server:windows
|
||||
sources:
|
||||
- "cmd/server/*.go"
|
||||
- "pkg/**/*.go"
|
||||
generates:
|
||||
- dist/bin/wavesrv.*
|
||||
|
||||
build:server:macos:
|
||||
desc: Build the wavesrv component for macOS (Darwin) platforms (generates artifacts for both arm64 and amd64).
|
||||
status:
|
||||
- exit {{if eq OS "darwin"}}1{{else}}0{{end}}
|
||||
platforms: [darwin]
|
||||
cmds:
|
||||
- cmd: "{{.RM}} dist/bin/wavesrv*"
|
||||
ignore_error: true
|
||||
@ -73,8 +135,7 @@ tasks:
|
||||
|
||||
build:server:windows:
|
||||
desc: Build the wavesrv component for Windows platforms (only generates artifacts for the current architecture).
|
||||
status:
|
||||
- exit {{if eq OS "windows"}}1{{else}}0{{end}}
|
||||
platforms: [windows]
|
||||
cmds:
|
||||
- cmd: "{{.RM}} dist/bin/wavesrv*"
|
||||
ignore_error: true
|
||||
@ -85,8 +146,7 @@ tasks:
|
||||
|
||||
build:server:linux:
|
||||
desc: Build the wavesrv component for Linux platforms (only generates artifacts for the current architecture).
|
||||
status:
|
||||
- exit {{if eq OS "linux"}}1{{else}}0{{end}}
|
||||
platforms: [linux]
|
||||
cmds:
|
||||
- cmd: "{{.RM}} dist/bin/wavesrv*"
|
||||
ignore_error: true
|
||||
@ -107,13 +167,6 @@ tasks:
|
||||
var: ARCHS
|
||||
split: ","
|
||||
as: GOARCH
|
||||
sources:
|
||||
- "cmd/server/*.go"
|
||||
- "pkg/**/*.go"
|
||||
generates:
|
||||
- dist/bin/wavesrv.*{{exeExt}}
|
||||
deps:
|
||||
- go:mod:tidy
|
||||
internal: true
|
||||
|
||||
build:wsh:
|
||||
@ -146,19 +199,13 @@ tasks:
|
||||
GOOS: windows
|
||||
GOARCH: arm64
|
||||
deps:
|
||||
- go:mod:tidy
|
||||
- generate
|
||||
|
||||
dev:installwsh:
|
||||
desc: quick shortcut to rebuild wsh and install for macos arm64
|
||||
requires:
|
||||
vars:
|
||||
- VERSION
|
||||
cmds:
|
||||
- task: build:wsh:internal
|
||||
vars:
|
||||
GOOS: darwin
|
||||
GOARCH: arm64
|
||||
- cp dist/bin/wsh-{{.VERSION}}-darwin.arm64 ~/.waveterm-dev/bin/wsh
|
||||
sources:
|
||||
- "cmd/wsh/**/*.go"
|
||||
- "pkg/**/*.go"
|
||||
generates:
|
||||
- dist/bin/wsh-*
|
||||
|
||||
build:wsh:internal:
|
||||
vars:
|
||||
@ -171,14 +218,7 @@ tasks:
|
||||
- GOOS
|
||||
- GOARCH
|
||||
- VERSION
|
||||
sources:
|
||||
- "cmd/wsh/**/*.go"
|
||||
- "pkg/**/*.go"
|
||||
generates:
|
||||
- dist/bin/wsh-{{.VERSION}}-{{.GOOS}}.{{.NORMALIZEDARCH}}{{.EXT}}
|
||||
cmd: (CGO_ENABLED=0 GOOS={{.GOOS}} GOARCH={{.GOARCH}} go build -ldflags="-s -w -X main.BuildTime=$({{.DATE}} +'%Y%m%d%H%M') -X main.WaveVersion={{.VERSION}}" -o dist/bin/wsh-{{.VERSION}}-{{.GOOS}}.{{.NORMALIZEDARCH}}{{.EXT}} cmd/wsh/main-wsh.go)
|
||||
deps:
|
||||
- go:mod:tidy
|
||||
internal: true
|
||||
|
||||
generate:
|
||||
@ -229,6 +269,62 @@ tasks:
|
||||
echo "https://$SUFFIX"
|
||||
fi
|
||||
done
|
||||
artifacts:snap:publish:*:
|
||||
desc: Publishes the specified artifacts version to Snapcraft.
|
||||
vars:
|
||||
UP_VERSION: '{{ replace "v" "" (index .MATCH 0)}}'
|
||||
CHANNEL: '{{if contains "beta" .UP_VERSION}}beta{{else}}beta,stable{{end}}'
|
||||
cmd: |
|
||||
echo "Releasing to channels: [{{.CHANNEL}}]"
|
||||
snapcraft upload --release={{.CHANNEL}} waveterm_{{.UP_VERSION}}_arm64.snap
|
||||
snapcraft upload --release={{.CHANNEL}} waveterm_{{.UP_VERSION}}_amd64.snap
|
||||
|
||||
artifacts:winget:publish:*:
|
||||
desc: Submits a version bump request to WinGet for the latest release.
|
||||
status:
|
||||
- exit {{if contains "beta" .UP_VERSION}}0{{else}}1{{end}}
|
||||
vars:
|
||||
UP_VERSION: '{{ replace "v" "" (index .MATCH 0)}}'
|
||||
cmd: |
|
||||
wingetcreate update {{.WINGET_PACKAGE}} -s -v {{.UP_VERSION}} -u "https://{{.RELEASES_BUCKET}}/{{.APP_NAME}}-win32-x64-{{.UP_VERSION}}.msi" -t $env:GITHUB_TOKEN
|
||||
|
||||
dev:installwsh:
|
||||
desc: quick shortcut to rebuild wsh and install for macos arm64
|
||||
requires:
|
||||
vars:
|
||||
- VERSION
|
||||
cmds:
|
||||
- task: build:wsh:internal
|
||||
vars:
|
||||
GOOS: darwin
|
||||
GOARCH: arm64
|
||||
- cp dist/bin/wsh-{{.VERSION}}-darwin.arm64 ~/Library/Application\ Support/waveterm-dev/bin/wsh
|
||||
|
||||
dev:clearconfig:
|
||||
desc: Clear the config directory for waveterm-dev
|
||||
cmd: "{{.RMRF}} ~/.config/waveterm-dev"
|
||||
|
||||
dev:cleardata:
|
||||
desc: Clear the data directory for waveterm-dev
|
||||
cmds:
|
||||
- task: dev:cleardata:windows
|
||||
- task: dev:cleardata:linux
|
||||
- task: dev:cleardata:macos
|
||||
|
||||
dev:cleardata:windows:
|
||||
internal: true
|
||||
platforms: [windows]
|
||||
cmd: '{{.RMRF}} %LOCALAPPDATA%\waveterm-dev\Data'
|
||||
|
||||
dev:cleardata:linux:
|
||||
internal: true
|
||||
platforms: [linux]
|
||||
cmd: "rm -rf ~/.local/share/waveterm-dev"
|
||||
|
||||
dev:cleardata:macos:
|
||||
internal: true
|
||||
platforms: [darwin]
|
||||
cmd: 'rm -rf ~/Library/Application\ Support/waveterm-dev'
|
||||
|
||||
yarn:
|
||||
desc: Runs `yarn`
|
||||
@ -251,3 +347,8 @@ tasks:
|
||||
sources:
|
||||
- go.mod
|
||||
cmd: go mod tidy
|
||||
|
||||
copyfiles:*:*:
|
||||
desc: Recursively copy directory and its contents.
|
||||
internal: true
|
||||
cmd: '{{if eq OS "windows"}}powershell Copy-Item -Recurse -Force -Path {{index .MATCH 0}} -Destination {{index .MATCH 1}}{{else}}mkdir -p "$(dirname {{index .MATCH 1}})" && cp -r {{index .MATCH 0}} {{index .MATCH 1}}{{end}}'
|
||||
|
Before (image error) Size: 34 KiB After (image error) Size: 37 KiB |
@ -1,20 +1,84 @@
|
||||
<svg width="1024" height="727" viewBox="0 0 1024 727" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M328.87 332.526C280.49 332.526 253.744 364.034 239.191 434.533L20.5 403.025C47.2464 172.231 136.925 60.3787 314.317 60.3787C445.688 60.3787 574.307 160.022 637.24 160.022C686.012 160.022 712.365 126.151 726.918 58.0156L945.609 89.5233C921.223 320.711 829.184 432.563 651.793 432.563C518.454 432.17 394.556 332.526 328.87 332.526Z"
|
||||
fill="url(#paint0_linear_1814_3217)" />
|
||||
<path
|
||||
d="M390.87 558.061C342.49 558.061 315.744 589.569 301.191 660.067L82.5 628.559C109.246 397.765 198.925 285.519 376.317 285.519C507.295 285.519 636.307 385.162 699.239 385.162C748.012 385.162 774.365 351.292 788.918 283.156L1007.61 314.664C983.223 545.852 891.184 657.704 713.793 657.704C580.454 657.704 456.556 558.061 390.87 558.061Z"
|
||||
fill="url(#paint1_linear_1814_3217)" />
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_1814_3217" x1="20.5503" y1="246.309" x2="945.797" y2="246.309"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.1418" stop-color="#1F4D22" />
|
||||
<stop offset="0.8656" stop-color="#418D31" />
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_1814_3217" x1="82.5673" y1="471.774" x2="1007.81" y2="471.774"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.2223" stop-color="#418D31" />
|
||||
<stop offset="0.7733" stop-color="#58C142" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="1024"
|
||||
height="1024"
|
||||
viewBox="0 0 1024 1024"
|
||||
fill="none"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
sodipodi:docname="appicon-windows.svg"
|
||||
inkscape:version="1.4 (e7c3feb1, 2024-10-09)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview5"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
showgrid="false"
|
||||
borderlayer="false"
|
||||
inkscape:zoom="0.99449794"
|
||||
inkscape:cx="511.31328"
|
||||
inkscape:cy="524.3852"
|
||||
inkscape:window-width="2288"
|
||||
inkscape:window-height="1186"
|
||||
inkscape:window-x="3103"
|
||||
inkscape:window-y="25"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg5" />
|
||||
<g
|
||||
id="g5"
|
||||
transform="translate(-2.055,152.9587)">
|
||||
<path
|
||||
d="m 328.87,332.526 c -48.38,0 -75.126,31.508 -89.679,102.007 L 20.5,403.025 C 47.2464,172.231 136.925,60.3787 314.317,60.3787 c 131.371,0 259.99,99.6433 322.923,99.6433 48.772,0 75.125,-33.871 89.678,-102.0064 L 945.609,89.5233 C 921.223,320.711 829.184,432.563 651.793,432.563 518.454,432.17 394.556,332.526 328.87,332.526 Z"
|
||||
fill="url(#paint0_linear_1814_3217)"
|
||||
id="path1"
|
||||
style="fill:url(#paint0_linear_1814_3217)" />
|
||||
<path
|
||||
d="m 390.87,558.061 c -48.38,0 -75.126,31.508 -89.679,102.006 L 82.5,628.559 c 26.746,-230.794 116.425,-343.04 293.817,-343.04 130.978,0 259.99,99.643 322.922,99.643 48.773,0 75.126,-33.87 89.679,-102.006 l 218.692,31.508 c -24.387,231.188 -116.426,343.04 -293.817,343.04 -133.339,0 -257.237,-99.643 -322.923,-99.643 z"
|
||||
fill="url(#paint1_linear_1814_3217)"
|
||||
id="path2"
|
||||
style="fill:url(#paint1_linear_1814_3217)" />
|
||||
</g>
|
||||
<defs
|
||||
id="defs5">
|
||||
<linearGradient
|
||||
id="paint0_linear_1814_3217"
|
||||
x1="20.550301"
|
||||
y1="246.30901"
|
||||
x2="945.797"
|
||||
y2="246.30901"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop
|
||||
offset="0.1418"
|
||||
stop-color="#1F4D22"
|
||||
id="stop2" />
|
||||
<stop
|
||||
offset="0.8656"
|
||||
stop-color="#418D31"
|
||||
id="stop3" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint1_linear_1814_3217"
|
||||
x1="82.567299"
|
||||
y1="471.77399"
|
||||
x2="1007.81"
|
||||
y2="471.77399"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop
|
||||
offset="0.2223"
|
||||
stop-color="#418D31"
|
||||
id="stop4" />
|
||||
<stop
|
||||
offset="0.7733"
|
||||
stop-color="#58C142"
|
||||
id="stop5" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before (image error) Size: 1.4 KiB After (image error) Size: 2.8 KiB |
27
build/entitlements.mac.plist
Normal file
@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<!-- required for electron -->
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.disable-library-validation</key>
|
||||
<true/>
|
||||
|
||||
<!-- we add the following entitlements so that *CLI* applications can request/use these features. this matches iTerm's permission set -->
|
||||
<key>com.apple.security.device.audio-input</key>
|
||||
<true/>
|
||||
<key>com.apple.security.device.camera</key>
|
||||
<true/>
|
||||
<key>com.apple.security.personal-information.addressbook</key>
|
||||
<true/>
|
||||
<key>com.apple.security.personal-information.calendars</key>
|
||||
<true/>
|
||||
<key>com.apple.security.personal-information.location</key>
|
||||
<true/>
|
||||
<key>com.apple.security.personal-information.photos-library</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
BIN
build/icons/128x128.png
Normal file
After (image error) Size: 6.2 KiB |
BIN
build/icons/16x16.png
Normal file
After (image error) Size: 1.1 KiB |
BIN
build/icons/256x256.png
Normal file
After (image error) Size: 13 KiB |
BIN
build/icons/32x32.png
Normal file
After (image error) Size: 1.8 KiB |
BIN
build/icons/512x512.png
Normal file
After (image error) Size: 31 KiB |
@ -27,8 +27,8 @@ func GenerateWshClient() error {
|
||||
"github.com/wavetermdev/waveterm/pkg/wshutil",
|
||||
"github.com/wavetermdev/waveterm/pkg/wshrpc",
|
||||
"github.com/wavetermdev/waveterm/pkg/waveobj",
|
||||
"github.com/wavetermdev/waveterm/pkg/wconfig",
|
||||
"github.com/wavetermdev/waveterm/pkg/wps",
|
||||
"github.com/wavetermdev/waveterm/pkg/vdom",
|
||||
})
|
||||
wshDeclMap := wshrpc.GenerateWshCommandDeclMap()
|
||||
for _, key := range utilfn.GetOrderedMapKeys(wshDeclMap) {
|
||||
|
@ -9,8 +9,6 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
|
||||
"runtime"
|
||||
"sync"
|
||||
@ -20,6 +18,8 @@ import (
|
||||
"github.com/wavetermdev/waveterm/pkg/authkey"
|
||||
"github.com/wavetermdev/waveterm/pkg/blockcontroller"
|
||||
"github.com/wavetermdev/waveterm/pkg/filestore"
|
||||
"github.com/wavetermdev/waveterm/pkg/panichandler"
|
||||
"github.com/wavetermdev/waveterm/pkg/remote/conncontroller"
|
||||
"github.com/wavetermdev/waveterm/pkg/service"
|
||||
"github.com/wavetermdev/waveterm/pkg/telemetry"
|
||||
"github.com/wavetermdev/waveterm/pkg/util/shellutil"
|
||||
@ -35,6 +35,7 @@ import (
|
||||
"github.com/wavetermdev/waveterm/pkg/wshrpc/wshremote"
|
||||
"github.com/wavetermdev/waveterm/pkg/wshrpc/wshserver"
|
||||
"github.com/wavetermdev/waveterm/pkg/wshutil"
|
||||
"github.com/wavetermdev/waveterm/pkg/wsl"
|
||||
"github.com/wavetermdev/waveterm/pkg/wstore"
|
||||
)
|
||||
|
||||
@ -46,8 +47,6 @@ const InitialTelemetryWait = 10 * time.Second
|
||||
const TelemetryTick = 2 * time.Minute
|
||||
const TelemetryInterval = 4 * time.Hour
|
||||
|
||||
const ReadySignalPidVarName = "WAVETERM_READY_SIGNAL_PID"
|
||||
|
||||
var shutdownOnce sync.Once
|
||||
|
||||
func doShutdown(reason string) {
|
||||
@ -59,6 +58,7 @@ func doShutdown(reason string) {
|
||||
shutdownActivityUpdate()
|
||||
sendTelemetryWrapper()
|
||||
// TODO deal with flush in progress
|
||||
clearTempFiles()
|
||||
filestore.WFS.FlushCache(ctx)
|
||||
watcher := wconfig.GetWatcher()
|
||||
if watcher != nil {
|
||||
@ -112,17 +112,19 @@ func telemetryLoop() {
|
||||
}
|
||||
}
|
||||
|
||||
func panicTelemetryHandler() {
|
||||
activity := wshrpc.ActivityUpdate{NumPanics: 1}
|
||||
err := telemetry.UpdateActivity(context.Background(), activity)
|
||||
if err != nil {
|
||||
log.Printf("error updating activity (panicTelemetryHandler): %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
func sendTelemetryWrapper() {
|
||||
defer func() {
|
||||
r := recover()
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
log.Printf("[error] in sendTelemetryWrapper: %v\n", r)
|
||||
debug.PrintStack()
|
||||
}()
|
||||
defer panichandler.PanicHandler("sendTelemetryWrapper")
|
||||
ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancelFn()
|
||||
beforeSendActivityUpdate(ctx)
|
||||
client, err := wstore.DBGetSingleton[*waveobj.Client](ctx)
|
||||
if err != nil {
|
||||
log.Printf("[error] getting client data for telemetry: %v\n", err)
|
||||
@ -134,13 +136,24 @@ func sendTelemetryWrapper() {
|
||||
}
|
||||
}
|
||||
|
||||
func beforeSendActivityUpdate(ctx context.Context) {
|
||||
activity := wshrpc.ActivityUpdate{}
|
||||
activity.NumTabs, _ = wstore.DBGetCount[*waveobj.Tab](ctx)
|
||||
activity.NumBlocks, _ = wstore.DBGetCount[*waveobj.Block](ctx)
|
||||
activity.Blocks, _ = wstore.DBGetBlockViewCounts(ctx)
|
||||
activity.NumWindows, _ = wstore.DBGetCount[*waveobj.Window](ctx)
|
||||
activity.NumSSHConn = conncontroller.GetNumSSHHasConnected()
|
||||
activity.NumWSLConn = wsl.GetNumWSLHasConnected()
|
||||
err := telemetry.UpdateActivity(ctx, activity)
|
||||
if err != nil {
|
||||
log.Printf("error updating before activity: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
func startupActivityUpdate() {
|
||||
ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancelFn()
|
||||
activity := telemetry.ActivityUpdate{
|
||||
Startup: 1,
|
||||
}
|
||||
activity.NumTabs, _ = wstore.DBGetCount[*waveobj.Tab](ctx)
|
||||
activity := wshrpc.ActivityUpdate{Startup: 1}
|
||||
err := telemetry.UpdateActivity(ctx, activity) // set at least one record into activity (don't use go routine wrap here)
|
||||
if err != nil {
|
||||
log.Printf("error updating startup activity: %v\n", err)
|
||||
@ -148,9 +161,9 @@ func startupActivityUpdate() {
|
||||
}
|
||||
|
||||
func shutdownActivityUpdate() {
|
||||
activity := telemetry.ActivityUpdate{Shutdown: 1}
|
||||
ctx, cancelFn := context.WithTimeout(context.Background(), 1*time.Second)
|
||||
defer cancelFn()
|
||||
activity := wshrpc.ActivityUpdate{Shutdown: 1}
|
||||
err := telemetry.UpdateActivity(ctx, activity) // do NOT use the go routine wrap here (this needs to be synchronous)
|
||||
if err != nil {
|
||||
log.Printf("error updating shutdown activity: %v\n", err)
|
||||
@ -159,11 +172,38 @@ func shutdownActivityUpdate() {
|
||||
|
||||
func createMainWshClient() {
|
||||
rpc := wshserver.GetMainRpcClient()
|
||||
wshutil.DefaultRouter.RegisterRoute(wshutil.DefaultRoute, rpc)
|
||||
wshutil.DefaultRouter.RegisterRoute(wshutil.DefaultRoute, rpc, true)
|
||||
wps.Broker.SetClient(wshutil.DefaultRouter)
|
||||
localConnWsh := wshutil.MakeWshRpc(nil, nil, wshrpc.RpcContext{Conn: wshrpc.LocalConnName}, &wshremote.ServerImpl{})
|
||||
go wshremote.RunSysInfoLoop(localConnWsh, wshrpc.LocalConnName)
|
||||
wshutil.DefaultRouter.RegisterRoute(wshutil.MakeConnectionRouteId(wshrpc.LocalConnName), localConnWsh)
|
||||
wshutil.DefaultRouter.RegisterRoute(wshutil.MakeConnectionRouteId(wshrpc.LocalConnName), localConnWsh, true)
|
||||
}
|
||||
|
||||
func grabAndRemoveEnvVars() error {
|
||||
err := authkey.SetAuthKeyFromEnv()
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting auth key: %v", err)
|
||||
}
|
||||
err = wavebase.CacheAndRemoveEnvVars()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = wcloud.CacheAndRemoveEnvVars()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func clearTempFiles() error {
|
||||
ctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancelFn()
|
||||
client, err := wstore.DBGetSingleton[*waveobj.Client](ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting client: %v", err)
|
||||
}
|
||||
filestore.WFS.DeleteZone(ctx, client.TempOID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
@ -172,9 +212,9 @@ func main() {
|
||||
wavebase.WaveVersion = WaveVersion
|
||||
wavebase.BuildTime = BuildTime
|
||||
|
||||
err := authkey.SetAuthKeyFromEnv()
|
||||
err := grabAndRemoveEnvVars()
|
||||
if err != nil {
|
||||
log.Printf("error setting auth key: %v\n", err)
|
||||
log.Printf("[error] %v\n", err)
|
||||
return
|
||||
}
|
||||
err = service.ValidateServiceMap()
|
||||
@ -182,7 +222,7 @@ func main() {
|
||||
log.Printf("error validating service map: %v\n", err)
|
||||
return
|
||||
}
|
||||
err = wavebase.EnsureWaveHomeDir()
|
||||
err = wavebase.EnsureWaveDataDir()
|
||||
if err != nil {
|
||||
log.Printf("error ensuring wave home dir: %v\n", err)
|
||||
return
|
||||
@ -197,6 +237,13 @@ func main() {
|
||||
log.Printf("error ensuring wave config dir: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: rather than ensure this dir exists, we should let the editor recursively create parent dirs on save
|
||||
err = wavebase.EnsureWavePresetsDir()
|
||||
if err != nil {
|
||||
log.Printf("error ensuring wave presets dir: %v\n", err)
|
||||
return
|
||||
}
|
||||
waveLock, err := wavebase.AcquireWaveLock()
|
||||
if err != nil {
|
||||
log.Printf("error acquiring wave lock (another instance of Wave is likely running): %v\n", err)
|
||||
@ -209,7 +256,8 @@ func main() {
|
||||
}
|
||||
}()
|
||||
log.Printf("wave version: %s (%s)\n", WaveVersion, BuildTime)
|
||||
log.Printf("wave home dir: %s\n", wavebase.GetWaveHomeDir())
|
||||
log.Printf("wave data dir: %s\n", wavebase.GetWaveDataDir())
|
||||
log.Printf("wave config dir: %s\n", wavebase.GetWaveConfigDir())
|
||||
err = filestore.InitFilestore()
|
||||
if err != nil {
|
||||
log.Printf("error initializing filestore: %v\n", err)
|
||||
@ -220,7 +268,9 @@ func main() {
|
||||
log.Printf("error initializing wstore: %v\n", err)
|
||||
return
|
||||
}
|
||||
panichandler.PanicTelemetryHandler = panicTelemetryHandler
|
||||
go func() {
|
||||
defer panichandler.PanicHandler("InitCustomShellStartupFiles")
|
||||
err := shellutil.InitCustomShellStartupFiles()
|
||||
if err != nil {
|
||||
log.Printf("error initializing wsh and shell-integration files: %v\n", err)
|
||||
@ -231,6 +281,11 @@ func main() {
|
||||
log.Printf("error ensuring initial data: %v\n", err)
|
||||
return
|
||||
}
|
||||
err = clearTempFiles()
|
||||
if err != nil {
|
||||
log.Printf("error clearing temp files: %v\n", err)
|
||||
return
|
||||
}
|
||||
if firstRun {
|
||||
migrateErr := wstore.TryMigrateOldHistory()
|
||||
if migrateErr != nil {
|
||||
@ -271,17 +326,11 @@ func main() {
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
pidStr := os.Getenv(ReadySignalPidVarName)
|
||||
if pidStr != "" {
|
||||
_, err := strconv.Atoi(pidStr)
|
||||
if err == nil {
|
||||
if BuildTime == "" {
|
||||
BuildTime = "0"
|
||||
}
|
||||
// use fmt instead of log here to make sure it goes directly to stderr
|
||||
fmt.Fprintf(os.Stderr, "WAVESRV-ESTART ws:%s web:%s version:%s buildtime:%s\n", wsListener.Addr(), webListener.Addr(), WaveVersion, BuildTime)
|
||||
}
|
||||
if BuildTime == "" {
|
||||
BuildTime = "0"
|
||||
}
|
||||
// use fmt instead of log here to make sure it goes directly to stderr
|
||||
fmt.Fprintf(os.Stderr, "WAVESRV-ESTART ws:%s web:%s version:%s buildtime:%s\n", wsListener.Addr(), webListener.Addr(), WaveVersion, BuildTime)
|
||||
}()
|
||||
go wshutil.RunWshRpcOverListener(unixListener)
|
||||
web.RunWebServer(webListener) // blocking
|
||||
|
@ -14,7 +14,7 @@ import (
|
||||
|
||||
func Page(ctx context.Context, props map[string]any) any {
|
||||
clicked, setClicked := vdom.UseState(ctx, false)
|
||||
var clickedDiv *vdom.Elem
|
||||
var clickedDiv *vdom.VDomElem
|
||||
if clicked {
|
||||
clickedDiv = vdom.Bind(`<div>clicked</div>`, nil)
|
||||
}
|
||||
@ -35,7 +35,7 @@ func Page(ctx context.Context, props map[string]any) any {
|
||||
}
|
||||
|
||||
func Button(ctx context.Context, props map[string]any) any {
|
||||
ref := vdom.UseRef(ctx, nil)
|
||||
ref := vdom.UseVDomRef(ctx)
|
||||
clName, setClName := vdom.UseState(ctx, "button")
|
||||
vdom.UseEffect(ctx, func() func() {
|
||||
fmt.Printf("Button useEffect\n")
|
||||
|
157
cmd/wsh/cmd/wshcmd-ai.go
Normal file
@ -0,0 +1,157 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/wavetermdev/waveterm/pkg/waveobj"
|
||||
"github.com/wavetermdev/waveterm/pkg/wshrpc"
|
||||
"github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient"
|
||||
"github.com/wavetermdev/waveterm/pkg/wshutil"
|
||||
)
|
||||
|
||||
var aiCmd = &cobra.Command{
|
||||
Use: "ai [-] [message...]",
|
||||
Short: "Send a message to an AI block",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: aiRun,
|
||||
PreRunE: preRunSetupRpcClient,
|
||||
DisableFlagsInUseLine: true,
|
||||
}
|
||||
|
||||
var aiFileFlags []string
|
||||
var aiNewBlockFlag bool
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(aiCmd)
|
||||
aiCmd.Flags().BoolVarP(&aiNewBlockFlag, "new", "n", false, "create a new AI block")
|
||||
aiCmd.Flags().StringArrayVarP(&aiFileFlags, "file", "f", nil, "attach file content (use '-' for stdin)")
|
||||
}
|
||||
|
||||
func encodeFile(builder *strings.Builder, file io.Reader, fileName string) error {
|
||||
data, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading file: %w", err)
|
||||
}
|
||||
// Start delimiter with the file name
|
||||
builder.WriteString(fmt.Sprintf("\n@@@start file %q\n", fileName))
|
||||
// Read the file content and write it to the builder
|
||||
builder.Write(data)
|
||||
// End delimiter with the file name
|
||||
builder.WriteString(fmt.Sprintf("\n@@@end file %q\n\n", fileName))
|
||||
return nil
|
||||
}
|
||||
|
||||
func aiRun(cmd *cobra.Command, args []string) (rtnErr error) {
|
||||
defer func() {
|
||||
sendActivity("ai", rtnErr == nil)
|
||||
}()
|
||||
|
||||
var stdinUsed bool
|
||||
var message strings.Builder
|
||||
|
||||
// Handle file attachments first
|
||||
for _, file := range aiFileFlags {
|
||||
if file == "-" {
|
||||
if stdinUsed {
|
||||
return fmt.Errorf("stdin (-) can only be used once")
|
||||
}
|
||||
stdinUsed = true
|
||||
if err := encodeFile(&message, os.Stdin, "<stdin>"); err != nil {
|
||||
return fmt.Errorf("reading from stdin: %w", err)
|
||||
}
|
||||
} else {
|
||||
fd, err := os.Open(file)
|
||||
if err != nil {
|
||||
return fmt.Errorf("opening file %s: %w", file, err)
|
||||
}
|
||||
defer fd.Close()
|
||||
if err := encodeFile(&message, fd, file); err != nil {
|
||||
return fmt.Errorf("reading file %s: %w", file, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default to "waveai" block
|
||||
isDefaultBlock := blockArg == ""
|
||||
if isDefaultBlock {
|
||||
blockArg = "view@waveai"
|
||||
}
|
||||
var fullORef *waveobj.ORef
|
||||
var err error
|
||||
if !aiNewBlockFlag {
|
||||
fullORef, err = resolveSimpleId(blockArg)
|
||||
}
|
||||
if (err != nil && isDefaultBlock) || aiNewBlockFlag {
|
||||
// Create new AI block if default block doesn't exist
|
||||
data := &wshrpc.CommandCreateBlockData{
|
||||
BlockDef: &waveobj.BlockDef{
|
||||
Meta: map[string]interface{}{
|
||||
waveobj.MetaKey_View: "waveai",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
newORef, err := wshclient.CreateBlockCommand(RpcClient, *data, &wshrpc.RpcOpts{Timeout: 2000})
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating AI block: %w", err)
|
||||
}
|
||||
fullORef = &newORef
|
||||
// Wait for the block's route to be available
|
||||
gotRoute, err := wshclient.WaitForRouteCommand(RpcClient, wshrpc.CommandWaitForRouteData{
|
||||
RouteId: wshutil.MakeFeBlockRouteId(fullORef.OID),
|
||||
WaitMs: 4000,
|
||||
}, &wshrpc.RpcOpts{Timeout: 5000})
|
||||
if err != nil {
|
||||
return fmt.Errorf("waiting for AI block: %w", err)
|
||||
}
|
||||
if !gotRoute {
|
||||
return fmt.Errorf("AI block route could not be established")
|
||||
}
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("resolving block: %w", err)
|
||||
}
|
||||
|
||||
// Create the route for this block
|
||||
route := wshutil.MakeFeBlockRouteId(fullORef.OID)
|
||||
|
||||
// Then handle main message
|
||||
if args[0] == "-" {
|
||||
if stdinUsed {
|
||||
return fmt.Errorf("stdin (-) can only be used once")
|
||||
}
|
||||
data, err := io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading from stdin: %w", err)
|
||||
}
|
||||
message.Write(data)
|
||||
} else {
|
||||
message.WriteString(strings.Join(args, " "))
|
||||
}
|
||||
|
||||
if message.Len() == 0 {
|
||||
return fmt.Errorf("message is empty")
|
||||
}
|
||||
if message.Len() > 10*1024 {
|
||||
return fmt.Errorf("current max message size is 10k")
|
||||
}
|
||||
|
||||
messageData := wshrpc.AiMessageData{
|
||||
Message: message.String(),
|
||||
}
|
||||
err = wshclient.AiSendMessageCommand(RpcClient, messageData, &wshrpc.RpcOpts{
|
||||
Route: route,
|
||||
Timeout: 2000,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("sending message: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -5,6 +5,7 @@ package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/wavetermdev/waveterm/pkg/remote"
|
||||
@ -13,40 +14,134 @@ import (
|
||||
)
|
||||
|
||||
var connCmd = &cobra.Command{
|
||||
Use: "conn [status|reinstall|disconnect|connect|ensure] [connection-name]",
|
||||
Short: "implements connection commands",
|
||||
Args: cobra.RangeArgs(1, 2),
|
||||
RunE: connRun,
|
||||
Use: "conn",
|
||||
Short: "manage Wave Terminal connections",
|
||||
Long: "Commands to manage Wave Terminal SSH and WSL connections",
|
||||
}
|
||||
|
||||
var connStatusCmd = &cobra.Command{
|
||||
Use: "status",
|
||||
Short: "show status of all connections",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: connStatusRun,
|
||||
PreRunE: preRunSetupRpcClient,
|
||||
}
|
||||
|
||||
var connReinstallCmd = &cobra.Command{
|
||||
Use: "reinstall CONNECTION",
|
||||
Short: "reinstall wsh on a connection",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: connReinstallRun,
|
||||
PreRunE: preRunSetupRpcClient,
|
||||
}
|
||||
|
||||
var connDisconnectCmd = &cobra.Command{
|
||||
Use: "disconnect CONNECTION",
|
||||
Short: "disconnect a connection",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: connDisconnectRun,
|
||||
PreRunE: preRunSetupRpcClient,
|
||||
}
|
||||
|
||||
var connDisconnectAllCmd = &cobra.Command{
|
||||
Use: "disconnectall",
|
||||
Short: "disconnect all connections",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: connDisconnectAllRun,
|
||||
PreRunE: preRunSetupRpcClient,
|
||||
}
|
||||
|
||||
var connConnectCmd = &cobra.Command{
|
||||
Use: "connect CONNECTION",
|
||||
Short: "connect to a connection",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: connConnectRun,
|
||||
PreRunE: preRunSetupRpcClient,
|
||||
}
|
||||
|
||||
var connEnsureCmd = &cobra.Command{
|
||||
Use: "ensure CONNECTION",
|
||||
Short: "ensure wsh is installed on a connection",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: connEnsureRun,
|
||||
PreRunE: preRunSetupRpcClient,
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(connCmd)
|
||||
connCmd.AddCommand(connStatusCmd)
|
||||
connCmd.AddCommand(connReinstallCmd)
|
||||
connCmd.AddCommand(connDisconnectCmd)
|
||||
connCmd.AddCommand(connDisconnectAllCmd)
|
||||
connCmd.AddCommand(connConnectCmd)
|
||||
connCmd.AddCommand(connEnsureCmd)
|
||||
}
|
||||
|
||||
func connStatus() error {
|
||||
resp, err := wshclient.ConnStatusCommand(RpcClient, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting connection status: %w", err)
|
||||
func validateConnectionName(name string) error {
|
||||
if !strings.HasPrefix(name, "wsl://") {
|
||||
_, err := remote.ParseOpts(name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot parse connection name: %w", err)
|
||||
}
|
||||
}
|
||||
if len(resp) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
func connStatusRun(cmd *cobra.Command, args []string) error {
|
||||
var allResp []wshrpc.ConnStatus
|
||||
sshResp, err := wshclient.ConnStatusCommand(RpcClient, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting ssh connection status: %w", err)
|
||||
}
|
||||
allResp = append(allResp, sshResp...)
|
||||
wslResp, err := wshclient.WslStatusCommand(RpcClient, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting wsl connection status: %w", err)
|
||||
}
|
||||
allResp = append(allResp, wslResp...)
|
||||
if len(allResp) == 0 {
|
||||
WriteStdout("no connections\n")
|
||||
return nil
|
||||
}
|
||||
WriteStdout("%-30s %-12s\n", "connection", "status")
|
||||
WriteStdout("----------------------------------------------\n")
|
||||
for _, conn := range resp {
|
||||
for _, conn := range allResp {
|
||||
str := fmt.Sprintf("%-30s %-12s", conn.Connection, conn.Status)
|
||||
if conn.Error != "" {
|
||||
str += fmt.Sprintf(" (%s)", conn.Error)
|
||||
}
|
||||
str += "\n"
|
||||
WriteStdout("%s\n", str)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func connDisconnectAll() error {
|
||||
func connReinstallRun(cmd *cobra.Command, args []string) error {
|
||||
connName := args[0]
|
||||
if err := validateConnectionName(connName); err != nil {
|
||||
return err
|
||||
}
|
||||
err := wshclient.ConnReinstallWshCommand(RpcClient, connName, &wshrpc.RpcOpts{Timeout: 60000})
|
||||
if err != nil {
|
||||
return fmt.Errorf("reinstalling connection: %w", err)
|
||||
}
|
||||
WriteStdout("wsh reinstalled on connection %q\n", connName)
|
||||
return nil
|
||||
}
|
||||
|
||||
func connDisconnectRun(cmd *cobra.Command, args []string) error {
|
||||
connName := args[0]
|
||||
if err := validateConnectionName(connName); err != nil {
|
||||
return err
|
||||
}
|
||||
err := wshclient.ConnDisconnectCommand(RpcClient, connName, &wshrpc.RpcOpts{Timeout: 10000})
|
||||
if err != nil {
|
||||
return fmt.Errorf("disconnecting %q error: %w", connName, err)
|
||||
}
|
||||
WriteStdout("disconnected %q\n", connName)
|
||||
return nil
|
||||
}
|
||||
|
||||
func connDisconnectAllRun(cmd *cobra.Command, args []string) error {
|
||||
resp, err := wshclient.ConnStatusCommand(RpcClient, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting connection status: %w", err)
|
||||
@ -56,44 +151,23 @@ func connDisconnectAll() error {
|
||||
}
|
||||
for _, conn := range resp {
|
||||
if conn.Status == "connected" {
|
||||
err := connDisconnect(conn.Connection)
|
||||
err := wshclient.ConnDisconnectCommand(RpcClient, conn.Connection, &wshrpc.RpcOpts{Timeout: 10000})
|
||||
if err != nil {
|
||||
WriteStdout("error disconnecting %q: %v\n", conn.Connection, err)
|
||||
} else {
|
||||
WriteStdout("disconnected %q\n", conn.Connection)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func connEnsure(connName string) error {
|
||||
err := wshclient.ConnEnsureCommand(RpcClient, connName, &wshrpc.RpcOpts{Timeout: 60000})
|
||||
if err != nil {
|
||||
return fmt.Errorf("ensuring connection: %w", err)
|
||||
func connConnectRun(cmd *cobra.Command, args []string) error {
|
||||
connName := args[0]
|
||||
if err := validateConnectionName(connName); err != nil {
|
||||
return err
|
||||
}
|
||||
WriteStdout("wsh ensured on connection %q\n", connName)
|
||||
return nil
|
||||
}
|
||||
|
||||
func connReinstall(connName string) error {
|
||||
err := wshclient.ConnReinstallWshCommand(RpcClient, connName, &wshrpc.RpcOpts{Timeout: 60000})
|
||||
if err != nil {
|
||||
return fmt.Errorf("reinstalling connection: %w", err)
|
||||
}
|
||||
WriteStdout("wsh reinstalled on connection %q\n", connName)
|
||||
return nil
|
||||
}
|
||||
|
||||
func connDisconnect(connName string) error {
|
||||
err := wshclient.ConnDisconnectCommand(RpcClient, connName, &wshrpc.RpcOpts{Timeout: 10000})
|
||||
if err != nil {
|
||||
return fmt.Errorf("disconnecting %q error: %w", connName, err)
|
||||
}
|
||||
WriteStdout("disconnected %q\n", connName)
|
||||
return nil
|
||||
}
|
||||
|
||||
func connConnect(connName string) error {
|
||||
err := wshclient.ConnConnectCommand(RpcClient, connName, &wshrpc.RpcOpts{Timeout: 60000})
|
||||
err := wshclient.ConnConnectCommand(RpcClient, wshrpc.ConnRequest{Host: connName}, &wshrpc.RpcOpts{Timeout: 60000})
|
||||
if err != nil {
|
||||
return fmt.Errorf("connecting connection: %w", err)
|
||||
}
|
||||
@ -101,32 +175,15 @@ func connConnect(connName string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func connRun(cmd *cobra.Command, args []string) error {
|
||||
connCmd := args[0]
|
||||
var connName string
|
||||
if connCmd != "status" && connCmd != "disconnectall" {
|
||||
if len(args) < 2 {
|
||||
return fmt.Errorf("connection name is required %q", connCmd)
|
||||
}
|
||||
connName = args[1]
|
||||
_, err := remote.ParseOpts(connName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot parse connection name: %w", err)
|
||||
}
|
||||
func connEnsureRun(cmd *cobra.Command, args []string) error {
|
||||
connName := args[0]
|
||||
if err := validateConnectionName(connName); err != nil {
|
||||
return err
|
||||
}
|
||||
if connCmd == "status" {
|
||||
return connStatus()
|
||||
} else if connCmd == "ensure" {
|
||||
return connEnsure(connName)
|
||||
} else if connCmd == "reinstall" {
|
||||
return connReinstall(connName)
|
||||
} else if connCmd == "disconnect" {
|
||||
return connDisconnect(connName)
|
||||
} else if connCmd == "disconnectall" {
|
||||
return connDisconnectAll()
|
||||
} else if connCmd == "connect" {
|
||||
return connConnect(connName)
|
||||
} else {
|
||||
return fmt.Errorf("unknown command %q", connCmd)
|
||||
err := wshclient.ConnEnsureCommand(RpcClient, connName, &wshrpc.RpcOpts{Timeout: 60000})
|
||||
if err != nil {
|
||||
return fmt.Errorf("ensuring connection: %w", err)
|
||||
}
|
||||
WriteStdout("wsh ensured on connection %q\n", connName)
|
||||
return nil
|
||||
}
|
||||
|
@ -4,29 +4,190 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/wavetermdev/waveterm/pkg/panichandler"
|
||||
"github.com/wavetermdev/waveterm/pkg/util/packetparser"
|
||||
"github.com/wavetermdev/waveterm/pkg/wavebase"
|
||||
"github.com/wavetermdev/waveterm/pkg/wshrpc"
|
||||
"github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient"
|
||||
"github.com/wavetermdev/waveterm/pkg/wshrpc/wshremote"
|
||||
"github.com/wavetermdev/waveterm/pkg/wshutil"
|
||||
)
|
||||
|
||||
var serverCmd = &cobra.Command{
|
||||
Use: "connserver",
|
||||
Hidden: true,
|
||||
Short: "remote server to power wave blocks",
|
||||
Args: cobra.NoArgs,
|
||||
Run: serverRun,
|
||||
PreRunE: preRunSetupRpcClient,
|
||||
Use: "connserver",
|
||||
Hidden: true,
|
||||
Short: "remote server to power wave blocks",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: serverRun,
|
||||
}
|
||||
|
||||
var connServerRouter bool
|
||||
|
||||
func init() {
|
||||
serverCmd.Flags().BoolVar(&connServerRouter, "router", false, "run in local router mode")
|
||||
rootCmd.AddCommand(serverCmd)
|
||||
}
|
||||
|
||||
func serverRun(cmd *cobra.Command, args []string) {
|
||||
func MakeRemoteUnixListener() (net.Listener, error) {
|
||||
serverAddr := wavebase.GetRemoteDomainSocketName()
|
||||
os.Remove(serverAddr) // ignore error
|
||||
rtn, err := net.Listen("unix", serverAddr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating listener at %v: %v", serverAddr, err)
|
||||
}
|
||||
os.Chmod(serverAddr, 0700)
|
||||
log.Printf("Server [unix-domain] listening on %s\n", serverAddr)
|
||||
return rtn, nil
|
||||
}
|
||||
|
||||
func handleNewListenerConn(conn net.Conn, router *wshutil.WshRouter) {
|
||||
var routeIdContainer atomic.Pointer[string]
|
||||
proxy := wshutil.MakeRpcProxy()
|
||||
go func() {
|
||||
defer panichandler.PanicHandler("handleNewListenerConn:AdaptOutputChToStream")
|
||||
writeErr := wshutil.AdaptOutputChToStream(proxy.ToRemoteCh, conn)
|
||||
if writeErr != nil {
|
||||
log.Printf("error writing to domain socket: %v\n", writeErr)
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
// when input is closed, close the connection
|
||||
defer panichandler.PanicHandler("handleNewListenerConn:AdaptStreamToMsgCh")
|
||||
defer func() {
|
||||
conn.Close()
|
||||
routeIdPtr := routeIdContainer.Load()
|
||||
if routeIdPtr != nil && *routeIdPtr != "" {
|
||||
router.UnregisterRoute(*routeIdPtr)
|
||||
disposeMsg := &wshutil.RpcMessage{
|
||||
Command: wshrpc.Command_Dispose,
|
||||
Data: wshrpc.CommandDisposeData{
|
||||
RouteId: *routeIdPtr,
|
||||
},
|
||||
Source: *routeIdPtr,
|
||||
AuthToken: proxy.GetAuthToken(),
|
||||
}
|
||||
disposeBytes, _ := json.Marshal(disposeMsg)
|
||||
router.InjectMessage(disposeBytes, *routeIdPtr)
|
||||
}
|
||||
}()
|
||||
wshutil.AdaptStreamToMsgCh(conn, proxy.FromRemoteCh)
|
||||
}()
|
||||
routeId, err := proxy.HandleClientProxyAuth(router)
|
||||
if err != nil {
|
||||
log.Printf("error handling client proxy auth: %v\n", err)
|
||||
conn.Close()
|
||||
return
|
||||
}
|
||||
router.RegisterRoute(routeId, proxy, false)
|
||||
routeIdContainer.Store(&routeId)
|
||||
}
|
||||
|
||||
func runListener(listener net.Listener, router *wshutil.WshRouter) {
|
||||
defer func() {
|
||||
log.Printf("listener closed, exiting\n")
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
wshutil.DoShutdown("", 1, true)
|
||||
}()
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
log.Printf("error accepting connection: %v\n", err)
|
||||
continue
|
||||
}
|
||||
go handleNewListenerConn(conn, router)
|
||||
}
|
||||
}
|
||||
|
||||
func setupConnServerRpcClientWithRouter(router *wshutil.WshRouter) (*wshutil.WshRpc, error) {
|
||||
jwtToken := os.Getenv(wshutil.WaveJwtTokenVarName)
|
||||
if jwtToken == "" {
|
||||
return nil, fmt.Errorf("no jwt token found for connserver")
|
||||
}
|
||||
rpcCtx, err := wshutil.ExtractUnverifiedRpcContext(jwtToken)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error extracting rpc context from %s: %v", wshutil.WaveJwtTokenVarName, err)
|
||||
}
|
||||
authRtn, err := router.HandleProxyAuth(jwtToken)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error handling proxy auth: %v", err)
|
||||
}
|
||||
inputCh := make(chan []byte, wshutil.DefaultInputChSize)
|
||||
outputCh := make(chan []byte, wshutil.DefaultOutputChSize)
|
||||
connServerClient := wshutil.MakeWshRpc(inputCh, outputCh, *rpcCtx, &wshremote.ServerImpl{LogWriter: os.Stdout})
|
||||
connServerClient.SetAuthToken(authRtn.AuthToken)
|
||||
router.RegisterRoute(authRtn.RouteId, connServerClient, false)
|
||||
wshclient.RouteAnnounceCommand(connServerClient, nil)
|
||||
return connServerClient, nil
|
||||
}
|
||||
|
||||
func serverRunRouter() error {
|
||||
router := wshutil.NewWshRouter()
|
||||
termProxy := wshutil.MakeRpcProxy()
|
||||
rawCh := make(chan []byte, wshutil.DefaultOutputChSize)
|
||||
go packetparser.Parse(os.Stdin, termProxy.FromRemoteCh, rawCh)
|
||||
go func() {
|
||||
defer panichandler.PanicHandler("serverRunRouter:WritePackets")
|
||||
for msg := range termProxy.ToRemoteCh {
|
||||
packetparser.WritePacket(os.Stdout, msg)
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
// just ignore and drain the rawCh (stdin)
|
||||
// when stdin is closed, shutdown
|
||||
defer wshutil.DoShutdown("", 0, true)
|
||||
for range rawCh {
|
||||
// ignore
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
for msg := range termProxy.FromRemoteCh {
|
||||
// send this to the router
|
||||
router.InjectMessage(msg, wshutil.UpstreamRoute)
|
||||
}
|
||||
}()
|
||||
router.SetUpstreamClient(termProxy)
|
||||
// now set up the domain socket
|
||||
unixListener, err := MakeRemoteUnixListener()
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot create unix listener: %v", err)
|
||||
}
|
||||
client, err := setupConnServerRpcClientWithRouter(router)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error setting up connserver rpc client: %v", err)
|
||||
}
|
||||
go runListener(unixListener, router)
|
||||
// run the sysinfo loop
|
||||
wshremote.RunSysInfoLoop(client, client.GetRpcContext().Conn)
|
||||
select {}
|
||||
}
|
||||
|
||||
func serverRunNormal() error {
|
||||
err := setupRpcClient(&wshremote.ServerImpl{LogWriter: os.Stdout})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
WriteStdout("running wsh connserver (%s)\n", RpcContext.Conn)
|
||||
go wshremote.RunSysInfoLoop(RpcClient, RpcContext.Conn)
|
||||
RpcClient.SetServerImpl(&wshremote.ServerImpl{LogWriter: os.Stdout})
|
||||
|
||||
select {} // run forever
|
||||
}
|
||||
|
||||
func serverRun(cmd *cobra.Command, args []string) error {
|
||||
if connServerRouter {
|
||||
return serverRunRouter()
|
||||
} else {
|
||||
return serverRunNormal()
|
||||
}
|
||||
}
|
||||
|
47
cmd/wsh/cmd/wshcmd-debug.go
Normal file
@ -0,0 +1,47 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient"
|
||||
)
|
||||
|
||||
var debugCmd = &cobra.Command{
|
||||
Use: "debug",
|
||||
Short: "debug commands",
|
||||
PersistentPreRunE: preRunSetupRpcClient,
|
||||
Hidden: true,
|
||||
}
|
||||
|
||||
var debugBlockIdsCmd = &cobra.Command{
|
||||
Use: "block",
|
||||
Short: "list sub-blockids for block",
|
||||
RunE: debugBlockIdsRun,
|
||||
Hidden: true,
|
||||
}
|
||||
|
||||
func init() {
|
||||
debugCmd.AddCommand(debugBlockIdsCmd)
|
||||
rootCmd.AddCommand(debugCmd)
|
||||
}
|
||||
|
||||
func debugBlockIdsRun(cmd *cobra.Command, args []string) error {
|
||||
oref, err := resolveBlockArg()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
blockInfo, err := wshclient.BlockInfoCommand(RpcClient, oref.OID, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
barr, err := json.MarshalIndent(blockInfo, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
WriteStdout("%s\n", string(barr))
|
||||
return nil
|
||||
}
|
@ -4,6 +4,8 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/wavetermdev/waveterm/pkg/wshrpc"
|
||||
)
|
||||
@ -11,7 +13,7 @@ import (
|
||||
var deleteBlockCmd = &cobra.Command{
|
||||
Use: "deleteblock",
|
||||
Short: "delete a block",
|
||||
Run: deleteBlockRun,
|
||||
RunE: deleteBlockRun,
|
||||
PreRunE: preRunSetupRpcClient,
|
||||
}
|
||||
|
||||
@ -19,29 +21,24 @@ func init() {
|
||||
rootCmd.AddCommand(deleteBlockCmd)
|
||||
}
|
||||
|
||||
func deleteBlockRun(cmd *cobra.Command, args []string) {
|
||||
oref := blockArg
|
||||
err := validateEasyORef(oref)
|
||||
func deleteBlockRun(cmd *cobra.Command, args []string) (rtnErr error) {
|
||||
defer func() {
|
||||
sendActivity("deleteblock", rtnErr == nil)
|
||||
}()
|
||||
fullORef, err := resolveBlockArg()
|
||||
if err != nil {
|
||||
WriteStderr("[error]%v\n", err)
|
||||
return
|
||||
}
|
||||
fullORef, err := resolveSimpleId(oref)
|
||||
if err != nil {
|
||||
WriteStderr("[error] resolving oref: %v\n", err)
|
||||
return
|
||||
return err
|
||||
}
|
||||
if fullORef.OType != "block" {
|
||||
WriteStderr("[error] oref is not a block\n")
|
||||
return
|
||||
return fmt.Errorf("object reference is not a block")
|
||||
}
|
||||
deleteBlockData := &wshrpc.CommandDeleteBlockData{
|
||||
BlockId: fullORef.OID,
|
||||
}
|
||||
_, err = RpcClient.SendRpcRequest(wshrpc.Command_DeleteBlock, deleteBlockData, &wshrpc.RpcOpts{Timeout: 2000})
|
||||
if err != nil {
|
||||
WriteStderr("[error] deleting block: %v\n", err)
|
||||
return
|
||||
return fmt.Errorf("delete block failed: %v", err)
|
||||
}
|
||||
WriteStdout("block deleted\n")
|
||||
return nil
|
||||
}
|
||||
|
62
cmd/wsh/cmd/wshcmd-editconfig.go
Normal file
@ -0,0 +1,62 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/wavetermdev/waveterm/pkg/waveobj"
|
||||
"github.com/wavetermdev/waveterm/pkg/wshrpc"
|
||||
"github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient"
|
||||
)
|
||||
|
||||
var editConfigCmd = &cobra.Command{
|
||||
Use: "editconfig [configfile]",
|
||||
Short: "edit Wave configuration files",
|
||||
Long: "Edit Wave configuration files. Defaults to settings.json if no file specified. Common files: settings.json, presets.json, widgets.json",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: editConfigRun,
|
||||
PreRunE: preRunSetupRpcClient,
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(editConfigCmd)
|
||||
}
|
||||
|
||||
func editConfigRun(cmd *cobra.Command, args []string) (rtnErr error) {
|
||||
defer func() {
|
||||
sendActivity("editconfig", rtnErr == nil)
|
||||
}()
|
||||
|
||||
// Get config directory from Wave info
|
||||
resp, err := wshclient.WaveInfoCommand(RpcClient, &wshrpc.RpcOpts{Timeout: 2000})
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting Wave info: %w", err)
|
||||
}
|
||||
|
||||
configFile := "settings.json" // default
|
||||
if len(args) > 0 {
|
||||
configFile = args[0]
|
||||
}
|
||||
|
||||
settingsFile := filepath.Join(resp.ConfigDir, configFile)
|
||||
|
||||
wshCmd := &wshrpc.CommandCreateBlockData{
|
||||
BlockDef: &waveobj.BlockDef{
|
||||
Meta: map[string]interface{}{
|
||||
waveobj.MetaKey_View: "preview",
|
||||
waveobj.MetaKey_File: settingsFile,
|
||||
waveobj.MetaKey_Edit: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err = RpcClient.SendRpcRequest(wshrpc.Command_CreateBlock, wshCmd, &wshrpc.RpcOpts{Timeout: 2000})
|
||||
if err != nil {
|
||||
return fmt.Errorf("opening config file: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
@ -4,6 +4,7 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@ -21,7 +22,7 @@ var editorCmd = &cobra.Command{
|
||||
Use: "editor",
|
||||
Short: "edit a file (blocks until editor is closed)",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: editorRun,
|
||||
RunE: editorRun,
|
||||
PreRunE: preRunSetupRpcClient,
|
||||
}
|
||||
|
||||
@ -30,21 +31,22 @@ func init() {
|
||||
rootCmd.AddCommand(editorCmd)
|
||||
}
|
||||
|
||||
func editorRun(cmd *cobra.Command, args []string) {
|
||||
func editorRun(cmd *cobra.Command, args []string) (rtnErr error) {
|
||||
defer func() {
|
||||
sendActivity("editor", rtnErr == nil)
|
||||
}()
|
||||
|
||||
fileArg := args[0]
|
||||
absFile, err := filepath.Abs(fileArg)
|
||||
if err != nil {
|
||||
WriteStderr("[error] getting absolute path: %v\n", err)
|
||||
return
|
||||
return fmt.Errorf("getting absolute path: %w", err)
|
||||
}
|
||||
_, err = os.Stat(absFile)
|
||||
if err == fs.ErrNotExist {
|
||||
WriteStderr("[error] file does not exist: %q\n", absFile)
|
||||
return
|
||||
return fmt.Errorf("file does not exist: %q", absFile)
|
||||
}
|
||||
if err != nil {
|
||||
WriteStderr("[error] getting file info: %v\n", err)
|
||||
return
|
||||
return fmt.Errorf("getting file info: %w", err)
|
||||
}
|
||||
wshCmd := wshrpc.CommandCreateBlockData{
|
||||
BlockDef: &waveobj.BlockDef{
|
||||
@ -61,15 +63,15 @@ func editorRun(cmd *cobra.Command, args []string) {
|
||||
}
|
||||
blockRef, err := wshclient.CreateBlockCommand(RpcClient, wshCmd, &wshrpc.RpcOpts{Timeout: 2000})
|
||||
if err != nil {
|
||||
WriteStderr("[error] running view command: %v\r\n", err)
|
||||
return
|
||||
return fmt.Errorf("running view command: %w", err)
|
||||
}
|
||||
doneCh := make(chan bool)
|
||||
RpcClient.EventListener.On("blockclose", func(event *wps.WaveEvent) {
|
||||
RpcClient.EventListener.On(wps.Event_BlockClose, func(event *wps.WaveEvent) {
|
||||
if event.HasScope(blockRef.String()) {
|
||||
close(doneCh)
|
||||
}
|
||||
})
|
||||
wshclient.EventSubCommand(RpcClient, wps.SubscriptionRequest{Event: "blockclose", Scopes: []string{blockRef.String()}}, nil)
|
||||
wshclient.EventSubCommand(RpcClient, wps.SubscriptionRequest{Event: wps.Event_BlockClose, Scopes: []string{blockRef.String()}}, nil)
|
||||
<-doneCh
|
||||
return nil
|
||||
}
|
||||
|
212
cmd/wsh/cmd/wshcmd-file-util.go
Normal file
@ -0,0 +1,212 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"strings"
|
||||
|
||||
"github.com/wavetermdev/waveterm/pkg/wshrpc"
|
||||
"github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient"
|
||||
)
|
||||
|
||||
func convertNotFoundErr(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if strings.HasPrefix(err.Error(), "NOTFOUND:") {
|
||||
return fs.ErrNotExist
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func ensureWaveFile(origName string, fileData wshrpc.CommandFileData) (*wshrpc.WaveFileInfo, error) {
|
||||
info, err := wshclient.FileInfoCommand(RpcClient, fileData, &wshrpc.RpcOpts{Timeout: DefaultFileTimeout})
|
||||
err = convertNotFoundErr(err)
|
||||
if err == fs.ErrNotExist {
|
||||
createData := wshrpc.CommandFileCreateData{
|
||||
ZoneId: fileData.ZoneId,
|
||||
FileName: fileData.FileName,
|
||||
}
|
||||
err = wshclient.FileCreateCommand(RpcClient, createData, &wshrpc.RpcOpts{Timeout: DefaultFileTimeout})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating file: %w", err)
|
||||
}
|
||||
info, err = wshclient.FileInfoCommand(RpcClient, fileData, &wshrpc.RpcOpts{Timeout: DefaultFileTimeout})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting file info: %w", err)
|
||||
}
|
||||
return info, err
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting file info: %w", err)
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func streamWriteToWaveFile(fileData wshrpc.CommandFileData, reader io.Reader) error {
|
||||
// First truncate the file with an empty write
|
||||
emptyWrite := fileData
|
||||
emptyWrite.Data64 = ""
|
||||
err := wshclient.FileWriteCommand(RpcClient, emptyWrite, &wshrpc.RpcOpts{Timeout: DefaultFileTimeout})
|
||||
if err != nil {
|
||||
return fmt.Errorf("initializing file with empty write: %w", err)
|
||||
}
|
||||
|
||||
const chunkSize = 32 * 1024 // 32KB chunks
|
||||
buf := make([]byte, chunkSize)
|
||||
totalWritten := int64(0)
|
||||
|
||||
for {
|
||||
n, err := reader.Read(buf)
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading input: %w", err)
|
||||
}
|
||||
|
||||
// Check total size
|
||||
totalWritten += int64(n)
|
||||
if totalWritten > MaxFileSize {
|
||||
return fmt.Errorf("input exceeds maximum file size of %d bytes", MaxFileSize)
|
||||
}
|
||||
|
||||
// Prepare and send chunk
|
||||
chunk := buf[:n]
|
||||
appendData := fileData
|
||||
appendData.Data64 = base64.StdEncoding.EncodeToString(chunk)
|
||||
|
||||
err = wshclient.FileAppendCommand(RpcClient, appendData, &wshrpc.RpcOpts{Timeout: fileTimeout})
|
||||
if err != nil {
|
||||
return fmt.Errorf("appending chunk to file: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func streamReadFromWaveFile(fileData wshrpc.CommandFileData, size int64, writer io.Writer) error {
|
||||
const chunkSize = 32 * 1024 // 32KB chunks
|
||||
for offset := int64(0); offset < size; offset += chunkSize {
|
||||
// Calculate the length of this chunk
|
||||
length := chunkSize
|
||||
if offset+int64(length) > size {
|
||||
length = int(size - offset)
|
||||
}
|
||||
|
||||
// Set up the ReadAt request
|
||||
fileData.At = &wshrpc.CommandFileDataAt{
|
||||
Offset: offset,
|
||||
Size: int64(length),
|
||||
}
|
||||
|
||||
// Read the chunk
|
||||
content64, err := wshclient.FileReadCommand(RpcClient, fileData, &wshrpc.RpcOpts{Timeout: fileTimeout})
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading chunk at offset %d: %w", offset, err)
|
||||
}
|
||||
|
||||
// Decode and write the chunk
|
||||
chunk, err := base64.StdEncoding.DecodeString(content64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("decoding chunk at offset %d: %w", offset, err)
|
||||
}
|
||||
|
||||
_, err = writer.Write(chunk)
|
||||
if err != nil {
|
||||
return fmt.Errorf("writing chunk at offset %d: %w", offset, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type fileListResult struct {
|
||||
info *wshrpc.WaveFileInfo
|
||||
err error
|
||||
}
|
||||
|
||||
func streamFileList(zoneId string, path string, recursive bool, filesOnly bool) (<-chan fileListResult, error) {
|
||||
resultChan := make(chan fileListResult)
|
||||
|
||||
// If path doesn't end in /, do a single file lookup
|
||||
if path != "" && !strings.HasSuffix(path, "/") {
|
||||
go func() {
|
||||
defer close(resultChan)
|
||||
|
||||
fileData := wshrpc.CommandFileData{
|
||||
ZoneId: zoneId,
|
||||
FileName: path,
|
||||
}
|
||||
|
||||
info, err := wshclient.FileInfoCommand(RpcClient, fileData, &wshrpc.RpcOpts{Timeout: 2000})
|
||||
err = convertNotFoundErr(err)
|
||||
if err == fs.ErrNotExist {
|
||||
resultChan <- fileListResult{err: fmt.Errorf("%s: No such file or directory", path)}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
resultChan <- fileListResult{err: err}
|
||||
return
|
||||
}
|
||||
resultChan <- fileListResult{info: info}
|
||||
}()
|
||||
return resultChan, nil
|
||||
}
|
||||
|
||||
// Directory listing case
|
||||
go func() {
|
||||
defer close(resultChan)
|
||||
|
||||
prefix := path
|
||||
prefixLen := len(prefix)
|
||||
offset := 0
|
||||
foundAny := false
|
||||
|
||||
for {
|
||||
listData := wshrpc.CommandFileListData{
|
||||
ZoneId: zoneId,
|
||||
Prefix: prefix,
|
||||
All: recursive,
|
||||
Offset: offset,
|
||||
Limit: 100,
|
||||
}
|
||||
|
||||
files, err := wshclient.FileListCommand(RpcClient, listData, &wshrpc.RpcOpts{Timeout: 2000})
|
||||
if err != nil {
|
||||
resultChan <- fileListResult{err: err}
|
||||
return
|
||||
}
|
||||
|
||||
if len(files) == 0 {
|
||||
if !foundAny && prefix != "" {
|
||||
resultChan <- fileListResult{err: fmt.Errorf("%s: No such file or directory", path)}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
for _, f := range files {
|
||||
if filesOnly && f.IsDir {
|
||||
continue
|
||||
}
|
||||
foundAny = true
|
||||
if prefixLen > 0 {
|
||||
f.Name = f.Name[prefixLen:]
|
||||
}
|
||||
resultChan <- fileListResult{info: f}
|
||||
}
|
||||
|
||||
if len(files) < 100 {
|
||||
return
|
||||
}
|
||||
offset += len(files)
|
||||
}
|
||||
}()
|
||||
|
||||
return resultChan, nil
|
||||
}
|
632
cmd/wsh/cmd/wshcmd-file.go
Normal file
@ -0,0 +1,632 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/wavetermdev/waveterm/pkg/util/colprint"
|
||||
"github.com/wavetermdev/waveterm/pkg/util/utilfn"
|
||||
"github.com/wavetermdev/waveterm/pkg/waveobj"
|
||||
"github.com/wavetermdev/waveterm/pkg/wshrpc"
|
||||
"github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
const (
|
||||
MaxFileSize = 10 * 1024 * 1024 // 10MB
|
||||
WaveFileScheme = "wavefile"
|
||||
WaveFilePrefix = "wavefile://"
|
||||
|
||||
DefaultFileTimeout = 5000
|
||||
)
|
||||
|
||||
var fileCmd = &cobra.Command{
|
||||
Use: "file",
|
||||
Short: "manage Wave Terminal files",
|
||||
Long: "Commands to manage Wave Terminal files stored in blocks",
|
||||
}
|
||||
|
||||
var fileTimeout int
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(fileCmd)
|
||||
|
||||
fileCmd.PersistentFlags().IntVarP(&fileTimeout, "timeout", "t", 15000, "timeout in milliseconds for long operations")
|
||||
|
||||
fileListCmd.Flags().BoolP("recursive", "r", false, "list subdirectories recursively")
|
||||
fileListCmd.Flags().BoolP("long", "l", false, "use long listing format")
|
||||
fileListCmd.Flags().BoolP("one", "1", false, "list one file per line")
|
||||
fileListCmd.Flags().BoolP("files", "f", false, "list files only")
|
||||
|
||||
fileCmd.AddCommand(fileListCmd)
|
||||
fileCmd.AddCommand(fileCatCmd)
|
||||
fileCmd.AddCommand(fileWriteCmd)
|
||||
fileCmd.AddCommand(fileRmCmd)
|
||||
fileCmd.AddCommand(fileInfoCmd)
|
||||
fileCmd.AddCommand(fileAppendCmd)
|
||||
fileCmd.AddCommand(fileCpCmd)
|
||||
}
|
||||
|
||||
type waveFileRef struct {
|
||||
zoneId string
|
||||
fileName string
|
||||
}
|
||||
|
||||
func parseWaveFileURL(fileURL string) (*waveFileRef, error) {
|
||||
if !strings.HasPrefix(fileURL, WaveFilePrefix) {
|
||||
return nil, fmt.Errorf("invalid file reference %q: must use wavefile:// URL format", fileURL)
|
||||
}
|
||||
|
||||
u, err := url.Parse(fileURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid wavefile URL: %w", err)
|
||||
}
|
||||
|
||||
if u.Scheme != WaveFileScheme {
|
||||
return nil, fmt.Errorf("invalid URL scheme %q: must be wavefile://", u.Scheme)
|
||||
}
|
||||
|
||||
// Path must start with /
|
||||
if !strings.HasPrefix(u.Path, "/") {
|
||||
return nil, fmt.Errorf("invalid wavefile URL: path must start with /")
|
||||
}
|
||||
|
||||
// Must have a host (zone)
|
||||
if u.Host == "" {
|
||||
return nil, fmt.Errorf("invalid wavefile URL: must specify zone (e.g., wavefile://block/file.txt)")
|
||||
}
|
||||
|
||||
return &waveFileRef{
|
||||
zoneId: u.Host,
|
||||
fileName: strings.TrimPrefix(u.Path, "/"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func resolveWaveFile(ref *waveFileRef) (*waveobj.ORef, error) {
|
||||
return resolveSimpleId(ref.zoneId)
|
||||
}
|
||||
|
||||
var fileListCmd = &cobra.Command{
|
||||
Use: "ls [wavefile://zone[/path]]",
|
||||
Short: "list wave files",
|
||||
Example: " wsh file ls wavefile://block/\n wsh file ls wavefile://client/configs/",
|
||||
RunE: activityWrap("file", fileListRun),
|
||||
PreRunE: preRunSetupRpcClient,
|
||||
}
|
||||
|
||||
var fileCatCmd = &cobra.Command{
|
||||
Use: "cat wavefile://zone/file",
|
||||
Short: "display contents of a wave file",
|
||||
Example: " wsh file cat wavefile://block/config.txt\n wsh file cat wavefile://client/settings.json",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: activityWrap("file", fileCatRun),
|
||||
PreRunE: preRunSetupRpcClient,
|
||||
}
|
||||
|
||||
var fileInfoCmd = &cobra.Command{
|
||||
Use: "info wavefile://zone/file",
|
||||
Short: "show wave file information",
|
||||
Example: " wsh file info wavefile://block/config.txt",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: activityWrap("file", fileInfoRun),
|
||||
PreRunE: preRunSetupRpcClient,
|
||||
}
|
||||
|
||||
var fileRmCmd = &cobra.Command{
|
||||
Use: "rm wavefile://zone/file",
|
||||
Short: "remove a wave file",
|
||||
Example: " wsh file rm wavefile://block/config.txt",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: activityWrap("file", fileRmRun),
|
||||
PreRunE: preRunSetupRpcClient,
|
||||
}
|
||||
|
||||
var fileWriteCmd = &cobra.Command{
|
||||
Use: "write wavefile://zone/file",
|
||||
Short: "write stdin into a wave file (up to 10MB)",
|
||||
Example: " echo 'hello' | wsh file write wavefile://block/greeting.txt",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: activityWrap("file", fileWriteRun),
|
||||
PreRunE: preRunSetupRpcClient,
|
||||
}
|
||||
|
||||
var fileAppendCmd = &cobra.Command{
|
||||
Use: "append wavefile://zone/file",
|
||||
Short: "append stdin to a wave file",
|
||||
Long: "append stdin to a wave file, buffering input and respecting 10MB total file size limit",
|
||||
Example: " tail -f log.txt | wsh file append wavefile://block/app.log",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: activityWrap("file", fileAppendRun),
|
||||
PreRunE: preRunSetupRpcClient,
|
||||
}
|
||||
|
||||
var fileCpCmd = &cobra.Command{
|
||||
Use: "cp source destination",
|
||||
Short: "copy between wave files and local files",
|
||||
Long: `Copy files between wave storage and local filesystem.
|
||||
Exactly one of source or destination must be a wavefile:// URL.`,
|
||||
Example: " wsh file cp wavefile://block/config.txt ./local-config.txt\n wsh file cp ./local-config.txt wavefile://block/config.txt",
|
||||
Args: cobra.ExactArgs(2),
|
||||
RunE: activityWrap("file", fileCpRun),
|
||||
PreRunE: preRunSetupRpcClient,
|
||||
}
|
||||
|
||||
func fileCatRun(cmd *cobra.Command, args []string) error {
|
||||
ref, err := parseWaveFileURL(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fullORef, err := resolveWaveFile(ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fileData := wshrpc.CommandFileData{
|
||||
ZoneId: fullORef.OID,
|
||||
FileName: ref.fileName,
|
||||
}
|
||||
|
||||
// Get file info first to check existence and get size
|
||||
info, err := wshclient.FileInfoCommand(RpcClient, fileData, &wshrpc.RpcOpts{Timeout: 2000})
|
||||
err = convertNotFoundErr(err)
|
||||
if err == fs.ErrNotExist {
|
||||
return fmt.Errorf("%s: no such file", args[0])
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting file info: %w", err)
|
||||
}
|
||||
|
||||
err = streamReadFromWaveFile(fileData, info.Size, os.Stdout)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func fileInfoRun(cmd *cobra.Command, args []string) error {
|
||||
ref, err := parseWaveFileURL(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fullORef, err := resolveWaveFile(ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fileData := wshrpc.CommandFileData{
|
||||
ZoneId: fullORef.OID,
|
||||
FileName: ref.fileName,
|
||||
}
|
||||
|
||||
info, err := wshclient.FileInfoCommand(RpcClient, fileData, &wshrpc.RpcOpts{Timeout: DefaultFileTimeout})
|
||||
err = convertNotFoundErr(err)
|
||||
if err == fs.ErrNotExist {
|
||||
return fmt.Errorf("%s: no such file", args[0])
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting file info: %w", err)
|
||||
}
|
||||
|
||||
WriteStdout("filename: %s\n", info.Name)
|
||||
WriteStdout("size: %d\n", info.Size)
|
||||
WriteStdout("ctime: %s\n", time.Unix(info.CreatedTs/1000, 0).Format(time.DateTime))
|
||||
WriteStdout("mtime: %s\n", time.Unix(info.ModTs/1000, 0).Format(time.DateTime))
|
||||
if len(info.Meta) > 0 {
|
||||
WriteStdout("metadata:\n")
|
||||
for k, v := range info.Meta {
|
||||
WriteStdout(" %s: %v\n", k, v)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func fileRmRun(cmd *cobra.Command, args []string) error {
|
||||
ref, err := parseWaveFileURL(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fullORef, err := resolveWaveFile(ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fileData := wshrpc.CommandFileData{
|
||||
ZoneId: fullORef.OID,
|
||||
FileName: ref.fileName,
|
||||
}
|
||||
|
||||
_, err = wshclient.FileInfoCommand(RpcClient, fileData, &wshrpc.RpcOpts{Timeout: DefaultFileTimeout})
|
||||
err = convertNotFoundErr(err)
|
||||
if err == fs.ErrNotExist {
|
||||
return fmt.Errorf("%s: no such file", args[0])
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting file info: %w", err)
|
||||
}
|
||||
|
||||
err = wshclient.FileDeleteCommand(RpcClient, fileData, &wshrpc.RpcOpts{Timeout: DefaultFileTimeout})
|
||||
if err != nil {
|
||||
return fmt.Errorf("removing file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func fileWriteRun(cmd *cobra.Command, args []string) error {
|
||||
ref, err := parseWaveFileURL(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fullORef, err := resolveWaveFile(ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fileData := wshrpc.CommandFileData{
|
||||
ZoneId: fullORef.OID,
|
||||
FileName: ref.fileName,
|
||||
}
|
||||
|
||||
_, err = ensureWaveFile(args[0], fileData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = streamWriteToWaveFile(fileData, WrappedStdin)
|
||||
if err != nil {
|
||||
return fmt.Errorf("writing file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func fileAppendRun(cmd *cobra.Command, args []string) error {
|
||||
ref, err := parseWaveFileURL(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fullORef, err := resolveWaveFile(ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fileData := wshrpc.CommandFileData{
|
||||
ZoneId: fullORef.OID,
|
||||
FileName: ref.fileName,
|
||||
}
|
||||
|
||||
info, err := ensureWaveFile(args[0], fileData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.Size >= MaxFileSize {
|
||||
return fmt.Errorf("file already at maximum size (%d bytes)", MaxFileSize)
|
||||
}
|
||||
|
||||
reader := bufio.NewReader(WrappedStdin)
|
||||
var buf bytes.Buffer
|
||||
remainingSpace := MaxFileSize - info.Size
|
||||
for {
|
||||
chunk := make([]byte, 8192)
|
||||
n, err := reader.Read(chunk)
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading input: %w", err)
|
||||
}
|
||||
|
||||
if int64(buf.Len()+n) > remainingSpace {
|
||||
return fmt.Errorf("append would exceed maximum file size of %d bytes", MaxFileSize)
|
||||
}
|
||||
|
||||
buf.Write(chunk[:n])
|
||||
|
||||
if buf.Len() >= 8192 { // 8KB batch size
|
||||
fileData.Data64 = base64.StdEncoding.EncodeToString(buf.Bytes())
|
||||
err = wshclient.FileAppendCommand(RpcClient, fileData, &wshrpc.RpcOpts{Timeout: fileTimeout})
|
||||
if err != nil {
|
||||
return fmt.Errorf("appending to file: %w", err)
|
||||
}
|
||||
remainingSpace -= int64(buf.Len())
|
||||
buf.Reset()
|
||||
}
|
||||
}
|
||||
|
||||
if buf.Len() > 0 {
|
||||
fileData.Data64 = base64.StdEncoding.EncodeToString(buf.Bytes())
|
||||
err = wshclient.FileAppendCommand(RpcClient, fileData, &wshrpc.RpcOpts{Timeout: fileTimeout})
|
||||
if err != nil {
|
||||
return fmt.Errorf("appending to file: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getTargetPath(src, dst string) (string, error) {
|
||||
var srcBase string
|
||||
if strings.HasPrefix(src, WaveFilePrefix) {
|
||||
srcBase = path.Base(src)
|
||||
} else {
|
||||
srcBase = filepath.Base(src)
|
||||
}
|
||||
|
||||
if strings.HasPrefix(dst, WaveFilePrefix) {
|
||||
// For wavefile URLs
|
||||
if strings.HasSuffix(dst, "/") {
|
||||
return dst + srcBase, nil
|
||||
}
|
||||
return dst, nil
|
||||
}
|
||||
|
||||
// For local paths
|
||||
dstInfo, err := os.Stat(dst)
|
||||
if err == nil && dstInfo.IsDir() {
|
||||
// If it's an existing directory, use the source filename
|
||||
return filepath.Join(dst, srcBase), nil
|
||||
}
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
// Return error if it's something other than not exists
|
||||
return "", fmt.Errorf("checking destination path: %w", err)
|
||||
}
|
||||
|
||||
return dst, nil
|
||||
}
|
||||
|
||||
func fileCpRun(cmd *cobra.Command, args []string) error {
|
||||
src, origDst := args[0], args[1]
|
||||
dst, err := getTargetPath(src, origDst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
srcIsWave := strings.HasPrefix(src, WaveFilePrefix)
|
||||
dstIsWave := strings.HasPrefix(dst, WaveFilePrefix)
|
||||
|
||||
if srcIsWave == dstIsWave {
|
||||
return fmt.Errorf("exactly one file must be a wavefile:// URL")
|
||||
}
|
||||
|
||||
if srcIsWave {
|
||||
return copyFromWaveToLocal(src, dst)
|
||||
} else {
|
||||
return copyFromLocalToWave(src, dst)
|
||||
}
|
||||
}
|
||||
|
||||
func copyFromWaveToLocal(src, dst string) error {
|
||||
ref, err := parseWaveFileURL(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fullORef, err := resolveWaveFile(ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fileData := wshrpc.CommandFileData{
|
||||
ZoneId: fullORef.OID,
|
||||
FileName: ref.fileName,
|
||||
}
|
||||
|
||||
// Get file info first to check existence and get size
|
||||
info, err := wshclient.FileInfoCommand(RpcClient, fileData, &wshrpc.RpcOpts{Timeout: 2000})
|
||||
err = convertNotFoundErr(err)
|
||||
if err == fs.ErrNotExist {
|
||||
return fmt.Errorf("%s: no such file", src)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting file info: %w", err)
|
||||
}
|
||||
|
||||
// Create the destination file
|
||||
f, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating local file: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
err = streamReadFromWaveFile(fileData, info.Size, f)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading wave file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyFromLocalToWave(src, dst string) error {
|
||||
ref, err := parseWaveFileURL(dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fullORef, err := resolveWaveFile(ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// stat local file
|
||||
stat, err := os.Stat(src)
|
||||
if err == fs.ErrNotExist {
|
||||
return fmt.Errorf("%s: no such file", src)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("stat local file: %w", err)
|
||||
}
|
||||
if stat.IsDir() {
|
||||
return fmt.Errorf("%s: is a directory", src)
|
||||
}
|
||||
|
||||
fileData := wshrpc.CommandFileData{
|
||||
ZoneId: fullORef.OID,
|
||||
FileName: ref.fileName,
|
||||
}
|
||||
|
||||
_, err = ensureWaveFile(dst, fileData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
file, err := os.Open(src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("opening local file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
err = streamWriteToWaveFile(fileData, file)
|
||||
if err != nil {
|
||||
return fmt.Errorf("writing wave file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func filePrintColumns(filesChan <-chan fileListResult) error {
|
||||
width := 80 // default if we can't get terminal
|
||||
if w, _, err := term.GetSize(int(os.Stdout.Fd())); err == nil {
|
||||
width = w
|
||||
}
|
||||
|
||||
numCols := width / 10
|
||||
if numCols < 1 {
|
||||
numCols = 1
|
||||
}
|
||||
|
||||
return colprint.PrintColumns(
|
||||
filesChan,
|
||||
numCols,
|
||||
100, // sample size
|
||||
func(f fileListResult) (string, error) {
|
||||
if f.err != nil {
|
||||
return "", f.err
|
||||
}
|
||||
return f.info.Name, nil
|
||||
},
|
||||
os.Stdout,
|
||||
)
|
||||
}
|
||||
|
||||
func filePrintLong(filesChan <-chan fileListResult) error {
|
||||
// Sample first 100 files to determine name width
|
||||
maxNameLen := 0
|
||||
var samples []*wshrpc.WaveFileInfo
|
||||
|
||||
for f := range filesChan {
|
||||
if f.err != nil {
|
||||
return f.err
|
||||
}
|
||||
samples = append(samples, f.info)
|
||||
if len(f.info.Name) > maxNameLen {
|
||||
maxNameLen = len(f.info.Name)
|
||||
}
|
||||
|
||||
if len(samples) >= 100 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Use sampled width, but cap it at 60 chars to prevent excessive width
|
||||
nameWidth := maxNameLen + 2
|
||||
if nameWidth > 60 {
|
||||
nameWidth = 60
|
||||
}
|
||||
|
||||
// Print samples
|
||||
for _, f := range samples {
|
||||
name := f.Name
|
||||
t := time.Unix(f.ModTs/1000, 0)
|
||||
timestamp := utilfn.FormatLsTime(t)
|
||||
if f.Size == 0 && strings.HasSuffix(name, "/") {
|
||||
fmt.Fprintf(os.Stdout, "%-*s %8s %s\n", nameWidth, name, "-", timestamp)
|
||||
} else {
|
||||
fmt.Fprintf(os.Stdout, "%-*s %8d %s\n", nameWidth, name, f.Size, timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
// Continue with remaining files
|
||||
for f := range filesChan {
|
||||
if f.err != nil {
|
||||
return f.err
|
||||
}
|
||||
name := f.info.Name
|
||||
timestamp := time.Unix(f.info.ModTs/1000, 0).Format("Jan 02 15:04")
|
||||
if f.info.Size == 0 && strings.HasSuffix(name, "/") {
|
||||
fmt.Fprintf(os.Stdout, "%-*s %8s %s\n", nameWidth, name, "-", timestamp)
|
||||
} else {
|
||||
fmt.Fprintf(os.Stdout, "%-*s %8d %s\n", nameWidth, name, f.info.Size, timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func fileListRun(cmd *cobra.Command, args []string) error {
|
||||
recursive, _ := cmd.Flags().GetBool("recursive")
|
||||
longForm, _ := cmd.Flags().GetBool("long")
|
||||
onePerLine, _ := cmd.Flags().GetBool("one")
|
||||
filesOnly, _ := cmd.Flags().GetBool("files")
|
||||
|
||||
// Check if we're in a pipe
|
||||
stat, _ := os.Stdout.Stat()
|
||||
isPipe := (stat.Mode() & os.ModeCharDevice) == 0
|
||||
if isPipe {
|
||||
onePerLine = true
|
||||
}
|
||||
|
||||
// Default to listing everything if no path specified
|
||||
if len(args) == 0 {
|
||||
args = append(args, "wavefile://client/")
|
||||
}
|
||||
|
||||
ref, err := parseWaveFileURL(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fullORef, err := resolveWaveFile(ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
filesChan, err := streamFileList(fullORef.OID, ref.fileName, recursive, filesOnly)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if longForm {
|
||||
return filePrintLong(filesChan)
|
||||
}
|
||||
|
||||
if onePerLine {
|
||||
for f := range filesChan {
|
||||
if f.err != nil {
|
||||
return f.err
|
||||
}
|
||||
fmt.Fprintln(os.Stdout, f.info.Name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return filePrintColumns(filesChan)
|
||||
}
|
@ -5,6 +5,9 @@ package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/wavetermdev/waveterm/pkg/wshrpc"
|
||||
@ -12,57 +15,106 @@ import (
|
||||
)
|
||||
|
||||
var getMetaCmd = &cobra.Command{
|
||||
Use: "getmeta [key]",
|
||||
Use: "getmeta [key...]",
|
||||
Short: "get metadata for an entity",
|
||||
Args: cobra.RangeArgs(0, 1),
|
||||
Run: getMetaRun,
|
||||
Long: "Get metadata for an entity. Keys can be exact matches or patterns like 'name:*' to get all keys that start with 'name:'",
|
||||
Args: cobra.ArbitraryArgs,
|
||||
RunE: getMetaRun,
|
||||
PreRunE: preRunSetupRpcClient,
|
||||
}
|
||||
|
||||
var getMetaRawOutput bool
|
||||
var getMetaClearPrefix bool
|
||||
var getMetaVerbose bool
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(getMetaCmd)
|
||||
getMetaCmd.Flags().BoolVarP(&getMetaVerbose, "verbose", "v", false, "output full metadata")
|
||||
getMetaCmd.Flags().BoolVar(&getMetaRawOutput, "raw", false, "output singleton string values without quotes")
|
||||
getMetaCmd.Flags().BoolVar(&getMetaClearPrefix, "clear-prefix", false, "output the special clearing key for prefix queries")
|
||||
}
|
||||
|
||||
func getMetaRun(cmd *cobra.Command, args []string) {
|
||||
oref := blockArg
|
||||
if oref == "" {
|
||||
WriteStderr("[error] oref is required")
|
||||
return
|
||||
func filterMetaKeys(meta map[string]interface{}, keys []string) map[string]interface{} {
|
||||
result := make(map[string]interface{})
|
||||
|
||||
// Process each requested key
|
||||
for _, key := range keys {
|
||||
if strings.HasSuffix(key, ":*") {
|
||||
// Handle pattern matching
|
||||
prefix := strings.TrimSuffix(key, "*")
|
||||
baseKey := strings.TrimSuffix(prefix, ":")
|
||||
|
||||
if getMetaClearPrefix {
|
||||
result[key] = true
|
||||
}
|
||||
|
||||
// Include the base key without colon if it exists
|
||||
if val, exists := meta[baseKey]; exists {
|
||||
result[baseKey] = val
|
||||
}
|
||||
|
||||
// Include all keys with the prefix
|
||||
for k, v := range meta {
|
||||
if strings.HasPrefix(k, prefix) {
|
||||
result[k] = v
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Handle exact key match
|
||||
if val, exists := meta[key]; exists {
|
||||
result[key] = val
|
||||
} else {
|
||||
result[key] = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
err := validateEasyORef(oref)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func getMetaRun(cmd *cobra.Command, args []string) (rtnErr error) {
|
||||
defer func() {
|
||||
sendActivity("getmeta", rtnErr == nil)
|
||||
}()
|
||||
fullORef, err := resolveBlockArg()
|
||||
if err != nil {
|
||||
WriteStderr("[error] %v\n", err)
|
||||
return
|
||||
return err
|
||||
}
|
||||
fullORef, err := resolveSimpleId(oref)
|
||||
if err != nil {
|
||||
WriteStderr("[error] resolving oref: %v\n", err)
|
||||
return
|
||||
if getMetaVerbose {
|
||||
fmt.Fprintf(os.Stderr, "resolved-id: %s\n", fullORef.String())
|
||||
}
|
||||
resp, err := wshclient.GetMetaCommand(RpcClient, wshrpc.CommandGetMetaData{ORef: *fullORef}, &wshrpc.RpcOpts{Timeout: 2000})
|
||||
if err != nil {
|
||||
WriteStderr("[error] getting metadata: %v\n", err)
|
||||
return
|
||||
return fmt.Errorf("getting metadata: %w", err)
|
||||
}
|
||||
|
||||
var output interface{}
|
||||
if len(args) > 0 {
|
||||
val, ok := resp[args[0]]
|
||||
if !ok {
|
||||
return
|
||||
if len(args) == 1 && !strings.HasSuffix(args[0], ":*") {
|
||||
// Single key case - output just the value
|
||||
output = resp[args[0]]
|
||||
} else {
|
||||
// Multiple keys or pattern matching case - output object
|
||||
output = filterMetaKeys(resp, args)
|
||||
}
|
||||
outBArr, err := json.MarshalIndent(val, "", " ")
|
||||
if err != nil {
|
||||
WriteStderr("[error] formatting metadata: %v\n", err)
|
||||
return
|
||||
}
|
||||
outStr := string(outBArr)
|
||||
WriteStdout("%s\n", outStr)
|
||||
} else {
|
||||
outBArr, err := json.MarshalIndent(resp, "", " ")
|
||||
if err != nil {
|
||||
WriteStderr("[error] formatting metadata: %v\n", err)
|
||||
// No args case - output full metadata
|
||||
output = resp
|
||||
}
|
||||
|
||||
// Handle raw string output
|
||||
if getMetaRawOutput {
|
||||
if str, ok := output.(string); ok {
|
||||
WriteStdout("%s\n", str)
|
||||
return
|
||||
}
|
||||
outStr := string(outBArr)
|
||||
WriteStdout("%s\n", outStr)
|
||||
}
|
||||
|
||||
outBArr, err := json.MarshalIndent(output, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("formatting metadata: %w", err)
|
||||
}
|
||||
outStr := string(outBArr)
|
||||
WriteStdout("%s\n", outStr)
|
||||
return nil
|
||||
}
|
||||
|
151
cmd/wsh/cmd/wshcmd-getvar.go
Normal file
@ -0,0 +1,151 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"sort"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/wavetermdev/waveterm/pkg/util/envutil"
|
||||
"github.com/wavetermdev/waveterm/pkg/wshrpc"
|
||||
"github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient"
|
||||
)
|
||||
|
||||
var getVarCmd = &cobra.Command{
|
||||
Use: "getvar [flags] [key]",
|
||||
Short: "get variable(s) from a block",
|
||||
Long: `Get variable(s) from a block. Without --all, requires a key argument.
|
||||
With --all, prints all variables. Use -0 for null-terminated output.`,
|
||||
Example: " wsh getvar FOO\n wsh getvar --all\n wsh getvar --all -0",
|
||||
RunE: getVarRun,
|
||||
PreRunE: preRunSetupRpcClient,
|
||||
}
|
||||
|
||||
var (
|
||||
getVarFileName string
|
||||
getVarAllVars bool
|
||||
getVarNullTerminate bool
|
||||
getVarLocal bool
|
||||
getVarFlagNL bool
|
||||
getVarFlagNoNL bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(getVarCmd)
|
||||
getVarCmd.Flags().StringVar(&getVarFileName, "varfile", DefaultVarFileName, "var file name")
|
||||
getVarCmd.Flags().BoolVar(&getVarAllVars, "all", false, "get all variables")
|
||||
getVarCmd.Flags().BoolVarP(&getVarNullTerminate, "null", "0", false, "use null terminators in output")
|
||||
getVarCmd.Flags().BoolVarP(&getVarLocal, "local", "l", false, "get variables local to block")
|
||||
getVarCmd.Flags().BoolVarP(&getVarFlagNL, "newline", "n", false, "print newline after output")
|
||||
getVarCmd.Flags().BoolVarP(&getVarFlagNoNL, "no-newline", "N", false, "do not print newline after output")
|
||||
}
|
||||
|
||||
func shouldPrintNewline() bool {
|
||||
isTty := getIsTty()
|
||||
if getVarFlagNL {
|
||||
return true
|
||||
}
|
||||
if getVarFlagNoNL {
|
||||
return false
|
||||
}
|
||||
return isTty
|
||||
}
|
||||
|
||||
func getVarRun(cmd *cobra.Command, args []string) error {
|
||||
defer func() {
|
||||
sendActivity("getvar", WshExitCode == 0)
|
||||
}()
|
||||
|
||||
// Resolve block to get zoneId
|
||||
if blockArg == "" {
|
||||
if getVarLocal {
|
||||
blockArg = "this"
|
||||
} else {
|
||||
blockArg = "client"
|
||||
}
|
||||
}
|
||||
fullORef, err := resolveBlockArg()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if getVarAllVars {
|
||||
if len(args) > 0 {
|
||||
return fmt.Errorf("cannot specify key with --all")
|
||||
}
|
||||
return getAllVariables(fullORef.OID)
|
||||
}
|
||||
|
||||
// Single variable case - existing logic
|
||||
if len(args) != 1 {
|
||||
return fmt.Errorf("requires a key argument")
|
||||
}
|
||||
|
||||
key := args[0]
|
||||
commandData := wshrpc.CommandVarData{
|
||||
Key: key,
|
||||
ZoneId: fullORef.OID,
|
||||
FileName: getVarFileName,
|
||||
}
|
||||
|
||||
resp, err := wshclient.GetVarCommand(RpcClient, commandData, &wshrpc.RpcOpts{Timeout: 2000})
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting variable: %w", err)
|
||||
}
|
||||
|
||||
if !resp.Exists {
|
||||
WshExitCode = 1
|
||||
return nil
|
||||
}
|
||||
|
||||
WriteStdout("%s", resp.Val)
|
||||
if shouldPrintNewline() {
|
||||
WriteStdout("\n")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getAllVariables(zoneId string) error {
|
||||
fileData := wshrpc.CommandFileData{
|
||||
ZoneId: zoneId,
|
||||
FileName: getVarFileName,
|
||||
}
|
||||
|
||||
envStr64, err := wshclient.FileReadCommand(RpcClient, fileData, &wshrpc.RpcOpts{Timeout: 2000})
|
||||
err = convertNotFoundErr(err)
|
||||
if err == fs.ErrNotExist {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading variables: %w", err)
|
||||
}
|
||||
envBytes, err := base64.StdEncoding.DecodeString(envStr64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("decoding variables: %w", err)
|
||||
}
|
||||
|
||||
envMap := envutil.EnvToMap(string(envBytes))
|
||||
|
||||
terminator := "\n"
|
||||
if getVarNullTerminate {
|
||||
terminator = "\x00"
|
||||
}
|
||||
|
||||
// Sort keys for consistent output
|
||||
keys := make([]string, 0, len(envMap))
|
||||
for k := range envMap {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
for _, k := range keys {
|
||||
WriteStdout("%s=%s%s", k, envMap[k], terminator)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/wavetermdev/waveterm/pkg/wshutil"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(htmlCmd)
|
||||
}
|
||||
|
||||
var htmlCmd = &cobra.Command{
|
||||
Use: "html",
|
||||
Hidden: true,
|
||||
Short: "Launch a demo html-mode terminal",
|
||||
Run: htmlRun,
|
||||
PreRunE: preRunSetupRpcClient,
|
||||
}
|
||||
|
||||
func htmlRun(cmd *cobra.Command, args []string) {
|
||||
defer wshutil.DoShutdown("normal exit", 0, true)
|
||||
setTermHtmlMode()
|
||||
for {
|
||||
var buf [1]byte
|
||||
_, err := WrappedStdin.Read(buf[:])
|
||||
if err != nil {
|
||||
wshutil.DoShutdown(fmt.Sprintf("stdin closed/error (%v)", err), 1, true)
|
||||
}
|
||||
if buf[0] == 0x03 {
|
||||
wshutil.DoShutdown("read Ctrl-C from stdin", 1, true)
|
||||
break
|
||||
}
|
||||
if buf[0] == 'x' {
|
||||
wshutil.DoShutdown("read 'x' from stdin", 0, true)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
@ -4,6 +4,8 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/wavetermdev/waveterm/pkg/wshrpc"
|
||||
"github.com/wavetermdev/waveterm/pkg/wshutil"
|
||||
@ -16,7 +18,7 @@ var setNotifyCmd = &cobra.Command{
|
||||
Use: "notify <message> [-t <title>] [-s]",
|
||||
Short: "create a notification",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: notifyRun,
|
||||
RunE: notifyRun,
|
||||
PreRunE: preRunSetupRpcClient,
|
||||
}
|
||||
|
||||
@ -26,7 +28,10 @@ func init() {
|
||||
rootCmd.AddCommand(setNotifyCmd)
|
||||
}
|
||||
|
||||
func notifyRun(cmd *cobra.Command, args []string) {
|
||||
func notifyRun(cmd *cobra.Command, args []string) (rtnErr error) {
|
||||
defer func() {
|
||||
sendActivity("notify", rtnErr == nil)
|
||||
}()
|
||||
message := args[0]
|
||||
notificationOptions := &wshrpc.WaveNotificationOptions{
|
||||
Title: notifyTitle,
|
||||
@ -35,7 +40,7 @@ func notifyRun(cmd *cobra.Command, args []string) {
|
||||
}
|
||||
_, err := RpcClient.SendRpcRequest(wshrpc.Command_Notify, notificationOptions, &wshrpc.RpcOpts{Timeout: 2000, Route: wshutil.ElectronRoute})
|
||||
if err != nil {
|
||||
WriteStderr("[error] sending notification: %v\n", err)
|
||||
return
|
||||
return fmt.Errorf("sending notification: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ import (
|
||||
)
|
||||
|
||||
var readFileCmd = &cobra.Command{
|
||||
Use: "readfile",
|
||||
Use: "readfile [filename]",
|
||||
Short: "read a blockfile",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: runReadFile,
|
||||
@ -24,22 +24,12 @@ func init() {
|
||||
}
|
||||
|
||||
func runReadFile(cmd *cobra.Command, args []string) {
|
||||
oref := args[0]
|
||||
if oref == "" {
|
||||
WriteStderr("[error] oref is required\n")
|
||||
return
|
||||
}
|
||||
err := validateEasyORef(oref)
|
||||
fullORef, err := resolveBlockArg()
|
||||
if err != nil {
|
||||
WriteStderr("[error] %v\n", err)
|
||||
return
|
||||
}
|
||||
fullORef, err := resolveSimpleId(oref)
|
||||
if err != nil {
|
||||
WriteStderr("error resolving oref: %v\n", err)
|
||||
return
|
||||
}
|
||||
resp64, err := wshclient.FileReadCommand(RpcClient, wshrpc.CommandFileData{ZoneId: fullORef.OID, FileName: args[1]}, &wshrpc.RpcOpts{Timeout: 5000})
|
||||
resp64, err := wshclient.FileReadCommand(RpcClient, wshrpc.CommandFileData{ZoneId: fullORef.OID, FileName: args[0]}, &wshrpc.RpcOpts{Timeout: 5000})
|
||||
if err != nil {
|
||||
WriteStderr("[error] reading file: %v\n", err)
|
||||
return
|
||||
|
@ -7,13 +7,9 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"regexp"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/wavetermdev/waveterm/pkg/waveobj"
|
||||
"github.com/wavetermdev/waveterm/pkg/wshrpc"
|
||||
@ -30,22 +26,12 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
var usingHtmlMode bool
|
||||
var WrappedStdin io.Reader = os.Stdin
|
||||
var RpcClient *wshutil.WshRpc
|
||||
var RpcContext wshrpc.RpcContext
|
||||
var UsingTermWshMode bool
|
||||
var blockArg string
|
||||
|
||||
func extraShutdownFn() {
|
||||
if usingHtmlMode {
|
||||
cmd := &wshrpc.CommandSetMetaData{
|
||||
Meta: map[string]any{"term:mode": nil},
|
||||
}
|
||||
RpcClient.SendCommand(wshrpc.Command_SetMeta, cmd, nil)
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
var WshExitCode int
|
||||
|
||||
func WriteStderr(fmtStr string, args ...interface{}) {
|
||||
output := fmt.Sprintf(fmtStr, args...)
|
||||
@ -71,6 +57,36 @@ func preRunSetupRpcClient(cmd *cobra.Command, args []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func getIsTty() bool {
|
||||
if fileInfo, _ := os.Stdout.Stat(); (fileInfo.Mode() & os.ModeCharDevice) != 0 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type RunEFnType = func(*cobra.Command, []string) error
|
||||
|
||||
func activityWrap(activityStr string, origRunE RunEFnType) RunEFnType {
|
||||
return func(cmd *cobra.Command, args []string) (rtnErr error) {
|
||||
defer func() {
|
||||
sendActivity(activityStr, rtnErr == nil)
|
||||
}()
|
||||
return origRunE(cmd, args)
|
||||
}
|
||||
}
|
||||
|
||||
func resolveBlockArg() (*waveobj.ORef, error) {
|
||||
oref := blockArg
|
||||
if oref == "" {
|
||||
oref = "this"
|
||||
}
|
||||
fullORef, err := resolveSimpleId(oref)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("resolving blockid: %w", err)
|
||||
}
|
||||
return fullORef, nil
|
||||
}
|
||||
|
||||
// returns the wrapped stdin and a new rpc client (that wraps the stdin input and stdout output)
|
||||
func setupRpcClient(serverImpl wshutil.ServerImpl) error {
|
||||
jwtToken := os.Getenv(wshutil.WaveJwtTokenVarName)
|
||||
@ -98,47 +114,6 @@ func setupRpcClient(serverImpl wshutil.ServerImpl) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func setTermHtmlMode() {
|
||||
wshutil.SetExtraShutdownFunc(extraShutdownFn)
|
||||
cmd := &wshrpc.CommandSetMetaData{
|
||||
Meta: map[string]any{"term:mode": "html"},
|
||||
}
|
||||
err := RpcClient.SendCommand(wshrpc.Command_SetMeta, cmd, nil)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error setting html mode: %v\r\n", err)
|
||||
}
|
||||
usingHtmlMode = true
|
||||
}
|
||||
|
||||
var oidRe = regexp.MustCompile(`^[0-9a-f]{8}$`)
|
||||
|
||||
func validateEasyORef(oref string) error {
|
||||
if oref == "this" || oref == "tab" {
|
||||
return nil
|
||||
}
|
||||
if num, err := strconv.Atoi(oref); err == nil && num >= 1 {
|
||||
return nil
|
||||
}
|
||||
if strings.Contains(oref, ":") {
|
||||
_, err := waveobj.ParseORef(oref)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid ORef: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if len(oref) == 8 {
|
||||
if !oidRe.MatchString(oref) {
|
||||
return fmt.Errorf("invalid short OID format, must only use 0-9a-f: %q", oref)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
_, err := uuid.Parse(oref)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid object reference (must be UUID, or a positive integer): %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isFullORef(orefStr string) bool {
|
||||
_, err := waveobj.ParseORef(orefStr)
|
||||
return err == nil
|
||||
@ -163,6 +138,23 @@ func resolveSimpleId(id string) (*waveobj.ORef, error) {
|
||||
return &oref, nil
|
||||
}
|
||||
|
||||
// this will send wsh activity to the client running on *your* local machine (it does not contact any wave cloud infrastructure)
|
||||
// if you've turned off telemetry in your local client, this data never gets sent to us
|
||||
// no parameters or timestamps are sent, as you can see below, it just sends the name of the command (and if there was an error)
|
||||
// (e.g. "wsh ai ..." would send "ai")
|
||||
// this helps us understand which commands are actually being used so we know where to concentrate our effort
|
||||
func sendActivity(wshCmdName string, success bool) {
|
||||
if RpcClient == nil || wshCmdName == "" {
|
||||
return
|
||||
}
|
||||
dataMap := make(map[string]int)
|
||||
dataMap[wshCmdName] = 1
|
||||
if !success {
|
||||
dataMap[wshCmdName+"#"+"error"] = 1
|
||||
}
|
||||
wshclient.WshActivityCommand(RpcClient, dataMap, nil)
|
||||
}
|
||||
|
||||
// Execute executes the root command.
|
||||
func Execute() {
|
||||
defer func() {
|
||||
@ -172,10 +164,10 @@ func Execute() {
|
||||
debug.PrintStack()
|
||||
wshutil.DoShutdown("", 1, true)
|
||||
} else {
|
||||
wshutil.DoShutdown("", 0, false)
|
||||
wshutil.DoShutdown("", WshExitCode, false)
|
||||
}
|
||||
}()
|
||||
rootCmd.PersistentFlags().StringVarP(&blockArg, "block", "b", "this", "for commands which require a block id")
|
||||
rootCmd.PersistentFlags().StringVarP(&blockArg, "block", "b", "", "for commands which require a block id")
|
||||
err := rootCmd.Execute()
|
||||
if err != nil {
|
||||
wshutil.DoShutdown("", 1, true)
|
||||
|
@ -4,8 +4,9 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/wavetermdev/waveterm/pkg/wconfig"
|
||||
"github.com/wavetermdev/waveterm/pkg/wshrpc"
|
||||
"github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient"
|
||||
)
|
||||
@ -14,7 +15,7 @@ var setConfigCmd = &cobra.Command{
|
||||
Use: "setconfig",
|
||||
Short: "set config",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Run: setConfigRun,
|
||||
RunE: setConfigRun,
|
||||
PreRunE: preRunSetupRpcClient,
|
||||
}
|
||||
|
||||
@ -22,18 +23,21 @@ func init() {
|
||||
rootCmd.AddCommand(setConfigCmd)
|
||||
}
|
||||
|
||||
func setConfigRun(cmd *cobra.Command, args []string) {
|
||||
func setConfigRun(cmd *cobra.Command, args []string) (rtnErr error) {
|
||||
defer func() {
|
||||
sendActivity("setconfig", rtnErr == nil)
|
||||
}()
|
||||
|
||||
metaSetsStrs := args[:]
|
||||
meta, err := parseMetaSets(metaSetsStrs)
|
||||
if err != nil {
|
||||
WriteStderr("[error] %v\n", err)
|
||||
return
|
||||
return err
|
||||
}
|
||||
commandData := wconfig.MetaSettingsType{MetaMapType: meta}
|
||||
commandData := wshrpc.MetaSettingsType{MetaMapType: meta}
|
||||
err = wshclient.SetConfigCommand(RpcClient, commandData, &wshrpc.RpcOpts{Timeout: 2000})
|
||||
if err != nil {
|
||||
WriteStderr("[error] setting config: %v\n", err)
|
||||
return
|
||||
return fmt.Errorf("setting config: %w", err)
|
||||
}
|
||||
WriteStdout("config set\n")
|
||||
return nil
|
||||
}
|
||||
|
@ -6,6 +6,8 @@ package cmd
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@ -14,15 +16,46 @@ import (
|
||||
)
|
||||
|
||||
var setMetaCmd = &cobra.Command{
|
||||
Use: "setmeta {blockid|blocknum|this} key=value ...",
|
||||
Use: "setmeta [-b {blockid|blocknum|this}] [--json file.json] key=value ...",
|
||||
Short: "set metadata for an entity",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Run: setMetaRun,
|
||||
Args: cobra.MinimumNArgs(0),
|
||||
RunE: setMetaRun,
|
||||
PreRunE: preRunSetupRpcClient,
|
||||
}
|
||||
|
||||
var setMetaJsonFilePath string
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(setMetaCmd)
|
||||
setMetaCmd.Flags().StringVar(&setMetaJsonFilePath, "json", "", "JSON file containing metadata to apply (use '-' for stdin)")
|
||||
}
|
||||
|
||||
func loadJSONFile(filepath string) (map[string]interface{}, error) {
|
||||
var data []byte
|
||||
var err error
|
||||
|
||||
if filepath == "-" {
|
||||
data, err = io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading from stdin: %v", err)
|
||||
}
|
||||
} else {
|
||||
data, err = os.ReadFile(filepath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading JSON file: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
var result map[string]interface{}
|
||||
if err := json.Unmarshal(data, &result); err != nil {
|
||||
return nil, fmt.Errorf("parsing JSON file: %v", err)
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
return nil, fmt.Errorf("JSON file must contain an object, not null")
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func parseMetaSets(metaSets []string) (map[string]interface{}, error) {
|
||||
@ -39,7 +72,7 @@ func parseMetaSets(metaSets []string) (map[string]interface{}, error) {
|
||||
meta[fields[0]] = true
|
||||
} else if setVal == "false" {
|
||||
meta[fields[0]] = false
|
||||
} else if setVal[0] == '[' || setVal[0] == '{' {
|
||||
} else if setVal[0] == '[' || setVal[0] == '{' || setVal[0] == '"' {
|
||||
var val interface{}
|
||||
err := json.Unmarshal([]byte(setVal), &val)
|
||||
if err != nil {
|
||||
@ -63,36 +96,58 @@ func parseMetaSets(metaSets []string) (map[string]interface{}, error) {
|
||||
return meta, nil
|
||||
}
|
||||
|
||||
func setMetaRun(cmd *cobra.Command, args []string) {
|
||||
oref := blockArg
|
||||
metaSetsStrs := args[:]
|
||||
if oref == "" {
|
||||
WriteStderr("[error] oref is required\n")
|
||||
return
|
||||
func simpleMergeMeta(meta map[string]interface{}, metaUpdate map[string]interface{}) map[string]interface{} {
|
||||
for k, v := range metaUpdate {
|
||||
if v == nil {
|
||||
delete(meta, k)
|
||||
} else {
|
||||
meta[k] = v
|
||||
}
|
||||
}
|
||||
err := validateEasyORef(oref)
|
||||
return meta
|
||||
}
|
||||
|
||||
func setMetaRun(cmd *cobra.Command, args []string) (rtnErr error) {
|
||||
defer func() {
|
||||
sendActivity("setmeta", rtnErr == nil)
|
||||
}()
|
||||
var jsonMeta map[string]interface{}
|
||||
if setMetaJsonFilePath != "" {
|
||||
var err error
|
||||
jsonMeta, err = loadJSONFile(setMetaJsonFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
cmdMeta, err := parseMetaSets(args)
|
||||
if err != nil {
|
||||
WriteStderr("[error] %v\n", err)
|
||||
return
|
||||
return err
|
||||
}
|
||||
meta, err := parseMetaSets(metaSetsStrs)
|
||||
|
||||
// Merge JSON metadata with command-line metadata, with command-line taking precedence
|
||||
var fullMeta map[string]any
|
||||
if len(jsonMeta) > 0 {
|
||||
fullMeta = simpleMergeMeta(jsonMeta, cmdMeta)
|
||||
} else {
|
||||
fullMeta = cmdMeta
|
||||
}
|
||||
if len(fullMeta) == 0 {
|
||||
return fmt.Errorf("no metadata keys specified")
|
||||
}
|
||||
fullORef, err := resolveBlockArg()
|
||||
if err != nil {
|
||||
WriteStderr("[error] %v\n", err)
|
||||
return
|
||||
}
|
||||
fullORef, err := resolveSimpleId(oref)
|
||||
if err != nil {
|
||||
WriteStderr("[error] resolving oref: %v\n", err)
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
setMetaWshCmd := &wshrpc.CommandSetMetaData{
|
||||
ORef: *fullORef,
|
||||
Meta: meta,
|
||||
Meta: fullMeta,
|
||||
}
|
||||
_, err = RpcClient.SendRpcRequest(wshrpc.Command_SetMeta, setMetaWshCmd, &wshrpc.RpcOpts{Timeout: 2000})
|
||||
if err != nil {
|
||||
WriteStderr("[error] setting metadata: %v\n", err)
|
||||
return
|
||||
return fmt.Errorf("setting metadata: %v", err)
|
||||
}
|
||||
WriteStdout("metadata set\n")
|
||||
return nil
|
||||
}
|
||||
|
101
cmd/wsh/cmd/wshcmd-setvar.go
Normal file
@ -0,0 +1,101 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/wavetermdev/waveterm/pkg/wshrpc"
|
||||
"github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient"
|
||||
)
|
||||
|
||||
const DefaultVarFileName = "var"
|
||||
|
||||
var setVarCmd = &cobra.Command{
|
||||
Use: "setvar [flags] KEY=VALUE...",
|
||||
Short: "set variable(s) for a block",
|
||||
Long: `Set one or more variables for a block.
|
||||
Use --remove/-r to remove variables instead of setting them.
|
||||
When setting, each argument must be in KEY=VALUE format.
|
||||
When removing, each argument is treated as a key to remove.`,
|
||||
Example: " wsh setvar FOO=bar BAZ=123\n wsh setvar -r FOO BAZ",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: setVarRun,
|
||||
PreRunE: preRunSetupRpcClient,
|
||||
}
|
||||
|
||||
var (
|
||||
setVarFileName string
|
||||
setVarRemoveVar bool
|
||||
setVarLocal bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(setVarCmd)
|
||||
setVarCmd.Flags().StringVar(&setVarFileName, "varfile", DefaultVarFileName, "var file name")
|
||||
setVarCmd.Flags().BoolVarP(&setVarLocal, "local", "l", false, "set variables local to block")
|
||||
setVarCmd.Flags().BoolVarP(&setVarRemoveVar, "remove", "r", false, "remove the variable(s) instead of setting")
|
||||
}
|
||||
|
||||
func parseKeyValue(arg string) (key, value string, err error) {
|
||||
if setVarRemoveVar {
|
||||
return arg, "", nil
|
||||
}
|
||||
|
||||
parts := strings.SplitN(arg, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
return "", "", fmt.Errorf("invalid KEY=VALUE format %q (= sign required)", arg)
|
||||
}
|
||||
key = parts[0]
|
||||
if key == "" {
|
||||
return "", "", fmt.Errorf("empty key not allowed")
|
||||
}
|
||||
return key, parts[1], nil
|
||||
}
|
||||
|
||||
func setVarRun(cmd *cobra.Command, args []string) (rtnErr error) {
|
||||
defer func() {
|
||||
sendActivity("setvar", rtnErr == nil)
|
||||
}()
|
||||
|
||||
// Resolve block to get zoneId
|
||||
if blockArg == "" {
|
||||
if getVarLocal {
|
||||
blockArg = "this"
|
||||
} else {
|
||||
blockArg = "client"
|
||||
}
|
||||
}
|
||||
fullORef, err := resolveBlockArg()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Process all variables
|
||||
for _, arg := range args {
|
||||
key, value, err := parseKeyValue(arg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
commandData := wshrpc.CommandVarData{
|
||||
Key: key,
|
||||
ZoneId: fullORef.OID,
|
||||
FileName: setVarFileName,
|
||||
Remove: setVarRemoveVar,
|
||||
}
|
||||
|
||||
if !setVarRemoveVar {
|
||||
commandData.Val = value
|
||||
}
|
||||
|
||||
err = wshclient.SetVarCommand(RpcClient, commandData, &wshrpc.RpcOpts{Timeout: 2000})
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting variable %s: %w", key, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
@ -4,31 +4,49 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/wavetermdev/waveterm/pkg/waveobj"
|
||||
"github.com/wavetermdev/waveterm/pkg/wshrpc"
|
||||
"github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient"
|
||||
)
|
||||
|
||||
var identityFiles []string
|
||||
|
||||
var sshCmd = &cobra.Command{
|
||||
Use: "ssh",
|
||||
Short: "connect this terminal to a remote host",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: sshRun,
|
||||
RunE: sshRun,
|
||||
PreRunE: preRunSetupRpcClient,
|
||||
}
|
||||
|
||||
func init() {
|
||||
sshCmd.Flags().StringArrayVarP(&identityFiles, "identityfile", "i", []string{}, "add an identity file for publickey authentication")
|
||||
rootCmd.AddCommand(sshCmd)
|
||||
}
|
||||
|
||||
func sshRun(cmd *cobra.Command, args []string) {
|
||||
func sshRun(cmd *cobra.Command, args []string) (rtnErr error) {
|
||||
defer func() {
|
||||
sendActivity("ssh", rtnErr == nil)
|
||||
}()
|
||||
|
||||
sshArg := args[0]
|
||||
blockId := RpcContext.BlockId
|
||||
if blockId == "" {
|
||||
WriteStderr("[error] cannot determine blockid (not in JWT)\n")
|
||||
return
|
||||
return fmt.Errorf("cannot determine blockid (not in JWT)")
|
||||
}
|
||||
// first, make a connection independent of the block
|
||||
connOpts := wshrpc.ConnRequest{
|
||||
Host: sshArg,
|
||||
Keywords: wshrpc.ConnKeywords{
|
||||
SshIdentityFile: identityFiles,
|
||||
},
|
||||
}
|
||||
wshclient.ConnConnectCommand(RpcClient, connOpts, nil)
|
||||
|
||||
// now, with that made, it will be straightforward to connect
|
||||
data := wshrpc.CommandSetMetaData{
|
||||
ORef: waveobj.MakeORef(waveobj.OType_Block, blockId),
|
||||
Meta: map[string]any{
|
||||
@ -37,8 +55,8 @@ func sshRun(cmd *cobra.Command, args []string) {
|
||||
}
|
||||
err := wshclient.SetMetaCommand(RpcClient, data, nil)
|
||||
if err != nil {
|
||||
WriteStderr("[error] setting switching connection: %v\n", err)
|
||||
return
|
||||
return fmt.Errorf("setting connection in block: %w", err)
|
||||
}
|
||||
WriteStderr("switched connection to %q\n", sshArg)
|
||||
return nil
|
||||
}
|
||||
|
@ -4,7 +4,7 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"log"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
@ -21,7 +21,7 @@ var termCmd = &cobra.Command{
|
||||
Use: "term",
|
||||
Short: "open a terminal in directory",
|
||||
Args: cobra.RangeArgs(0, 1),
|
||||
Run: termRun,
|
||||
RunE: termRun,
|
||||
PreRunE: preRunSetupRpcClient,
|
||||
}
|
||||
|
||||
@ -30,29 +30,30 @@ func init() {
|
||||
rootCmd.AddCommand(termCmd)
|
||||
}
|
||||
|
||||
func termRun(cmd *cobra.Command, args []string) {
|
||||
func termRun(cmd *cobra.Command, args []string) (rtnErr error) {
|
||||
defer func() {
|
||||
sendActivity("term", rtnErr == nil)
|
||||
}()
|
||||
|
||||
var cwd string
|
||||
if len(args) > 0 {
|
||||
cwd = args[0]
|
||||
cwdExpanded, err := wavebase.ExpandHomeDir(cwd)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return
|
||||
return err
|
||||
}
|
||||
cwd = cwdExpanded
|
||||
} else {
|
||||
var err error
|
||||
cwd, err = os.Getwd()
|
||||
if err != nil {
|
||||
WriteStderr("[error] getting current directory: %v\n", err)
|
||||
return
|
||||
return fmt.Errorf("getting current directory: %w", err)
|
||||
}
|
||||
}
|
||||
var err error
|
||||
cwd, err = filepath.Abs(cwd)
|
||||
if err != nil {
|
||||
WriteStderr("[error] getting absolute path: %v\n", err)
|
||||
return
|
||||
return fmt.Errorf("getting absolute path: %w", err)
|
||||
}
|
||||
createBlockData := wshrpc.CommandCreateBlockData{
|
||||
BlockDef: &waveobj.BlockDef{
|
||||
@ -69,8 +70,8 @@ func termRun(cmd *cobra.Command, args []string) {
|
||||
}
|
||||
oref, err := wshclient.CreateBlockCommand(RpcClient, createBlockData, nil)
|
||||
if err != nil {
|
||||
WriteStderr("[error] creating new terminal block: %v\n", err)
|
||||
return
|
||||
return fmt.Errorf("creating new terminal block: %w", err)
|
||||
}
|
||||
WriteStdout("terminal block created: %s\n", oref)
|
||||
return nil
|
||||
}
|
||||
|
@ -4,18 +4,75 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/wavetermdev/waveterm/pkg/wavebase"
|
||||
"github.com/wavetermdev/waveterm/pkg/wshrpc"
|
||||
"github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient"
|
||||
"github.com/wavetermdev/waveterm/pkg/wshutil"
|
||||
)
|
||||
|
||||
var versionVerbose bool
|
||||
var versionJSON bool
|
||||
|
||||
// versionCmd represents the version command
|
||||
var versionCmd = &cobra.Command{
|
||||
Use: "version [-v] [--json]",
|
||||
Short: "Print the version number of wsh",
|
||||
RunE: runVersionCmd,
|
||||
}
|
||||
|
||||
func init() {
|
||||
versionCmd.Flags().BoolVarP(&versionVerbose, "verbose", "v", false, "Display full version information")
|
||||
versionCmd.Flags().BoolVar(&versionJSON, "json", false, "Output version information in JSON format")
|
||||
rootCmd.AddCommand(versionCmd)
|
||||
}
|
||||
|
||||
var versionCmd = &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Print the version number of wsh",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
func runVersionCmd(cmd *cobra.Command, args []string) error {
|
||||
if !versionVerbose && !versionJSON {
|
||||
WriteStdout("wsh v%s\n", wavebase.WaveVersion)
|
||||
},
|
||||
return nil
|
||||
}
|
||||
|
||||
err := preRunSetupRpcClient(cmd, args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := wshclient.WaveInfoCommand(RpcClient, &wshrpc.RpcOpts{Timeout: 2000})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
updateChannel, err := wshclient.GetUpdateChannelCommand(RpcClient, &wshrpc.RpcOpts{Timeout: 2000, Route: wshutil.ElectronRoute})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if versionJSON {
|
||||
info := map[string]interface{}{
|
||||
"version": resp.Version,
|
||||
"clientid": resp.ClientId,
|
||||
"buildtime": resp.BuildTime,
|
||||
"configdir": resp.ConfigDir,
|
||||
"datadir": resp.DataDir,
|
||||
"updatechannel": updateChannel,
|
||||
}
|
||||
outBArr, err := json.MarshalIndent(info, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("formatting version info: %v", err)
|
||||
}
|
||||
WriteStdout("%s\n", string(outBArr))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Default verbose text output
|
||||
fmt.Printf("v%s (%s)\n", resp.Version, resp.BuildTime)
|
||||
fmt.Printf("clientid: %s\n", resp.ClientId)
|
||||
fmt.Printf("configdir: %s\n", resp.ConfigDir)
|
||||
fmt.Printf("datadir: %s\n", resp.DataDir)
|
||||
fmt.Printf("update-channel: %s\n", updateChannel)
|
||||
return nil
|
||||
}
|
||||
|
@ -4,6 +4,7 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@ -20,7 +21,7 @@ var viewCmd = &cobra.Command{
|
||||
Use: "view {file|directory|URL}",
|
||||
Short: "preview/edit a file or directory",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: viewRun,
|
||||
RunE: viewRun,
|
||||
PreRunE: preRunSetupRpcClient,
|
||||
}
|
||||
|
||||
@ -28,7 +29,7 @@ var editCmd = &cobra.Command{
|
||||
Use: "edit {file}",
|
||||
Short: "edit a file",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: viewRun,
|
||||
RunE: viewRun,
|
||||
PreRunE: preRunSetupRpcClient,
|
||||
}
|
||||
|
||||
@ -38,7 +39,10 @@ func init() {
|
||||
rootCmd.AddCommand(editCmd)
|
||||
}
|
||||
|
||||
func viewRun(cmd *cobra.Command, args []string) {
|
||||
func viewRun(cmd *cobra.Command, args []string) (rtnErr error) {
|
||||
defer func() {
|
||||
sendActivity("view", rtnErr == nil)
|
||||
}()
|
||||
fileArg := args[0]
|
||||
conn := RpcContext.Conn
|
||||
var wshCmd *wshrpc.CommandCreateBlockData
|
||||
@ -55,22 +59,18 @@ func viewRun(cmd *cobra.Command, args []string) {
|
||||
} else {
|
||||
absFile, err := filepath.Abs(fileArg)
|
||||
if err != nil {
|
||||
WriteStderr("[error] getting absolute path: %v\n", err)
|
||||
return
|
||||
return fmt.Errorf("getting absolute path: %w", err)
|
||||
}
|
||||
absParent, err := filepath.Abs(filepath.Dir(fileArg))
|
||||
if err != nil {
|
||||
WriteStderr("[error] getting absolute path of parent dir: %v\n", err)
|
||||
return
|
||||
return fmt.Errorf("getting absolute path of parent dir: %w", err)
|
||||
}
|
||||
_, err = os.Stat(absParent)
|
||||
if err == fs.ErrNotExist {
|
||||
WriteStderr("[error] parent directory does not exist: %q\n", absParent)
|
||||
return
|
||||
return fmt.Errorf("parent directory does not exist: %q", absParent)
|
||||
}
|
||||
if err != nil {
|
||||
WriteStderr("[error] getting file info: %v\n", err)
|
||||
return
|
||||
return fmt.Errorf("getting file info: %w", err)
|
||||
}
|
||||
wshCmd = &wshrpc.CommandCreateBlockData{
|
||||
BlockDef: &waveobj.BlockDef{
|
||||
@ -90,7 +90,7 @@ func viewRun(cmd *cobra.Command, args []string) {
|
||||
}
|
||||
_, err := RpcClient.SendRpcRequest(wshrpc.Command_CreateBlock, wshCmd, &wshrpc.RpcOpts{Timeout: 2000})
|
||||
if err != nil {
|
||||
WriteStderr("[error] running view command: %v\r\n", err)
|
||||
return
|
||||
return fmt.Errorf("running view command: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -28,9 +28,9 @@ var webOpenCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
var webGetCmd = &cobra.Command{
|
||||
Use: "get [--inner] [--all] [--json] blockid css-selector",
|
||||
Use: "get [--inner] [--all] [--json] css-selector",
|
||||
Short: "get the html for a css selector",
|
||||
Args: cobra.ExactArgs(2),
|
||||
Args: cobra.ExactArgs(1),
|
||||
Hidden: true,
|
||||
RunE: webGetRun,
|
||||
}
|
||||
@ -51,15 +51,7 @@ func init() {
|
||||
}
|
||||
|
||||
func webGetRun(cmd *cobra.Command, args []string) error {
|
||||
oref := args[0]
|
||||
if oref == "" {
|
||||
return fmt.Errorf("blockid not specified")
|
||||
}
|
||||
err := validateEasyORef(oref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fullORef, err := resolveSimpleId(oref)
|
||||
fullORef, err := resolveBlockArg()
|
||||
if err != nil {
|
||||
return fmt.Errorf("resolving blockid: %w", err)
|
||||
}
|
||||
@ -67,14 +59,14 @@ func webGetRun(cmd *cobra.Command, args []string) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting block info: %w", err)
|
||||
}
|
||||
if blockInfo.Meta.GetString(waveobj.MetaKey_View, "") != "web" {
|
||||
if blockInfo.Block.Meta.GetString(waveobj.MetaKey_View, "") != "web" {
|
||||
return fmt.Errorf("block %s is not a web block", fullORef.OID)
|
||||
}
|
||||
data := wshrpc.CommandWebSelectorData{
|
||||
WindowId: blockInfo.WindowId,
|
||||
BlockId: fullORef.OID,
|
||||
TabId: blockInfo.TabId,
|
||||
Selector: args[1],
|
||||
Selector: args[0],
|
||||
Opts: &wshrpc.WebSelectorOpts{
|
||||
Inner: webGetInner,
|
||||
All: webGetAll,
|
||||
@ -101,7 +93,11 @@ func webGetRun(cmd *cobra.Command, args []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func webOpenRun(cmd *cobra.Command, args []string) error {
|
||||
func webOpenRun(cmd *cobra.Command, args []string) (rtnErr error) {
|
||||
defer func() {
|
||||
sendActivity("web", rtnErr == nil)
|
||||
}()
|
||||
|
||||
wshCmd := wshrpc.CommandCreateBlockData{
|
||||
BlockDef: &waveobj.BlockDef{
|
||||
Meta: map[string]any{
|
||||
|
63
cmd/wsh/cmd/wshcmd-wsl.go
Normal file
@ -0,0 +1,63 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/wavetermdev/waveterm/pkg/waveobj"
|
||||
"github.com/wavetermdev/waveterm/pkg/wshrpc"
|
||||
"github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient"
|
||||
)
|
||||
|
||||
var distroName string
|
||||
|
||||
var wslCmd = &cobra.Command{
|
||||
Use: "wsl [-d <Distro>]",
|
||||
Short: "connect this terminal to a local wsl connection",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: wslRun,
|
||||
PreRunE: preRunSetupRpcClient,
|
||||
}
|
||||
|
||||
func init() {
|
||||
wslCmd.Flags().StringVarP(&distroName, "distribution", "d", "", "Run the specified distribution")
|
||||
rootCmd.AddCommand(wslCmd)
|
||||
}
|
||||
|
||||
func wslRun(cmd *cobra.Command, args []string) (rtnErr error) {
|
||||
defer func() {
|
||||
sendActivity("wsl", rtnErr == nil)
|
||||
}()
|
||||
|
||||
var err error
|
||||
if distroName == "" {
|
||||
// get default distro from the host
|
||||
distroName, err = wshclient.WslDefaultDistroCommand(RpcClient, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if !strings.HasPrefix(distroName, "wsl://") {
|
||||
distroName = "wsl://" + distroName
|
||||
}
|
||||
blockId := RpcContext.BlockId
|
||||
if blockId == "" {
|
||||
return fmt.Errorf("cannot determine blockid (not in JWT)")
|
||||
}
|
||||
data := wshrpc.CommandSetMetaData{
|
||||
ORef: waveobj.MakeORef(waveobj.OType_Block, blockId),
|
||||
Meta: map[string]any{
|
||||
waveobj.MetaKey_Connection: distroName,
|
||||
},
|
||||
}
|
||||
err = wshclient.SetMetaCommand(RpcClient, data, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting connection in block: %w", err)
|
||||
}
|
||||
WriteStderr("switched connection to %q\n", distroName)
|
||||
return nil
|
||||
}
|
1
db/migrations-wstore/000005_blockparent.down.sql
Normal file
@ -0,0 +1 @@
|
||||
-- we don't need to remove parentoref
|
4
db/migrations-wstore/000005_blockparent.up.sql
Normal file
@ -0,0 +1,4 @@
|
||||
UPDATE db_block
|
||||
SET data = json_set(db_block.data, '$.parentoref', 'tab:' || db_tab.oid)
|
||||
FROM db_tab
|
||||
WHERE db_block.oid IN (SELECT value FROM json_each(db_tab.data, '$.blockids'));
|
13
docs/.editorconfig
Normal file
@ -0,0 +1,13 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
|
||||
[*.{js,jsx,ts,tsx,cjs,json,yml,yaml,css,less}]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[CNAME]
|
||||
insert_final_newline = false
|
22
docs/.gitignore
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
# Dependencies
|
||||
/node_modules
|
||||
/.yarn
|
||||
|
||||
# Production
|
||||
/build
|
||||
build.zip
|
||||
|
||||
# Generated files
|
||||
.docusaurus
|
||||
.cache-loader
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
6
docs/.prettierignore
Normal file
@ -0,0 +1,6 @@
|
||||
build
|
||||
.git
|
||||
node_modules
|
||||
*.min.*
|
||||
*.mdx
|
||||
CNAME
|
8
docs/.remarkrc
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"plugins": [
|
||||
"remark-preset-lint-consistent",
|
||||
"remark-preset-lint-recommended",
|
||||
"remark-mdx",
|
||||
"remark-frontmatter"
|
||||
]
|
||||
}
|
1
docs/.yarnrc.yml
Normal file
@ -0,0 +1 @@
|
||||
nodeLinker: node-modules
|
40
docs/README.md
Normal file
@ -0,0 +1,40 @@
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="../assets/wave-dark.png">
|
||||
<source media="(prefers-color-scheme: light)" srcset="../assets/wave-light.png">
|
||||
<img alt="Wave Terminal Logo" src="../assets/wave-light.png" width="240">
|
||||
</picture>
|
||||
<br/>
|
||||
</p>
|
||||
|
||||
# Wave Terminal Documentation
|
||||
|
||||
This is the home for Wave Terminal's documentation site. This README is specifically about _building_ and contributing to the docs site. If you are looking for the actual hosted docs, go here -- https://docs.waveterm.dev
|
||||
|
||||
### Installation
|
||||
|
||||
Our docs are built using [Docusaurus](https://docusaurus.io/), a modern static website generator.
|
||||
|
||||
```sh
|
||||
yarn
|
||||
```
|
||||
|
||||
### Local Development
|
||||
|
||||
```sh
|
||||
yarn start
|
||||
```
|
||||
|
||||
This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
|
||||
|
||||
### Build
|
||||
|
||||
```sh
|
||||
yarn build
|
||||
```
|
||||
|
||||
This command generates static content into the `build` directory and can be served using any static contents hosting service.
|
||||
|
||||
### Deployment
|
||||
|
||||
Deployments are handled automatically by the [Docsite and Storybook CI/CD workflow](../.github/workflows/deploy-docsite.yml)
|
3
docs/babel.config.js
Normal file
@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
presets: [require.resolve("@docusaurus/core/lib/babel/preset")],
|
||||
};
|
186
docs/docs/config.mdx
Normal file
@ -0,0 +1,186 @@
|
||||
---
|
||||
sidebar_position: 3
|
||||
id: "config"
|
||||
title: "Configuration"
|
||||
---
|
||||
|
||||
Wave's configuration files are located at `~/.config/waveterm/`.
|
||||
|
||||
The main configuration file is `settings.json` (`~/.config/waveterm/settings.json`).
|
||||
|
||||
The file is structured as a mostly flat JSON file. Instead of using sub-objects we prefer to
|
||||
use ":" as level separators.
|
||||
|
||||
:::info
|
||||
|
||||
The easiest way to edit your config files is to use the wsh editconfig command which will open your Wave config file in our built-in preview editor.
|
||||
|
||||
```
|
||||
wsh editconfig
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
## Configuration Keys
|
||||
|
||||
| Key Name | Type | Function |
|
||||
| ------------------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| ai:preset | string | the default AI preset to use |
|
||||
| ai:baseurl | string | Set the AI Base Url (must be OpenAI compatible) |
|
||||
| ai:apitoken | string | your AI api token |
|
||||
| ai:apitype | string | defaults to "open_ai", but can also set to "azure" (forspecial Azure AI handling) or "anthropic" |
|
||||
| ai:name | string | string to display in the Wave AI block header |
|
||||
| ai:model | string | model name to pass to API |
|
||||
| ai:apiversion | string | for Azure AI only (when apitype is "azure", this will default to "2023-05-15") |
|
||||
| ai:orgid | string | |
|
||||
| ai:maxtokens | int | max tokens to pass to API |
|
||||
| ai:timeoutms | int | timeout (in milliseconds) for AI calls |
|
||||
| conn:askbeforewshinstall | bool | set to false to disable popup asking if you want to install wsh extensions on new machines |
|
||||
| term:fontsize | float | the fontsize for the terminal block |
|
||||
| term:fontfamily | string | font family to use for terminal block |
|
||||
| term:disablewebgl | bool | set to false to disable WebGL acceleration in terminal |
|
||||
| term:localshellpath | string | set to override the default shell path for local terminals |
|
||||
| term:localshellopts | string[] | set to pass additional parameters to the term:localshellpath |
|
||||
| term:copyonselect | bool | set to false to disable terminal copy-on-select |
|
||||
| editor:minimapenabled | bool | set to false to disable editor minimap |
|
||||
| editor:stickscrollenabled | bool | |
|
||||
| web:openlinksinternally | bool | set to false to open web links in external browser |
|
||||
| web:defaulturl | string | default web page to open in the web widget when no url is provided (homepage) |
|
||||
| web:defaultsearch | string | search template for web searches. e.g. `https://www.google.com/search?q={query}`. "\{query}" gets replaced by search term |
|
||||
| blockheader:showblockids | bool | show first 8 chars of blockid in the header |
|
||||
| autoupdate:enabled | bool | enable/disable checking for updates (requires app restart) |
|
||||
| autoupdate:intervalms | float64 | time in milliseconds to wait between update checks (requires app restart) |
|
||||
| autoupdate:installonquit | bool | whether to automatically install updates on quit (requires app restart) |
|
||||
| autoupdate:channel | string | the auto update channel "latest" (stable builds), or "beta" (updated more frequently) (requires app restart) |
|
||||
| widget:showhelp | bool | whether to show help/tips widgets in right sidebar |
|
||||
| window:transparent | bool | set to true to enable window transparency (cannot be combined with `window:blur`) (macOS and Windows only, requires app restart, see [note on Windows compatibility](https://www.electronjs.org/docs/latest/tutorial/window-customization#limitations)) |
|
||||
| window:blur | bool | set to enable window background blurring (cannot be combined with `window:transparent`) (macOS and Windows only, requires app restart, see [note on Windows compatibility](https://www.electronjs.org/docs/latest/tutorial/window-customization#limitations)) |
|
||||
| window:opacity | float64 | 0-1, window opacity when `window:transparent` or `window:blur` are set |
|
||||
| window:bgcolor | string | set the window background color (should be hex: #xxxxxx) |
|
||||
| window:reducedmotion | bool | set to true to disable most animations |
|
||||
| window:tilegapsize | int | set to change override default gap size (in CSS pixels) between blocks |
|
||||
| window:magnifiedblockopacity | float64 | change the opacity of a magnified block (must be between 0 and 1, defaults to 0.6) |
|
||||
| window:magnifiedblocksize | float64 | change the size of a magnified block as a percentage of the dimensions of its parent layout (must be between 0 and 1, defaults to 0.9) |
|
||||
| window:magnifiedblockblurprimarypx | int | change the blur in CSS pixels that is applied directly behind a magnified block (see [backdrop-filter](https://developer.mozilla.org/en-US/docs/Web/CSS/backdrop-filter) for more info on how this gets applied) |
|
||||
| window:magnifiedblockblursecondarypx | int | change the blur in CSS pixels that is applied to the visible portions of non-magnified blocks when a block is magnified (see [backdrop-filter](https://developer.mozilla.org/en-US/docs/Web/CSS/backdrop-filter) for more info on how this gets applied) |
|
||||
| window:showmenubar | bool | set to use the OS-native menu bar (Windows and Linux only, requires app restart) |
|
||||
| window:nativetitlebar | bool | set to use the OS-native title bar, rather than the overlay (Windows and Linux only, requires app restart) |
|
||||
| window:disablehardwareacceleration | bool | set to disable Chromium hardware acceleration to resolve graphical bugs (requires app restart) |
|
||||
| telemetry:enabled | bool | set to enable/disable telemetry |
|
||||
|
||||
For reference this is the current default configuration (v0.9.3):
|
||||
|
||||
```json
|
||||
{
|
||||
"ai:preset": "ai@global",
|
||||
"ai:model": "gpt-4o-mini",
|
||||
"ai:maxtokens": 2048,
|
||||
"ai:timeoutms": 60000,
|
||||
"autoupdate:enabled": true,
|
||||
"autoupdate:installonquit": true,
|
||||
"autoupdate:intervalms": 3600000,
|
||||
"conn:askbeforewshinstall": true,
|
||||
"editor:minimapenabled": true,
|
||||
"web:defaulturl": "https://github.com/wavetermdev/waveterm",
|
||||
"web:defaultsearch": "https://www.google.com/search?q={query}",
|
||||
"window:tilegapsize": 3,
|
||||
"window:maxtabcachesize": 10,
|
||||
"window:nativetitlebar": true,
|
||||
"window:magnifiedblockopacity": 0.6,
|
||||
"window:magnifiedblocksize": 0.9,
|
||||
"window:magnifiedblockblurprimarypx": 10,
|
||||
"window:magnifiedblockblursecondarypx": 2,
|
||||
"telemetry:enabled": true,
|
||||
"term:copyonselect": true
|
||||
}
|
||||
```
|
||||
|
||||
:::warning
|
||||
|
||||
If you installed Wave pre-v0.9.0 your configuration file will be located at
|
||||
`~/.waveterm/config/settings.json`. This includes all of the other configuration
|
||||
files as well: `termthemes.json`, `presets.json`, and `widgets.json`.
|
||||
|
||||
:::
|
||||
|
||||
### Terminal Theming
|
||||
|
||||
User-defined terminal themes are located in `~/.config/waveterm/termthemes.json`.
|
||||
|
||||
This JSON file is structured as an object, with each sub-key defining a theme.
|
||||
Themes are applied by right-clicking on the terminal's header bar and selecting an entry from the "Themes" sub-menu. Alternatively they can be applied to
|
||||
the block's metadata key `term:theme`. This uses the JSON key value as the identifier. Note, for best consistency all colors should be of the format "#rrggbb" or "#rrggbbaa" (aa = alpha channel for transparency).
|
||||
|
||||
```
|
||||
wsh setmeta this term:theme="default-dark"
|
||||
```
|
||||
|
||||
Here is an example of defining a full terminal theme. All of the built-in themes are defined here: https://github.com/wavetermdev/waveterm/blob/main/pkg/wconfig/defaultconfig/termthemes.json (if you'd like to add a popular terminal theme, please submit a PR!)
|
||||
|
||||
```json
|
||||
{
|
||||
"default-dark": {
|
||||
"display:name": "Default Dark",
|
||||
"display:order": 1,
|
||||
"black": "#757575",
|
||||
"red": "#cc685c",
|
||||
"green": "#76c266",
|
||||
"yellow": "#cbca9b",
|
||||
"blue": "#85aacb",
|
||||
"magenta": "#cc72ca",
|
||||
"cyan": "#74a7cb",
|
||||
"white": "#c1c1c1",
|
||||
"brightBlack": "#727272",
|
||||
"brightRed": "#cc9d97",
|
||||
"brightGreen": "#a3dd97",
|
||||
"brightYellow": "#cbcaaa",
|
||||
"brightBlue": "#9ab6cb",
|
||||
"brightMagenta": "#cc8ecb",
|
||||
"brightCyan": "#b7b8cb",
|
||||
"brightWhite": "#f0f0f0",
|
||||
"gray": "#8b918a",
|
||||
"cmdtext": "#f0f0f0",
|
||||
"foreground": "#c1c1c1",
|
||||
"selectionBackground": "",
|
||||
"background": "#00000077",
|
||||
"cursorAccent": ""
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
:::info
|
||||
|
||||
You can easily open the termthemes.json config file by running:
|
||||
|
||||
```
|
||||
wsh editconfig termthemes.json
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
| Key Name | Type | ANSI FG# | ANSI BG# | Function |
|
||||
| ------------------- | --------- | -------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| display:name | string | | | the name as it will appear in the UI context menu |
|
||||
| display:order | float | | | entries in the context menu are sorted by display:order |
|
||||
| black | CSS color | 30 | 40 | color for black |
|
||||
| red | CSS color | 31 | 41 | color for red |
|
||||
| green | CSS color | 32 | 42 | color for green |
|
||||
| yellow | CSS color | 33 | 43 | color for yellow |
|
||||
| blue | CSS color | 34 | 44 | color for blue |
|
||||
| magenta | CSS color | 35 | 45 | color for magenta |
|
||||
| cyan | CSS color | 36 | 46 | color for cyan |
|
||||
| white | CSS color | 37 | 47 | color for white |
|
||||
| brightBlack | CSS color | 90 | 100 | color for bright black |
|
||||
| brightRed | CSS color | 91 | 101 | color for bright red |
|
||||
| brightGreen | CSS color | 92 | 102 | color for bright green |
|
||||
| brightYellow | CSS color | 93 | 103 | color for bright yellow |
|
||||
| brightBlue | CSS color | 94 | 104 | color for bright blue |
|
||||
| brightMagenta | CSS color | 95 | 105 | color for bright magenta |
|
||||
| brightCyan | CSS color | 96 | 106 | color for bright cyan |
|
||||
| brightWhite | CSS color | 97 | 107 | color for bright white |
|
||||
| gray | CSS color | | | currently unused |
|
||||
| cmdtext | CSS color | | | currently unused |
|
||||
| foreground | CSS color | | | foreground color (default when no color code is applied) |
|
||||
| background | CSS color | | | background color (default when no color code is applied), must have alpha channel (#rrggbbaa) if you want the terminal to be transparent |
|
||||
| cursorAccent | CSS color | | | color for cursor |
|
||||
| selectionBackground | CSS color | | | background color for selected text |
|
60
docs/docs/connections.mdx
Normal file
@ -0,0 +1,60 @@
|
||||
---
|
||||
sidebar_position: 7
|
||||
id: "connections"
|
||||
title: "Connections"
|
||||
---
|
||||
|
||||
# Connections
|
||||
|
||||
Wave allows users to connect to various machines and unify them together in a way that preserves the unique behavior of each. At the moment, this extends to SSH remote connections and local WSL connections.
|
||||
|
||||
## Access a Connection in a Block
|
||||
|
||||
The easiest way to access connections is to click the <i className="fa-sharp fa-laptop"/> icon. From there, you can either type `[user]@[host]` for a desired SSH remote or type `wsl://<distribution name>` for a desired WSL distribution. Alternatively, if the connection already exists in the dropdown list, you can either click it or navigate to it with arrow keys and press enter to connect.
|
||||
|
||||
![a dropdown showing a list of connections that already exist](/img/connection-dropdown.png)
|
||||
|
||||
## What are wsh Shell Extensions?
|
||||
|
||||
`wsh` is a small program that helps manage waveterm regardless of which machine you are currently connected to. In order to not interrupt the normal flow of the remote session, we install it on your remote machine at `~/.waveterm/bin/wsh`. Then, when wave connects to your connection (and only when wave connects to your connection), `~/.waveterm/bin` is added to your `PATH` for that individual session. For more info on what `wsh` is capable of, see [wsh command](/wsh). And if you wish to view the source code of `wsh`, you can find it [here](https://github.com/wavetermdev/waveterm/tree/main/cmd/wsh).
|
||||
|
||||
## Add a New Connection to the Dropdown
|
||||
|
||||
The SSH values that are loaded into the dropdown by default are obtained by parsing your `~/.ssh/config` and `/etc/ssh/ssh_config` files. Adding a new connection is as simple as adding a new `Host` to one of these files, typically the `~/.ssh/config` file.
|
||||
|
||||
WSL values are added by searching the installed WSL distributions as they appear in the Windows Registry.
|
||||
|
||||
## SSH Config Parsing
|
||||
|
||||
At the moment, we are capable of parsing any SSH config file that does not contain the `Match` keyword. This keyword is incompatible with a library we are using, but we are hoping to fix that soon. While all other valid keywords are parsed, we only support the functionality of a small subset of them at the moment:
|
||||
| Keyword | Description |
|
||||
|---------|-------------|
|
||||
| Host | The pattern to match when attempting to connect via `[user]@[host]`. We list hosts that do not contain any wildcards characters (`*`, `?`, or `!`). Even if a host pattern contains wildcards, it will still be parsed when determining the values associated with the keys as usual.|
|
||||
| User | The user of the SSH remote connection. This will default to the current user on the local machine if not specified.|
|
||||
| Port | The port to connect to the remote on. `22` is the default if not specified.|
|
||||
| IdentityFile | This can be specified more than once per host. It gives the path to a private identity file (id_rsa, id_ed25519, id_ecdsa, etc.) that is used to authenticate the connection. Each will be tried in order, and they can be encrypted with a passphrase if desired. If no value is set, the default is to try in order: ~/.ssh/id_rsa, ~/.ssh/id_ecdsa, ~/.ssh/id_ecdsa_sk, ~/.ssh/id_ed25519_sk, ~/.ssh/id_dsa.|
|
||||
|BatchMode| If set to true, user interaction via password, challenge/response, and publickey passphrase authentication will be disabled. It is set to false by default.|
|
||||
|PubkeyAuthentication| (partial) This is used to specify if pubkey authentication should be attempted. It is partially implementented as the `unbound` and `host-bound` values simply work the same as the `yes` value. The default is `yes`.|
|
||||
|PasswordAuthentication| This is used to specify if password authentication should be attempted. The default is `yes`.|
|
||||
|KbdInteractiveAuthentication| This is used to specify if keyboard-interactive authentication should be attempted. The default is `yes`.|
|
||||
|PreferredAuthentications| (partial) Specifies the order the client should attempt to authenticate in. It is partially implemented as it does not support `gssapi-with-mic` or `hostbased` authentication. The default is `publickey,keyboard-interactive,password`|
|
||||
|AddKeysToAgent| (partial) This option will automatically add keys and their corresponding passphrase to your running ssh agent if it is enabled. It is partially supported as it can only accept `yes` and `no` as valid inputs. Other inputs such as `confirm` or a time interval will behave the same as `no`. The default value is `no`.|
|
||||
|ProxyJump| Specifies one or more jump proxies in a comma separated list. Each will be visited sequentially using TCP forwarding before connecting to the desired connection (also using TCP forwarding). It can be set to `none` to disable the feature.|
|
||||
|
||||
### Example SSH Config Host
|
||||
|
||||
For a quick example, a host in your config file may look like:
|
||||
|
||||
```
|
||||
Host myhost
|
||||
User username
|
||||
HostName 203.0.113.254
|
||||
IdentityFile ~/.ssh/id_rsa
|
||||
AddKeysToAgent yes
|
||||
```
|
||||
|
||||
You would then be able to access this connection with `myhost` or `username@myhost`. And if you wanted to manually specify a port such as port 2222, you could do that by either adding `Port 2222` to the config file or connecting to `username@myhost:2222`.
|
||||
|
||||
## Managing Connections with the CLI
|
||||
|
||||
The `wsh` command gives some commands specifically for interacting with the connections. You can view these [here](/wsh#conn).
|
70
docs/docs/customization.mdx
Normal file
@ -0,0 +1,70 @@
|
||||
---
|
||||
sidebar_position: 1.5
|
||||
id: "customization"
|
||||
title: "Customization"
|
||||
---
|
||||
|
||||
## Tab Themes
|
||||
|
||||
<img
|
||||
title="Tab Context Menu"
|
||||
style={{ float: "right", margin: "0 0 10px 10px" }}
|
||||
src="./img/tab-context-menu.png"
|
||||
width="300"
|
||||
/>
|
||||
|
||||
Right click on any tab to bring up a menu which allows you to rename the tab and select different backgrounds.
|
||||
|
||||
It is also possible to create your own themes using custom colors, gradients, images and more by editing your presets.json config file. To see how Wave's built in tab themes are defined, you can check out our [default presets file](https://github.com/wavetermdev/waveterm/blob/main/pkg/wconfig/defaultconfig/presets.json).
|
||||
|
||||
<div style={{ clear: "both" }} />
|
||||
|
||||
## Terminal Customization
|
||||
|
||||
<img
|
||||
title="Terminal Context Menu"
|
||||
style={{ float: "right", margin: "0 0 10px 10px" }}
|
||||
src="./img/terminal-context-menu.png"
|
||||
width="300"
|
||||
/>
|
||||
|
||||
#### Terminal Theme
|
||||
|
||||
Right click in the header area of any terminal block to bring up a menu which allows you to set a terminal
|
||||
theme for that terminal.
|
||||
|
||||
You can set the default theme for all terminals (which haven't had their theme manually overridden) by editing your settings.json file and adding the key `term:theme` and setting it to the appropriate key. The keys can be found
|
||||
in the [default termthemes.json file](https://github.com/wavetermdev/waveterm/blob/main/pkg/wconfig/defaultconfig/termthemes.json).
|
||||
|
||||
If you add your own termthemes.json file in the config directory, you can also add your own custom terminal themes (just follow the same format).
|
||||
|
||||
<div style={{ clear: "both" }} />
|
||||
|
||||
#### Font Size
|
||||
|
||||
From the same context menu you can also change the font-size of the terminal. To change the default font size across all of your (non-overridden) terminals, you can set the config key `term:fontsize` to the size you want. e.g. `{ "term:fontsize": 14}`.
|
||||
|
||||
#### Font Family
|
||||
|
||||
There is no UI to edit your default terminal font family. But, it _can_ be overridden. In your settings.json file you can add the key `term:fontfamily` and set it to a font that is _installed_ on your local system. If type a font that is not installed, or use a non-monospace font, your terminal will look terrible (don't do that 🙂), delete the key to return to using the default.
|
||||
|
||||
## Widgets Sidebar
|
||||
|
||||
<img
|
||||
title="Terminal Context Menu"
|
||||
style={{ float: "right", margin: "0 0 10px 10px" }}
|
||||
src="./img/custom-widgets.png"
|
||||
width="120"
|
||||
/>
|
||||
|
||||
See [Custom Widgets](/customwidgets) for detailed documentation around changing what appears in your right widget sidebar.
|
||||
|
||||
Using widgets.json, you'll be able to remove any default widgets and add widgets of your own. You can fully customize the icons, colors, text, and defaults (like directories, webpages, AI model, remote connection, commands, etc.) of your custom widgets.
|
||||
|
||||
You can also suppress the help widgets in the bottom right by setting the config key `widget:showhelp` to `false`.
|
||||
|
||||
<div style={{ clear: "both" }} />
|
||||
|
||||
## Presets
|
||||
|
||||
For more advanced customization, to set up multiple AI models, and your own tab backgrounds, check out our [Presets Documentation](./presets).
|
307
docs/docs/customwidgets.mdx
Normal file
@ -0,0 +1,307 @@
|
||||
---
|
||||
sidebar_position: 6
|
||||
id: "customwidgets"
|
||||
title: "Custom Widgets"
|
||||
---
|
||||
|
||||
# Custom Widgets
|
||||
|
||||
Wave allows users to create their own widgets to uniquely customize their experience for what works for them. While we plan on greatly expanding on this in the future, it is already possible to make some widgets that you can access at the press of a button. All widgets can be created by modifying the `<WAVETERM_HOME>/config/widgets.json` file. By adding a widget to this file, it is possible to add widgets to the widget bar. By default, the widget bar looks like this:
|
||||
![The default widget bar](/img/all-widgets-default.webp)
|
||||
|
||||
By adding additional widgets, it is possible to get a widget bar that looks like this:
|
||||
|
||||
![A widget bar with custom widgets added](/img/all-widgets-extra.webp)
|
||||
|
||||
## The Structure of a Widget
|
||||
|
||||
All widgets share a similar structure that roughly looks like the example below:
|
||||
|
||||
```json
|
||||
"<widget name>": {
|
||||
"icon": "<font awesome icon name>",
|
||||
"label": "<the text label of the widget>",
|
||||
"color": "<the color of the label>",
|
||||
"blockdef": {
|
||||
"meta": {
|
||||
"view": "term",
|
||||
"controller": "cmd",
|
||||
"cmd": "<the actual cli command>"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This consists of a couple different parts. First and foremost, each widget has a unique identifying name. The value associated with this name is the outer `WidgetConfigType`. It is outlined in red below:
|
||||
|
||||
![An example of a widget with outer keys labeled as WidgetConfigType and inner keys labeled as MetaTSType. In the example, the outer keys are icon, label, color, and blockdef. The inner keys are view, controller, and cmd.](/img/widget-example.webp)
|
||||
|
||||
This `WidgetConfigType` is shared between all types of widgets. That is to say, all widgets—regardless of type— will use the same keys for this. The accepted keys are:
|
||||
|
||||
| Key | Description |
|
||||
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| "display:order" | (optional) Overrides the order of widgets with a number in case you want the widget to be different than the order provided in the `widgets.json` file. Defaults to 0. |
|
||||
| "icon" | (optional) The name of a [font awesome icon](#font-awesome-icons). Defaults to `"browser"`. |
|
||||
| "color" | (optional) A string representing a color as would be used in CSS. Hex codes and custom CSS properties are included. This defaults to `"var(--secondary-text-color)"` which is a color wave uses for text that should be differentiated from other text. Out of the box, it is `"#c3c8c2"`. |
|
||||
| "label" | (optional) A string representing the label that appears underneath the widget. It will also act as a tooltip on hover if the `"description"` key isn't filled out. It is null by default. |
|
||||
| "description" | (optional) A description of what the widget does. If it is specified, this serves as a tooltip on hover. It is null by default. |
|
||||
| "blockdef" | This is where the the non-visual portion of the widget is defined. Note that all further definition takes place inside a meta object inside this one. |
|
||||
|
||||
<a name="font-awesome-icons" />
|
||||
:::info
|
||||
|
||||
**Font Awesome Icons**
|
||||
|
||||
[Font Awesome](https://fontawesome.com/search) provides a ton of useful icons that you can use as a widget icon in your app. At it's simplest, you can just provide the icon name and it will be used. For example, the string `"house"`, will provide an icon containing a house. We also allow you to apply a few different styles to your icon by modifying the name as follows:
|
||||
|
||||
| format | description |
|
||||
| ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| <icon name> | The plain icon with no additional styles applied. |
|
||||
| solid@<icon name> | Adds the `fa-solid` class to the icon to fill in the content with a fill color rather than leaving it a background. |
|
||||
| regular@<icon name> | Adds the `fa-regular` class to the icon to ensure the content will not have a fill color and will use a standard outline instead. |
|
||||
| brands@<icon name> | This is required to add the required `fa-brands` class to an icon associated with a brand. Without this, brand icons will not render properly. This will not work with icons that aren't brand icons. |
|
||||
|
||||
:::
|
||||
|
||||
The other options are part of the inner `MetaTSType` (outlined in blue in the image). This contains all of the details about how the widget actually works. The valid keys vary with each type of widget. They will be individually explored in more detail below.
|
||||
|
||||
# Terminal and CLI Widgets
|
||||
|
||||
A terminal widget, or CLI widget, is a widget that simply opens a terminal and runs a CLI command. They tend to look something like the example below:
|
||||
|
||||
```json
|
||||
{
|
||||
<... other widgets go here ...>,
|
||||
"<widget name>": {
|
||||
"icon": "<font awesome icon name>",
|
||||
"label": "<the text label of the widget>",
|
||||
"color": "<the color of the label>",
|
||||
"blockdef": {
|
||||
"meta": {
|
||||
"view": "term",
|
||||
"controller": "cmd"
|
||||
"cmd": "<the actual cli command>"
|
||||
}
|
||||
}
|
||||
},
|
||||
<... other widgets go here ...>
|
||||
}
|
||||
```
|
||||
|
||||
The `WidgetConfigType` takes the usual options common to all widgets. The `MetaTSType` can include the keys listed below:
|
||||
|
||||
| Key | Description |
|
||||
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| "view" | A string that specifies the general type of widget. In the case of custom terminal widgets, this must be set to `"term"`. |
|
||||
| "controller" | A string that specifies the type of command being used. For more persistent shell sessions, set it to "shell". For one off commands, set it to `"cmd"`. When `"cmd"` is set, the widget has an additional refresh button in its header that allows the command to be re-run. |
|
||||
| "cmd" | (optional) When the `"controller"` is set to `"cmd"`, this option provides the actual command to be run. Note that because it is run as a command, there is no shell session unless you are launching a command that contains a shell session itself. Defaults to an empty string. |
|
||||
| "cmd:interactive" | (optional) When the `"controller"` is set to `"term", this boolean adds the interactive flag to the launched terminal. Defaults to false. |
|
||||
| "cmd:login" | (optional) When the `"controller"` is set to `"term"`, this boolean adds the login flag to the term command. Defaults to false. |
|
||||
| "cmd:runonstart" | (optional) The command will rerun when the app is started. Without it, you must manually run the command. Defaults to true. |
|
||||
| "cmd:clearonstart" | (optional) When the cmd starts, the contents of the block are cleared out. Defaults to false. |
|
||||
| "cmd:clearonrestart" | (optional) When the app restarts, the contents of the block are cleared out. Defaults to false. |
|
||||
| "cmd:env" | (optional) A key-value object represting environment variables to be run with the command. Currently only works locally. Defaults to an empty object. |
|
||||
| "cmd:cwd" | (optional) A string representing the current working directory to be run with the command. Currently only works locally. Defaults to the home directory. |
|
||||
| "cmd:nowsh" | (optional) A boolean that will turn off wsh integration for the command. Defaults to false. |
|
||||
| "term:localshellpath" | (optional) Sets the shell used for running your widget command. Only works locally. If left blank, wave will determine your system default instead. |
|
||||
| "term:localshellopts" | (optional) Sets the shell options meant to be used with `"term:localshellpath"`. This is useful if you are using a nonstandard shell and need to provide a specific option that we do not cover. Only works locally. Defaults to an empty string. |
|
||||
|
||||
## Example Terminal Widgets
|
||||
|
||||
Here are a few simple widgets to serve as examples.
|
||||
|
||||
Suppose I want a widget that will run speedtest-go when opened. Then, I can define a widget as
|
||||
|
||||
```json
|
||||
{
|
||||
<... other widgets go here ...>,
|
||||
"speedtest" : {
|
||||
"icon": "gauge-high",
|
||||
"label": "speed",
|
||||
"blockdef": {
|
||||
"meta": {
|
||||
"view": "term",
|
||||
"controller": "cmd",
|
||||
"cmd": "speedtest-go --unix",
|
||||
"cmd:clearonstart"
|
||||
}
|
||||
}
|
||||
},
|
||||
<... other widgets go here ...>
|
||||
}
|
||||
```
|
||||
|
||||
This adds an icon to the widget bar that you can press to launch a terminal running the `speedtest-go --unix` command.
|
||||
![The example speedtest widget](/img/widget-example-speed.webp)
|
||||
|
||||
Using `"cmd"` for the `"controller"` is the simplest way to accomplish this. `"cmd:clearonstart"` isn't necessary, but it makes it so every time the command is run (which can be done by right clicking the header and selecting `Force Controller Restart`), the previous contents are cleared out.
|
||||
|
||||
Now suppose I wanted to run a TUI app, for instance, `dua`. Well, it turns out that you can more or less do the same thing:
|
||||
|
||||
```json
|
||||
<... other widgets go here ...>,
|
||||
"dua" : {
|
||||
"icon": "brands@linux",
|
||||
"label": "dua",
|
||||
"blockdef": {
|
||||
"meta": {
|
||||
"view": "term",
|
||||
"controller": "cmd",
|
||||
"cmd": "dua"
|
||||
}
|
||||
}
|
||||
},
|
||||
<... other widgets go here ...>
|
||||
```
|
||||
|
||||
This adds an icon to the widget bar that you can press to launch a terminal running the `dua` command.
|
||||
![The example speedtest widget](/img/widget-example-dua.webp)
|
||||
|
||||
Because this is a TUI app that does not return anything when closed, the `"cmd:clearonstart"` option doesn't change the behavior, so it has been excluded.
|
||||
|
||||
# Web Widgets
|
||||
|
||||
Sometimes, it is desireable to open a page directly to a website. That can easily be accomplished by creating a custom `"web"` widget. They have the following form in general:
|
||||
|
||||
```json
|
||||
{
|
||||
<... other widgets go here ...>,
|
||||
"<widget name>": {
|
||||
"icon": "<font awesome icon name>",
|
||||
"label": "<the text label of the widget>",
|
||||
"color": "<the color of the label>",
|
||||
"blockdef": {
|
||||
"meta": {
|
||||
"view": "web",
|
||||
"url": "<url of the first webpage>"
|
||||
}
|
||||
}
|
||||
},
|
||||
<... other widgets go here ...>
|
||||
}
|
||||
```
|
||||
|
||||
The `WidgetConfigType` takes the usual options common to all widgets. The `MetaTSType` can include the keys listed below:
|
||||
| Key | Description |
|
||||
|-----|-------------|
|
||||
| "view" | A string that specifies the general type of widget. In the case of custom web widgets, this must be set to `"web"`.|
|
||||
| "url" | This string is the url of the current page. As part of a widget, it will serve as the page the widget starts at. If not specified, this will default to the globally configurable `"web:defaulturl"` which is [https://github.com/wavetermdev/waveterm](https://github.com/wavetermdev/waveterm) on a fresh install. |
|
||||
| "pinnedurl" | (optional) This string is the url the homepage button will take you to. If not specified, this will default to the globally configurable `"web:defaulturl"` which is [https://github.com/wavetermdev/waveterm](https://github.com/wavetermdev/waveterm) on a fresh install. |
|
||||
|
||||
## Example Web Widgets
|
||||
|
||||
Say you want a widget that automatically starts at YouTube and will use YouTube as the home page. This can be done using:
|
||||
|
||||
```json
|
||||
<... other widgets go here ...>,
|
||||
"youtube" : {
|
||||
"icon": "brands@youtube",
|
||||
"label": "youtube",
|
||||
"blockdef": {
|
||||
"meta": {
|
||||
"view": "web",
|
||||
"url": "https://youtube.com",
|
||||
"pinnedurl": "https://youtube.com"
|
||||
}
|
||||
}
|
||||
},
|
||||
<... other widgets go here ...>
|
||||
```
|
||||
|
||||
This adds an icon to the widget bar that you can press to launch a web widget on the youtube homepage.
|
||||
![The example speedtest widget](/img/widget-example-youtube.webp)
|
||||
|
||||
Alternatively, say you want a web widget that opens to github as if it were a bookmark, but will use google as its home page after that. This can easily be done with:
|
||||
|
||||
```json
|
||||
<... other widgets go here ...>,
|
||||
"github" : {
|
||||
"icon": "brands@github",
|
||||
"label": "github",
|
||||
"blockdef": {
|
||||
"meta": {
|
||||
"view": "web",
|
||||
"url": "https://github.com",
|
||||
"pinnedurl": "https://google.com"
|
||||
}
|
||||
}
|
||||
},
|
||||
<... other widgets go here ...>
|
||||
```
|
||||
|
||||
This adds an icon to the widget bar that you can press to launch a web widget on the github homepage.
|
||||
![The example speedtest widget](/img/widget-example-github.webp)
|
||||
|
||||
# Sysinfo Widgets
|
||||
|
||||
The Sysinfo Widget is intentionally kept to a very small subset of possible values that we will expand over time. But it is still possible to configure your own version of it—for instance, if you want to load a different plot by default. The general form of this widget is:
|
||||
|
||||
```json
|
||||
{
|
||||
<... other widgets go here ...>,
|
||||
"<widget name>": {
|
||||
"icon": "<font awesome icon name>",
|
||||
"label": "<the text label of the widget>",
|
||||
"color": "<the color of the label>",
|
||||
"blockdef": {
|
||||
"meta": {
|
||||
"view": "sysinfo",
|
||||
"graph:numpoints": <the max number of points in the graph>,
|
||||
"sysinfo:type": <the name of the plot collection>,
|
||||
}
|
||||
}
|
||||
},
|
||||
<... other widgets go here ...>
|
||||
}
|
||||
```
|
||||
|
||||
The `WidgetConfigType` takes the usual options common to all widgets. The `MetaTSType` can include the keys listed below:
|
||||
| Key | Description |
|
||||
|-----|-------------|
|
||||
| "view" | A string that specifies the general type of widget. In the case of custom sysinfo widgets, this must be set to `"sysinfo"`.|
|
||||
| "graph:numpoints" | The maximum amount of points that can be shown on the graph. Equivalently, the number of seconds the graph window covers. This defaults to 100.|
|
||||
| "sysinfo:type" | A string representing the collection of types to show on the graph. Valid values for this are `"CPU"`, `"Mem"`, `"CPU + Mem"`, and `All CPU`. Note that these are case sensitive. If no value is provided, the plot will default to showing `"CPU"`.|
|
||||
|
||||
## Example Sysinfo Widgets
|
||||
|
||||
Suppose you have a build process that lasts 3 minutes and you'd like to be able to see the entire build on the sysinfo graph. Also, you would really like to view both the cpu and memory since both are impacted by this process. In that case, you can set up a widget as follows:
|
||||
|
||||
```json
|
||||
<... other widgets go here ...>,
|
||||
"3min-info" : {
|
||||
"icon": "circle-3",
|
||||
"label": "3mininfo",
|
||||
"blockdef": {
|
||||
"meta": {
|
||||
"view": "sysinfo",
|
||||
"graph:numpoints": 180,
|
||||
"sysinfo:type": "CPU + Mem"
|
||||
}
|
||||
}
|
||||
},
|
||||
<... other widgets go here ...>
|
||||
```
|
||||
|
||||
This adds an icon to the widget bar that you can press to launch the CPU and Memory plots by default with 180 seconds of data.
|
||||
![The example speedtest widget](/img/widget-example-3mininfo.webp)
|
||||
|
||||
Now, suppose you are fine with the default 100 points (and 100 seconds) but would like to show all of the CPU data when launched. In that case, you can write:
|
||||
|
||||
```json
|
||||
<... other widgets go here ...>,
|
||||
"all-cpu" : {
|
||||
"icon": "chart-scatter",
|
||||
"label": "all-cpu",
|
||||
"blockdef": {
|
||||
"meta": {
|
||||
"view": "sysinfo",
|
||||
"sysinfo:type": "All CPU"
|
||||
}
|
||||
}
|
||||
},
|
||||
<... other widgets go here ...>
|
||||
```
|
||||
|
||||
This adds an icon to the widget bar that you can press to launch All CPU plots by default.
|
||||
|
||||
![The example speedtest widget](/img/widget-example-all-cpu.webp)
|
88
docs/docs/faq.mdx
Normal file
@ -0,0 +1,88 @@
|
||||
---
|
||||
sidebar_position: 101
|
||||
id: "faq"
|
||||
title: "FAQ"
|
||||
---
|
||||
|
||||
# FAQ
|
||||
|
||||
### How do I set up my own LLM?
|
||||
|
||||
Open your [config file](./config) in Wave using `wsh editconfig` (the config file is normally located
|
||||
at `~/.config/waveterm/settings.json`).
|
||||
|
||||
| Key Name | Type | Function |
|
||||
| ------------ | ------ | ----------------------------------------------- |
|
||||
| ai:baseurl | string | Set the AI Base Url (must be OpenAI compatible) |
|
||||
| ai:apitoken | string | your AI api token |
|
||||
| ai:name | string | string to display in the Wave AI block header |
|
||||
| ai:model | string | model name to pass to API |
|
||||
| ai:maxtokens | int | max tokens to pass to API |
|
||||
| ai:timeoutms | int | timeout (in milliseconds) for AI calls |
|
||||
|
||||
Here's an example of pointing it to a local Ollama instance. Note that to get the text in the header of the AI block
|
||||
to update, you'll need to set the "ai:name" key. For ollama, you'll also need to provide something for the
|
||||
apitoken (even though it is ignored).
|
||||
|
||||
Here are the ollma open AI compatibility docs: https://github.com/ollama/ollama/blob/main/docs/openai.md
|
||||
|
||||
```json
|
||||
{
|
||||
"ai:*": true,
|
||||
"ai:baseurl": "http://localhost:11434/v1",
|
||||
"ai:name": "llama3.2",
|
||||
"ai:model": "llama3.2",
|
||||
"ai:apitoken": "ollama"
|
||||
}
|
||||
```
|
||||
|
||||
Note: we set the `ai:*` key to true to clear all the existing "ai" keys so this config is a clean slate.
|
||||
|
||||
To switch between multiple models, consider [adding AI Presets](./presets) instead.
|
||||
|
||||
### How can I connect to Azure AI?
|
||||
|
||||
Open your [config file](./config) in Wave using `wsh editconfig` (the config file is normally located
|
||||
at `~/.config/waveterm/settings.json`).
|
||||
|
||||
You'll need to set your `ai:baseurl` to your Azure AI Base URL (do not include query parameters or `api-version`).
|
||||
You'll also need to set `ai:apitype` to `azure`. You can then set the `ai:model`, and `ai:apitoken` appropriately
|
||||
for your setup.
|
||||
|
||||
### How can I connect to Claude?
|
||||
|
||||
Open your [config file](./config) in Wave using `wsh editconfig`.
|
||||
|
||||
Set these keys:
|
||||
|
||||
```json
|
||||
{
|
||||
"ai:*": true,
|
||||
"ai:apitype": "anthropic",
|
||||
"ai:model": "claude-3-5-sonnet-latest",
|
||||
"ai:apitoken": "<your anthropic API key>"
|
||||
}
|
||||
```
|
||||
|
||||
Note: we set the `ai:*` key to true to clear all the existing "ai" keys so this config is a clean slate.
|
||||
|
||||
To switch between models, consider [adding AI Presets](./presets) instead.
|
||||
|
||||
### How can I see the block numbers?
|
||||
|
||||
The block numbers will appear when you hold down Ctrl-Shift (and disappear once you release the key combo).
|
||||
|
||||
### How do I make a remote connection?
|
||||
|
||||
There is a button in the header. Click the <i className="fa-sharp fa-laptop"/> or <i className="fa-sharp fa-arrow-right-arrow-left"/>
|
||||
and type the `[user]@[host]` that you wish to connect to.
|
||||
|
||||
### On Windows, how can I use Git Bash as my default shell?
|
||||
|
||||
In order to make Git Bash your default shell you'll need to set the configuration variable `term:localshellpath` to
|
||||
the location of the Git Bash "bash.exe" binary. By default it is located at "C:\Program Files\Git\bin\bash.exe".
|
||||
Just remember in JSON, backslashes need to be escaped. So add this to your [settings.json](./config) file:
|
||||
|
||||
```json
|
||||
"term:localshellpath": "C:\\Program Files\\Git\\bin\\bash.exe"
|
||||
```
|
8
docs/docs/features/_category_.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"label": "Features",
|
||||
"position": 2,
|
||||
"link": {
|
||||
"type": "generated-index",
|
||||
"description": "Overview of Wave Terminal Features"
|
||||
}
|
||||
}
|
3
docs/docs/features/ai.mdx
Normal file
@ -0,0 +1,3 @@
|
||||
# Wave AI
|
||||
|
||||
TODO: Add content
|
3
docs/docs/features/browser.mdx
Normal file
@ -0,0 +1,3 @@
|
||||
# Inline Web Browser
|
||||
|
||||
TODO: Add content
|
3
docs/docs/features/charts.mdx
Normal file
@ -0,0 +1,3 @@
|
||||
# Data Visualization
|
||||
|
||||
TODO: Add content
|
3
docs/docs/features/preview.mdx
Normal file
@ -0,0 +1,3 @@
|
||||
# Preview
|
||||
|
||||
TODO: Add content
|
3
docs/docs/features/remotes.mdx
Normal file
@ -0,0 +1,3 @@
|
||||
# Remote Connections
|
||||
|
||||
TODO: Add content
|
70
docs/docs/gettingstarted.mdx
Normal file
@ -0,0 +1,70 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
id: "gettingstarted"
|
||||
title: "Getting Started"
|
||||
---
|
||||
|
||||
import { PlatformProvider, PlatformSelectorButton, PlatformItem } from "@site/src/components/platformcontext.tsx";
|
||||
|
||||
<PlatformProvider>
|
||||
<PlatformSelectorButton />
|
||||
|
||||
## Installation
|
||||
|
||||
You can install Wave directly from our [Downloads page](https://www.waveterm.dev/download) or by using a package manager.
|
||||
|
||||
Unless otherwise noted, the package manager entries are supported officially by Command Line Inc.
|
||||
|
||||
### Package managers
|
||||
|
||||
<PlatformItem platforms={["mac"]}>
|
||||
|
||||
#### Homebrew
|
||||
|
||||
Wave is available on macOS as a [Homebrew Cask](https://formulae.brew.sh/cask/wave):
|
||||
|
||||
```bash
|
||||
brew install --cask wave
|
||||
```
|
||||
|
||||
</PlatformItem>
|
||||
<PlatformItem platforms={["windows"]}>
|
||||
|
||||
Wave is available on Windows via [Chocolatey](https://community.chocolatey.org/packages/wave) and the [Windows Package Manager](https://winstall.app/apps/CommandLine.Wave).
|
||||
|
||||
#### Chocolatey
|
||||
|
||||
```Powershell
|
||||
choco install wave
|
||||
```
|
||||
|
||||
#### Windows Package Manager
|
||||
|
||||
```Powershell
|
||||
winget install CommandLine.Wave
|
||||
```
|
||||
|
||||
</PlatformItem>
|
||||
<PlatformItem platforms={["linux"]}>
|
||||
|
||||
Wave is available in the following package managers for Linux
|
||||
|
||||
#### Snap
|
||||
|
||||
Different Linux distributions have different ways of enabling Snap. You can find distro-specific instructions in our [Snapcraft listing](https://snapcraft.io/waveterm).
|
||||
|
||||
```bash
|
||||
sudo snap install --classic waveterm
|
||||
```
|
||||
|
||||
#### AUR/Pacman (community)
|
||||
|
||||
This is a [community-maintained AUR package](https://aur.archlinux.org/packages/waveterm) for installing Wave on Arch distributions.
|
||||
|
||||
#### Nix (community)
|
||||
|
||||
This is a [community-maintained Nix package](https://search.nixos.org/packages?channel=unstable&show=waveterm&size=50&sort=relevance&type=packages&query=waveterm) for installing on NixOS or any other Linux distribution set up with Nix.
|
||||
|
||||
</PlatformItem>
|
||||
|
||||
</PlatformProvider>
|
BIN
docs/docs/img/backgrounds-menu.png
Normal file
After (image error) Size: 45 KiB |
BIN
docs/docs/img/block-drag-example.jpg
Normal file
After (image error) Size: 206 KiB |
BIN
docs/docs/img/drag-edge.png
Normal file
After (image error) Size: 717 KiB |
BIN
docs/docs/img/drag-swap.png
Normal file
After (image error) Size: 709 KiB |
BIN
docs/docs/img/node-resize.png
Normal file
After (image error) Size: 299 KiB |
BIN
docs/docs/img/waveai-model-dropdown.png
Normal file
After (image error) Size: 8.7 KiB |
87
docs/docs/index.mdx
Normal file
@ -0,0 +1,87 @@
|
||||
---
|
||||
sidebar_position: -1
|
||||
id: "index"
|
||||
title: "Home"
|
||||
hide_title: true
|
||||
hide_table_of_contents: true
|
||||
---
|
||||
|
||||
import { Card, CardGroup } from "@site/src/components/card.tsx";
|
||||
|
||||
# Welcome to Wave Terminal
|
||||
|
||||
Wave is an [open-source](https://github.com/wavetermdev/waveterm) terminal that adds the ability to launch graphical widgets, controlled and integrated directly with the CLI. We support MacOS, Linux, and Windows ([Getting Started](./gettingstarted)).
|
||||
|
||||
Wave isn't just another terminal emulator; it's a rethink on how terminals are built. For too long there has been a disconnect between the CLI and the web. If you want fast, keyboard-accessible, easy-to-write applications, you use the CLI, but if you want graphical interfaces, native widgets, copy/paste, scrolling, variable font sizes, then you'd have to turn to the web. Wave's goal is to bridge that gap.
|
||||
|
||||
![Wave Screenshot](/img/wave-screenshot.webp)
|
||||
|
||||
<CardGroup>
|
||||
<Card
|
||||
href="./customization"
|
||||
icon="fa-file-magnifying-glass"
|
||||
title="Customization"
|
||||
description="Set up tabs and terminals to match your workflow needs."
|
||||
/>
|
||||
<Card
|
||||
href="./keybindings"
|
||||
icon="fa-file-magnifying-glass"
|
||||
title="Key Bindings"
|
||||
description="Boost efficiency with keyboard shortcuts for faster navigation."
|
||||
/>
|
||||
<Card
|
||||
href="./layout"
|
||||
icon="fa-file-magnifying-glass"
|
||||
title="Layout"
|
||||
description="Organize your workspace using our layout system."
|
||||
/>
|
||||
<Card
|
||||
href="./connections"
|
||||
icon="fa-file-magnifying-glass"
|
||||
title="Remote Connections"
|
||||
description="Quickly SSH or connect to remote machines in one step."
|
||||
/>
|
||||
<Card
|
||||
href="./widgets"
|
||||
icon="fa-file-magnifying-glass"
|
||||
title="Widgets"
|
||||
description="Explore built-in tools to extend your terminal’s functionality."
|
||||
/>
|
||||
<Card
|
||||
href="./wsh-command"
|
||||
icon="fa-file-magnifying-glass"
|
||||
title="wsh Command"
|
||||
description="Control Wave and launch widgets directly from the command line."
|
||||
/>
|
||||
</CardGroup>
|
||||
|
||||
<div style={{ marginBottom: 30 }} />
|
||||
|
||||
:::info
|
||||
|
||||
If you have a question, please feel free to ask us in [Discord](https://discord.gg/XfvZ334gwU). If you'd like to file a bug/enchancement, please use [Github Issues](https://github.com/wavetermdev/waveterm/issues). These docs are also open-source and we do accept PRs for docs [here](https://github.com/wavetermdev/waveterm/blob/main/docs). You can click the "Edit this page" link at the bottom of the page to get taken directly to the editor page for that document in GitHub.
|
||||
|
||||
:::
|
||||
|
||||
<div class="reference-links">
|
||||
|
||||
Other References:
|
||||
|
||||
- [Configuration](./config)
|
||||
- [Custom Widgets](./customwidgets)
|
||||
- [Telemetry](./telemetry)
|
||||
- [FAQ](./faq)
|
||||
- [Release Notes](./releasenotes)
|
||||
|
||||
</div>
|
||||
|
||||
## Links
|
||||
|
||||
- **Homepage** https://waveterm.dev
|
||||
- **Download** https://waveterm.dev/download
|
||||
- **Discord** https://discord.gg/XfvZ334gwU
|
||||
- **GitHub** https://github.com/wavetermdev/waveterm/
|
||||
|
||||
## Looking for WaveLegacy documentation?
|
||||
|
||||
WaveLegacy docs can be found at [legacydocs.waveterm.dev](https://legacydocs.waveterm.dev).
|
71
docs/docs/keybindings.mdx
Normal file
@ -0,0 +1,71 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
id: "keybindings"
|
||||
title: "Key Bindings"
|
||||
---
|
||||
|
||||
import { Kbd } from "@site/src/components/kbd.tsx";
|
||||
import { PlatformProvider, PlatformSelectorButton } from "@site/src/components/platformcontext.tsx";
|
||||
|
||||
<PlatformProvider>
|
||||
|
||||
Here's the set of default keybindings available in Wave. It is split into sections.
|
||||
Some keybindings are always active. Others are only active for certain types of blocks.
|
||||
|
||||
Note that these are the MacOS keybindings (they use "Cmd"). For Windows and Linux,
|
||||
replace "Cmd" with "Alt" (note that "Ctrl" is "Ctrl" on both Mac, Windows, and Linux).
|
||||
|
||||
## Global Keybindings
|
||||
|
||||
<PlatformSelectorButton />
|
||||
<div style={{ marginBottom: 20 }}></div>
|
||||
|
||||
| Key | Function |
|
||||
| ---------------------------- | --------------------------------------------------------------------------------- |
|
||||
| <Kbd k="Cmd:t"/> | Open a new tab |
|
||||
| <Kbd k="Cmd:n"/> | Open a new terminal block (defaults to the same connection and working directory) |
|
||||
| <Kbd k="Cmd:w"/> | Close the current block |
|
||||
| <Kbd k="Cmd:m"/> | Magnify / Un-Magnify the current block |
|
||||
| <Kbd k="Cmd:g"/> | Open the "connection" switcher |
|
||||
| <Kbd k="Cmd:i"/> | Refocus the current block (useful if the block has lost input focus) |
|
||||
| <Kbd k="Ctrl:Shift"/> | Show block numbers |
|
||||
| <Kbd k="Ctrl:Shift:1-9"/> | Switch to block number |
|
||||
| <Kbd k="Ctrl:Shift:Arrows"/> | Move left, right, up, down between blocks |
|
||||
| <Kbd k="Cmd:1-9"/> | Switch to tab number |
|
||||
| <Kbd k="Cmd:["/> | Switch tab left |
|
||||
| <Kbd k="Cmd:]"/> | Switch tab right |
|
||||
| <Kbd k="Cmd:Shift:r"/> | Refresh the UI |
|
||||
|
||||
## File Preview Keybindings
|
||||
|
||||
| Key | Function |
|
||||
| ----------------------------------------- | -------------------------------------------------------------------------------------------------- |
|
||||
| <Kbd k="[text]"/> | Any regular character (e.g. "a", "b") will filter the file list |
|
||||
| <Kbd k="Escape"/> | Clears the filter |
|
||||
| <Kbd k="ArrowUp"/> / <Kbd k="ArrowDown"/> | Change file selection up/down |
|
||||
| <Kbd k="Enter"/> | Open the currently selected file/directory |
|
||||
| <Kbd k="Cmd:ArrowUp"/> | Move "up" a directory (parent directory) |
|
||||
| <Kbd k="Cmd:ArrowLeft"/> | Back, move to the previously selected file/directory |
|
||||
| <Kbd k="Cmd:ArrowRight"/> | Forward (opposite of back) |
|
||||
| <Kbd k="Cmd:o"/> | Open a new file (accepts relative paths to the current directory) |
|
||||
| <Kbd k="Cmd:s"/> | When file editor is open, save file |
|
||||
| <Kbd k="Cmd:e"/> | For files that can be previewed or edited (markdown, CSVs), switches between preview and edit mode |
|
||||
| <Kbd k="Cmd:r"/> | When file editor is open, revert changes |
|
||||
|
||||
## Web Keybindings
|
||||
|
||||
| Key | Function |
|
||||
| ------------------------- | ------------------------------------------------------------- |
|
||||
| <Kbd k="Cmd:l"/> | Focus the URL input bar |
|
||||
| <Kbd k="Escape"/> | When the URL input bar is focused, will focus the web content |
|
||||
| <Kbd k="Cmd:r"/> | Reload webpage |
|
||||
| <Kbd k="Cmd:ArrowLeft"/> | Back |
|
||||
| <Kbd k="Cmd:ArrowRight"/> | Forward |
|
||||
|
||||
## WaveAI Keybindings
|
||||
|
||||
| Key | Function |
|
||||
| ---------------- | ------------- |
|
||||
| <Kbd k="Cmd:l"/> | Clear AI Chat |
|
||||
|
||||
</PlatformProvider>
|
89
docs/docs/layout.mdx
Normal file
@ -0,0 +1,89 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
id: "layout"
|
||||
title: "Tab Layout System"
|
||||
---
|
||||
|
||||
import { PlatformProvider, PlatformSelectorButton } from "@site/src/components/platformcontext.tsx";
|
||||
import { Kbd } from "@site/src/components/kbd.tsx";
|
||||
|
||||
<PlatformProvider>
|
||||
<PlatformSelectorButton/>
|
||||
|
||||
![screenshot showing a block being dragged over another block, with the placeholder depicting a out-of-line before outer drop](./img/drag-edge.png)
|
||||
|
||||
## Layout system under the hood
|
||||
|
||||
:::info
|
||||
|
||||
**Definitions**
|
||||
|
||||
- Layout tree: the in-memory representation of a tab layout, comprised of nodes
|
||||
- Node: An entry in the layout tree, either a single block (a leaf) or an ordered list of nodes. Defines a tiling direction (row or column) and a unitless size
|
||||
- Block: The contents of a leaf in the layout tree, defines what contents is displayed at the given layout location
|
||||
|
||||
:::
|
||||
|
||||
Our layout system emulates the [CSS Flexbox](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_flexible_box_layout/Basic_concepts_of_flexbox) system, comprising of a tree of columns and rows. Under the hood, the layout is represented as an n-tree, where each node in the tree is either a single block, or a list of nodes. Each level in the tree alternates the direction in which it tiles (level 1 tiles as a row, level 2 as a column, level 3 as a row, etc.).
|
||||
|
||||
## Layout actions
|
||||
|
||||
### Add a new block
|
||||
|
||||
You can add new blocks by selecting a widget from the right sidebar.
|
||||
|
||||
Starting at the topmost level of the tree, since the first level tiles as a row, new blocks will be added to the right side of existing blocks. Once there are 5 blocks across, new blocks will begin being added below existing blocks, starting from the right side and working to the left. As a new block gets added below an existing one, the node containing the existing block is converted from a single-block node to a list node and the existing block definition is moved one level deeper in the tree as the first element of the node list. New blocks will always be added to the last-available node in the deepest level, where available is defined as having less than five children. We don't set a limit on the number of blocks in a tab, but you may experience degraded performance past around 25 blocks.
|
||||
|
||||
While we define a 5-child limit for each node in the tree when automatically placing new blocks, there is no actual limit to the number of children a node can hold. After the block is placed, you are free to move it wherever in the layout
|
||||
|
||||
### Delete a block
|
||||
|
||||
You can delete blocks by clicking the <i className="fa-sharp fa-xmark-large"/> button in the top-right corner of the block, by right-clicking on the block header and selecting "Close Block" from the context menu, or by running the [`wsh deleteblock` command](./wsh#deleteblock). Alternatively, the currently focused block/widget can be closed by pressing <Kbd k="Cmd:w"/>
|
||||
|
||||
When you delete a block, the layout tree will be automatically adjusted to minimize the tree depth.
|
||||
|
||||
### Move a block
|
||||
|
||||
You can move blocks by clicking on the block header and dragging the block around the tab. You will see placeholders appear to show where the block will land when you drop it.
|
||||
|
||||
There are 7 different drop targets for any given block. A block is divided into quadrants along its diagonals. If the block is tiling as a row (left-to-right), dropping a block into the left or right quadrant will place the dropped block in the same level as the targeted block. This can be considered dropping the block inline. If you drop the block out of line (in quadrants corresponding to opposite tiling direction), the block will either be placed one level above or one level below the targeted block. Dropping the block towards the outside will place it in the same level as the target block's parent, while dropping it towards the center of the block will create a new level, where both the target block and the dropped block will be moved. The middle fifth of the block is reserved for the swap action. Dropping a block here will cause the target block and the dropped block to swap positions in the layout.
|
||||
|
||||
<video width="100%" height="100%" playsinline autoplay muted controls>
|
||||
<source src="./img/drag-move-24fps-crf43.mp4" type="video/mp4" />
|
||||
</video>
|
||||
|
||||
#### Possible block movements
|
||||
|
||||
:::note
|
||||
All block movements except for Swap will cause the rest of the layout to shift to accommodate the block's new displacement.
|
||||
:::
|
||||
|
||||
![screenshot showing a block being dragged over another block, with the placeholder depicting a swap movement](./img/drag-swap.png)
|
||||
![annotated example showing the drop targets within a block](./img/block-drag-example.jpg)
|
||||
|
||||
1. Inline before: Drops the block under the same node as the target block, placing it before the target in the same tiling direction
|
||||
2. Inline after: Drops the block under the same node as the target block, placing it after the target in the same tiling direction
|
||||
3. Out-of-line before outer: Drops the block before the target block's parent node in the opposite tiling direction
|
||||
4. Out-of-line before inner: Segments the target block, creating a new node in the tree. Places the dropped block before the target block in the opposite tiling direction.
|
||||
5. Out-of-line after inner: Segments the target block, creating a new node in the tree. Places the dropped block after the target block in the opposite tiling direction.
|
||||
6. Out-of-line after outer: Drops the block after the target block's parent node in the opposite tiling direction
|
||||
7. Swap: Swaps the position of the dropped block and the targeted block in the layout, preserving the rest of the layout
|
||||
|
||||
### Resize a block
|
||||
|
||||
<video width="100%" height="100%" playsinline autoplay muted controls>
|
||||
<source src="./img/resize-24fps-crf43.mp4" type="video/mp4" />
|
||||
</video>
|
||||
|
||||
![screenshot showing the line that appears when the cursor hovers over the margin of a block, indicating which blocks
|
||||
will be resized by dragging the margin](./img/node-resize.png)
|
||||
|
||||
You do not directly resize a block. Rather, you resize the nodes containing the blocks. If you hover your mouse over the margin of a block, you will see the cursor change to <i className="fa-sharp fa-arrows-left-right"/> or <i className="fa-sharp fa-arrows-up-down"/> to indicate the direction the node can be resized. You will also see a line appear after 500ms to show you how many blocks will be resized by moving that margin. Clicking and dragging on this margin will cause the block(s) to get resized.
|
||||
|
||||
Node sizes are unitless values. The ratio of all node sizes at a given tree level determines the displacement of each node. If you move a block and its node is deleted, the other nodes at the given tree level will adjust their sizes to account for the new size ratio.
|
||||
|
||||
## Change the gap size between blocks
|
||||
|
||||
The gap between blocks defaults to 3px, but this value can be changed by modifying the `window:tilegapsize` configuration value. See [Configuration](./config) for more information on how to change configuration values.
|
||||
|
||||
</PlatformProvider>
|
121
docs/docs/presets.mdx
Normal file
@ -0,0 +1,121 @@
|
||||
---
|
||||
sidebar_position: 3
|
||||
id: "presets"
|
||||
title: "Presets"
|
||||
---
|
||||
|
||||
Presets can be used to apply multiple setting overrides at once to either a tab or a block. They are currently supported in two scenarios: tab backgrounds and AI models.
|
||||
|
||||
You can set presets either by placing them in `~/.config/waveterm/presets.json` or by placing them in a JSON file in the `~/.config/waveterm/presets/` directory. All presets will be aggregated regardless of which file they're placed in so you can use the `presets` directory to organize them as you see fit.
|
||||
|
||||
:::info
|
||||
|
||||
You can open up the main presets config file in Wave by running:
|
||||
|
||||
```
|
||||
wsh editconfig presets.json
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
### File format
|
||||
|
||||
Presets follow the following format:
|
||||
|
||||
```json
|
||||
{
|
||||
...
|
||||
"<preset-type>@<preset-key>": {
|
||||
"display:name": "<Preset name>",
|
||||
"display:order": "<number>", // optional
|
||||
"<overridden-config-key-1>": "<overridden-config-value-1>"
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
A complete example of a preset for a tab background is the following:
|
||||
|
||||
```json
|
||||
{
|
||||
"bg@rainbow": {
|
||||
"display:name": "Rainbow",
|
||||
"display:order": 2.1,
|
||||
"bg:*": true,
|
||||
"bg": "linear-gradient( 226.4deg, rgba(255,26,1,1) 28.9%, rgba(254,155,1,1) 33%, rgba(255,241,0,1) 48.6%, rgba(34,218,1,1) 65.3%, rgba(0,141,254,1) 80.6%, rgba(113,63,254,1) 100.1% )",
|
||||
"bg:opacity": 0.3
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
A complete example of a preset for an AI model is the following:
|
||||
|
||||
```json
|
||||
{
|
||||
"ai@wave": {
|
||||
"display:name": "Ollama - llama3.1",
|
||||
"display:order": 0,
|
||||
"ai:baseurl": "http://localhost:11434",
|
||||
"ai:model": "llama3.1:latest"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Preset type
|
||||
|
||||
The type of the preset determines where it can be discovered in the app. Currently, the two types that will be discovered in the app are `bg` and `ai`.
|
||||
|
||||
`bg` will be served in the "Backgrounds" submenu of the Tab context menu (which can be found by right-clicking on a tab).
|
||||
|
||||
![screenshot showing the default options in the backgrounds submenu of the tab context menu](./img/backgrounds-menu.png)
|
||||
|
||||
`ai` will be served in the models dropdown in the block header of the "Wave AI" widget.
|
||||
|
||||
![screenshot showing the default options in the models dropdown in the block header of the "Wave AI" widget](./img/waveai-model-dropdown.png)
|
||||
|
||||
### Available configuration keys
|
||||
|
||||
The following configuration keys are available for use in presets:
|
||||
|
||||
#### Common keys
|
||||
|
||||
| Key Name | Type | Function |
|
||||
| ------------- | ------ | ---------------------------------------------------------------------- |
|
||||
| display:name | string | the name to use when displaying the preset in a menu (required) |
|
||||
| display:order | float | the order in which the preset should be displayed in a menu (optional) |
|
||||
|
||||
:::info
|
||||
|
||||
Configs in a preset are applied in order to override the default config values, which will persist for the remainder of the tab or block's lifetime. Setting `bg:*` or `ai:*` to `"true"` will clear the values of any previously overridden Background or AI configurations, respectively, setting them back to their defaults. You almost always want to add these keys to your presets in order to create a clean slate and prevent previously set values from leaking in.
|
||||
|
||||
:::
|
||||
|
||||
#### AI configurations
|
||||
|
||||
| Key Name | Type | Function |
|
||||
| ------------- | ------ | -------------------------------------------------------------------------------------------------- |
|
||||
| ai:\* | bool | reset all existing ai keys |
|
||||
| ai:preset | string | the default AI preset to use |
|
||||
| ai:baseurl | string | Set the AI Base Url (must be OpenAI compatible) |
|
||||
| ai:apitoken | string | your AI api token |
|
||||
| ai:apitype | string | defaults to "open_ai", but can also set to "azure" (for special Azure AI handling), or "anthropic" |
|
||||
| ai:name | string | string to display in the Wave AI block header |
|
||||
| ai:model | string | model name to pass to API |
|
||||
| ai:apiversion | string | for Azure AI only (when apitype is "azure", this will default to "2023-05-15") |
|
||||
| ai:orgid | string | |
|
||||
| ai:maxtokens | int | max tokens to pass to API |
|
||||
| ai:timeoutms | int | timeout (in milliseconds) for AI calls |
|
||||
|
||||
#### Background configurations
|
||||
|
||||
| Key Name | Type | Function |
|
||||
| -------------------- | ------ | ----------------------------------------------------------------------------------------------- |
|
||||
| bg:\* | bool | reset all existing bg keys |
|
||||
| bg:opacity | float | the opacity of the background |
|
||||
| bg:blendmode | string | the [blend mode](https://developer.mozilla.org/en-US/docs/Web/CSS/blend-mode) of the background |
|
||||
| bg:bordercolor | string | the color of the border |
|
||||
| bg:activebordercolor | string | the color of the border when a block is active |
|
||||
|
||||
#### Unset a default value
|
||||
|
||||
To unset a default value in a preset, add an override that sets it to an empty string, like `""`.
|
210
docs/docs/releasenotes.mdx
Normal file
@ -0,0 +1,210 @@
|
||||
---
|
||||
id: "releasenotes"
|
||||
title: "Release Notes"
|
||||
sidebar_position: 200
|
||||
---
|
||||
|
||||
# Release Notes
|
||||
|
||||
### v0.9.3 — Nov 20, 2024
|
||||
|
||||
New minor release that introduces Wave's connected computing extensions. We've introduced new `wsh` commands that allow you to store variables and files, and access them across terminal sessions (on both local and remote machines).
|
||||
|
||||
- `wsh setvar/getvar` to get and set variables -- [Docs](https://docs.waveterm.dev/wsh#getvarsetvar)
|
||||
- `wsh file` operations (cat, write, append, rm, info, cp, and ls) -- [Docs](https://docs.waveterm.dev/wsh#file)
|
||||
- Improved golang panic handling to prevent backend crashes
|
||||
- Improved SSH config logging and fixes a reused connection bug
|
||||
- Updated telemetry to track additional counters
|
||||
- New configuration settings (under "window:magnifiedblock") to control magnified block margins and display
|
||||
- New block/zone aliases (client, global, block, workspace, temp)
|
||||
- `wsh ai` file attachments are now rendered with special handling in the AI block
|
||||
- New ephemeral block type for creating modal widgets which will not disturb the underlying layout
|
||||
- Editing the AI presets file from the Wave AI block now brings up an ephemeral editor
|
||||
- Clicking outside of a magnified bglock will now un-magnify it
|
||||
- New button to clear the AI chat (also bound to Cmd-L)
|
||||
- New button to reset terminal commands in custom cmd widgets
|
||||
- [bugfix] Presets directory was not loading correctly on Windows
|
||||
- [bugfix] Magnified blocks were not showing correct on startup
|
||||
- [bugfix] Window opacity and background color was not getting applied properly in all cases
|
||||
- [bugfix] Fix terminal theming when applying global defaults [#1287](https://github.com/wavetermdev/waveterm/issues/1287)
|
||||
- MacOS 10.15 (Catalina) is no longer supported
|
||||
- Other bug fixes, docs improvements, and dependency bumps
|
||||
|
||||
### v0.9.2 — Nov 11, 2024
|
||||
|
||||
New minor release with bug fixes and new features! Fixed the bug around making Wave fullscreen (also affecting certain window managers like Hyprland). We've also put a lot of work into the doc site (https://docs.waveterm.dev), including documenting how [Widgets](./widgets) and [Presets](./presets) work!
|
||||
|
||||
- Updated documentation
|
||||
- Wave AI now supports the Anthropic API! Checkout the [FAQ](./faq) for how to use the Claude models with Wave AI.
|
||||
- Removed defaultwidgets.json and unified it to widgets.json. Makes it more straightforward to override the default widgets.
|
||||
- New resolvers for `-b` param in `wsh`. "tab:N" for accessing the nth tab, "[view]" and "[view]:N" for accessing blocks of a particlar view.
|
||||
- New `wsh ai` command to send AI chats (and files) directly to a new or existing AI block
|
||||
- wsh setmeta/getmeta improvements. Allow setmeta to take a json file (and also read from stdin), also better output formats for getmeta (compatible with setmeta).
|
||||
- [bugfix] Set max completion tokens in the OpenAI API so we can now work with o1 models (also fallback to non-streaming mode)
|
||||
- [bugfix] Fixed content resizing when entering "full screen" mode. This bug also affected certain window managers (like Hyprland)
|
||||
- Lots of other small bug fixes, docs updates, and dependency bumps
|
||||
|
||||
### v0.9.1 — Nov 1, 2024
|
||||
|
||||
Minor bug fix release to follow-up on the v0.9.0 build. Lots of issues fixed (especially for Windows).
|
||||
|
||||
- CLI applications that need microphone, camera, or location access will now work on MacOS. You'll see a security popup in Wave to allow/deny [#1086](https://github.com/wavetermdev/waveterm/issues/1086)
|
||||
- Can now use `wsh version -v` to print out the new data/config directories
|
||||
- Restores the old T1, T2, T3, ... tab naming logic
|
||||
- Temporarily revert to using the "Title Bar" on windows to mitgate a bug where the window controls were overlaying on top of our tabs (working on a real fix for the next release)
|
||||
- There is a new setting in the editor to enable/disable word wrapping [#1038](https://github.com/wavetermdev/waveterm/issues/1038)
|
||||
- Ctrl-S will now save files in codeedit [#1081](https://github.com/wavetermdev/waveterm/issues/1081)
|
||||
- [#1020](https://github.com/wavetermdev/waveterm/issues/1020) there is now a preset config option to change the active border color in tab themes
|
||||
- [bugfix] Multiple fixes for [#1167](https://github.com/wavetermdev/waveterm/issues/1167) to try to address tab loss while updating
|
||||
- [bugfix] Windows app crashed on opening View menu because of a bad accelerator key
|
||||
- [bugfix] The auto-updater messages in the tab bar are now more consistent when switching tabs, and we don't show errors when the network is disconnected
|
||||
- [bugfix] Full-screen mode now actually shows tabs in full screen
|
||||
- [bugfix] [#1175](https://github.com/wavetermdev/waveterm/issues/1175) can now edit .awk files
|
||||
- [bugfix] [#1066](https://github.com/wavetermdev/waveterm/issues/1066) applying a default theme now updates the background appropriately without a refresh
|
||||
|
||||
### v0.9.0 — Oct 28, 2024
|
||||
|
||||
New major Wave Terminal release! Wave tabs are now cached. Tab switching performance is
|
||||
now much faster and webview state, editor state, and scroll positions are now persisted
|
||||
across tab changes. We also have native WSL2 support. You can create native Wave connections
|
||||
to your Windows WSL2 distributions using the connection button.
|
||||
|
||||
We've also laid the groundwork for some big features that will be released over the
|
||||
next couple of weeks, including Workspaces, AI improvments, and custom widgets.
|
||||
|
||||
Lots of other smaller changes and bug fixes. See full list of PRs at https://github.com/wavetermdev/waveterm/releases/tag/v0.9.0
|
||||
|
||||
### v0.8.13 — Oct 24, 2024
|
||||
|
||||
- Wave is now available as a Snap for Linux users! You can find it [in the Snap Store](https://snapcraft.io/waveterm).
|
||||
- Wave is now available via the Windows Package Manager! You can install it via `winget install CommandLine.Wave`
|
||||
- can now use "term:fontsize" to override an individual terminal block's font size (also in context menu)
|
||||
- we now allow mixed case hostnames for connections to be compatible with ssh config
|
||||
- The Linux app icon is now updated to match the Windows icon
|
||||
- [bugfix] fixed a bug that sometimes caused escape sequences to be printed when switching between tabs
|
||||
- [bugfix] fixed an issue where the preview block was not cleaning up temp files (Windows only)
|
||||
- [bugfix] fixed chrome sandbox permissions errors in linux
|
||||
- [bugfix] fixed shutdown logic on MacOS/Linux which sometimes allowed orphaned processes to survive
|
||||
|
||||
### v0.8.12 — Oct 18, 2024
|
||||
|
||||
- Added support for multiple AI configurations! You can now run Open AI side-by-side with Ollama models. Can create AI presets in presets.json, and can easily switch between them using a new dropdown in the AI widget
|
||||
- Fix WebSocket reconnection error. this sometimes caused the terminal to hang when waking up from sleep
|
||||
- Added memory graphs, and per-CPU graphs to the sysinfo widget (and renamed it from cpuplot)
|
||||
- Added a new huge red "Config Error" button when there are parse errors in the config JSON file
|
||||
- Preview/CodeEdit widget now shows errors (squiggly lines) when JSON or YAML files fail to parse
|
||||
- New app icon for Windows to better match Fluent UI standards
|
||||
- Added copy-on-select to the terminal (on by default, can disable using "term:copyonselect")
|
||||
- Added a button to mute audio in webviews
|
||||
- Added a right-click "Open Clipboard URL" to easily open a webview from an URL stored in your system clipboard
|
||||
- [bugfix] fixed blank "help" pages when waking from sleep or restarting the app
|
||||
|
||||
### v0.8.11 — Oct 10, 2024
|
||||
|
||||
Hotfix release to address a couple of bugs introduced in v0.8.10
|
||||
|
||||
- Fixes a regression in v0.8.10 which caused new tabs to sometimes come up blank and broken
|
||||
- Layout fixes to the AI widget spacing
|
||||
- Terminal scrollbar is now semi-transparent and overlays last column
|
||||
- Fixes initial window size (on first startup) for both smaller and larger screens
|
||||
- Added a "Don't Ask Again" checkbox for installing `wsh` on remote machines (sets a new config flag)
|
||||
- Prevent the app from downgrading when you install a beta build. Installing a beta-build will now switch you to the beta-update channel.
|
||||
|
||||
### v0.8.10 — Oct 9, 2024
|
||||
|
||||
Minor big fix release (but there are some new features).
|
||||
|
||||
- added support for Azure AI [See FAQ](https://docs.waveterm.dev/faq#how-can-i-connect-to-azure-ai)
|
||||
- AI errors now appear in the chat
|
||||
- on MacOS, hitting "Space" in directorypreview will open selected file in Quick Look
|
||||
- [bugfix] fixed transparency settings
|
||||
- [bugfix] fixed issue with non-standard port numbers in connection dropdown
|
||||
- [bugfix] fixed issue with embedded docsite (returned 404 after refresh)
|
||||
|
||||
### v0.8.9 — Oct 8, 2024
|
||||
|
||||
Lots of bug fixes and new features!
|
||||
|
||||
- New "help" view -- uses an embedded version of our doc site -- https://docs.waveterm.dev
|
||||
- [breaking] wsh getmeta, wsh setmeta, and wsh deleteblock now take a blockid using a `-b` parameter instead of as a positional parameter
|
||||
- allow metadata to override the block icon, header, and text (frame:title, frame:icon, and frame:text)
|
||||
- home button on web widget to return to the homepage, option to set a homepage default for the whole app or just for the given block
|
||||
- checkpoint the terminal less often to reduce frequency of output bug (still working on a full fix)
|
||||
- new terminal themes -- Warm Yellow, and One Dark Pro
|
||||
- we now support github flavored markdown alerts
|
||||
- `wsh notify` command to send a desktop notification
|
||||
- `wsh createblock` to create any block via the CLI
|
||||
- right click to "Save Image" in webview
|
||||
- `wsh edit` will now allow you to open new files (as long as the parent directly exists)
|
||||
- added 8 new fun tab background presets (right click on any tab and select "Backgrounds" to try them out)
|
||||
- [config] new config key "term:scrollback" to set the number of lines of scrollback for terminals. Use "-1" to set 0, max is 10000.
|
||||
- [config] new config key "term:theme" to set the default terminal theme for all new terminals
|
||||
- [config] new config key "preview:showhiddenfiles" to set the default "show hidden files" setting for preview
|
||||
- [bugfix] fixed an formatting issue with `wsh getmeta`
|
||||
- [bugfix] fix for startup issue on Linux when home directory is an NFS mount
|
||||
- [bugfix] fix cursor color in terminal themes to work
|
||||
- [bugfix] fix some double scrollbars when showing markdown content
|
||||
- [bugfix] improved shutdown sequence to better capture wavesrv logs
|
||||
- [bugfix] fix Alt+G keyboard accelerator for Linux/Windows
|
||||
- other assorted bug fixes, cleanups, and security fixes
|
||||
|
||||
### v0.8.8 — Oct 1, 2024
|
||||
|
||||
Quick patch release to fix Windows/Linux "Alt" keybindings. Also brings a huge performance improvement to AI streaming speed.
|
||||
|
||||
### v0.8.7 — Sep 30, 2024
|
||||
|
||||
Quick patch release to fix bugs:
|
||||
|
||||
- Fixes windows SSH connections (invalid path while trying to install wsh tools)
|
||||
- Fixes an issue resolving `~` in windows paths `~\` now works instead of just `~/`
|
||||
- Tries to fix background color for webpages. Pulls meta tag for color-scheme, and sets a black background if dark detected (fixes issue rendering raw githubusercontent files)
|
||||
- Fixed our useDimensions hook to fire correctly. Fixes some sizing issues including allowing error messages to show consistently when SSH connections fail.
|
||||
- Allow "data:" urls in custom tab backgrounds
|
||||
- All the alias "tab" for the current tab's UUID when using wsh
|
||||
- [BUILD] conditional write generated files only if they are updated
|
||||
|
||||
### v0.8.6 — Sep 26, 2024
|
||||
|
||||
Another quick hotfix update. Fixes an issue where, if you deleted all of the tabs in a window, the window would be restored on next startup as completely blank.
|
||||
|
||||
Also, as a bonus, we added fish shell support!
|
||||
|
||||
### v0.8.5 — Sep 25, 2024
|
||||
|
||||
Hot fix, dowgrade `jotai` library. Upgrading caused a major regression in codeedit which did not allow
|
||||
users to edit files.
|
||||
|
||||
### v0.8.4 — Sep 25, 2024
|
||||
|
||||
- Added a setting `window:disablehardwareacceleration` to disable native hardware acceleration
|
||||
- New startup model for legacy users given them the option to download the WaveLegacy
|
||||
- Use WAVETERM_HOME for the home directory consistently
|
||||
|
||||
### v0.8.3 — Sep 25, 2024
|
||||
|
||||
More hotfixes for Linux users. We now link against an older version of glibc and use
|
||||
the zig compiler on linux (the newer version caused us not to run on older distros).
|
||||
Also fixes a permissions issue when installing via .deb. There is also a new config value
|
||||
`window:nativetitlebar` which restores the native titlebar on windows/linux.
|
||||
|
||||
### v0.8.2 — Sep 24, 2024
|
||||
|
||||
Hot fix, fixes a nasty crash on startup for Linux users (dynamic linking but with netcgo DNS library)
|
||||
|
||||
### v0.8.1 — Sep 23, 2024
|
||||
|
||||
Minor cleanup release.
|
||||
|
||||
- fix number parsing for certain config file values
|
||||
- add link to docs site
|
||||
- add new back button for directory view
|
||||
- telemetry fixes
|
||||
|
||||
### v0.8.0 — Sep 20, 2024
|
||||
|
||||
**Major New Relase of Wave Terminal**
|
||||
|
||||
The new build is a fresh start, and a clean break from the current version. As such, your history, settings, and configuration will not be carried over. If you'd like to continue to run the legacy version, you will need to download it separately.
|
||||
|
||||
Release Artificats and source code diffs can be found on (Github)[https://github.com/wavetermdev/waveterm].
|
118
docs/docs/telemetry.mdx
Normal file
@ -0,0 +1,118 @@
|
||||
---
|
||||
sidebar_position: 100
|
||||
id: "telemetry"
|
||||
title: "Telemetry"
|
||||
---
|
||||
|
||||
Wave Terminal collects telemetry data to help us track feature use, direct future product efforts, and generate aggregate metrics on Wave's popularity and usage. We do not collect or store any PII (personal identifiable information) and all metric data is only associated with and aggregated using your randomly generated _ClientId_. You may opt out of collection at any time.
|
||||
|
||||
If you would like to turn telemetry on or off, the first opportunity is a button on the initial welcome page. After this, it can be turned off by adding `"telemetry:enabled": false` to the `config/settings.json` file. It can alternatively be turned on by adding `"telemetry:enabled": true` to the `config/settings.json` file.
|
||||
|
||||
:::info
|
||||
|
||||
You can also change your telemetry setting by running the wsh command:
|
||||
|
||||
```
|
||||
wsh setconfig telemetry:enabled=true
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## Sending Telemetry
|
||||
|
||||
Provided that telemetry is enabled, it is sent 10 seconds after Waveterm is first booted and then again every 4 hours thereafter. It can also be sent in response to a few special cases listed below. When telemetry is sent, it is grouped into individual days as determined by your time zone. Any data from a previous day is marked as `Uploaded` so it will not need to be sent again.
|
||||
|
||||
### Sending Once Telemetry is Enabled
|
||||
|
||||
As soon as telemetry is enabled, a telemetry update is sent regardless of how long it has been since the last send. This does not reset the usual timer for telemetry sends.
|
||||
|
||||
### Notifying that Telemetry is Disabled
|
||||
|
||||
As soon as telemetry is disabled, Waveterm sends a special update that notifies us of this change. See [When Telemetry is Turned Off](#when-telemetry-is-turned-off) for more info. The timer still runs in the background but no data is sent.
|
||||
|
||||
### When Waveterm is Closed
|
||||
|
||||
Provided that telemetry is enabled, it will be sent when Waveterm is closed.
|
||||
|
||||
---
|
||||
|
||||
## Telemetry Data
|
||||
|
||||
When telemetry is active, we collect the following data. It is stored in the `telemetry.TelemetryData` type in the source code.
|
||||
|
||||
| Name | Description |
|
||||
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| ActiveMinutes | The number of minutes that the user has actively used Waveterm on a given day. This requires the terminal window to be in focus while the user is actively interacting with it. |
|
||||
| FgMinutes | The number of minutes that Waveterm has been in the foreground on a given day. This requires the terminal window to be in focus regardless of user interaction. |
|
||||
| OpenMinutes | The number of minutes that Waveterm has been open on a given day. This only requires that the terminal is open, even if the window is out of focus. |
|
||||
| NumBlocks | The number of existing blocks open on a given day |
|
||||
| NumTabs | The number of existing tabs open on a given day. |
|
||||
| NewTab | The number of new tabs created on a given day |
|
||||
| NumWindows | The number of existing windows open on a given day. |
|
||||
| NewTab | The number of new tabs opened on a given day. |
|
||||
| NumStartup | The number of times waveterm has been started on a given day. |
|
||||
| NumShutdown | The number of times waveterm has been shut down on a given day. |
|
||||
| SetTabTheme | The number of times the tab theme is changed from the context menu |
|
||||
| NumMagnify | The number of times any block is magnified |
|
||||
| NumPanics | The number of backend (golang) panics caught in the current day |
|
||||
| NumSSHConn | The number of distinct SSH connections that have been made to distinct hosts |
|
||||
| NumWSLConns | The number of distinct WSL connections that have been made to distinct distros |
|
||||
| Renderers | The number of new block views of each type are open on a given day. |
|
||||
| WshCmds | The number of wsh commands of each type run on a given day |
|
||||
| Blocks | The number of blocks of different view types open on a given day |
|
||||
| Conn | The number of successful remote connections made (and errors) on a given day |
|
||||
|
||||
## Associated Data
|
||||
|
||||
In addition to the telemetry data collected, the following is also reported. It is stored in the `telemetry.ActivityType` type in the source code.
|
||||
|
||||
| Name | Description |
|
||||
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| Day | The date the telemetry is associated with. It does not include the time. |
|
||||
| Uploaded | A boolean that indicates if the telemetry for this day is finalized. It is false during the day the telemetry is associated with, but gets set true at the first telemetry upload after that. Once it is true, the data for that particular day will not be sent up with the telemetry any more. |
|
||||
| TzName | The code for the timezone the user's OS is reporting (e.g. PST, GMT, JST) |
|
||||
| TzOffset | The offset for the timezone the user's OS is reporting (e.g. -08:00, +00:00, +09:00) |
|
||||
| ClientVersion | Which version of Waveterm is installed. |
|
||||
| ClientArch | This includes the user's operating system (e.g. linux or darwin) and architecture (e.g. x86_64 or arm64). It does not include data for any Connections at this time. |
|
||||
| BuildTime | This serves as a more accurate version number that keeps track of when we built the version. It has no bearing on when that version was installed by you. |
|
||||
| OSRelease | This lists the version of the operating system the user has installed. |
|
||||
| Displays | Display resolutions (added in v0.9.3 to help us understand what screen resolutions to optimize for) |
|
||||
|
||||
## Telemetry Metadata
|
||||
|
||||
Lastly, some data is sent along with the telemetry that describes how to classify it. It is stored in the `wcloud.TelemetryInputType` in the source code.
|
||||
|
||||
| Name | Description |
|
||||
| ----------------- | --------------------------------------------------------------------------------------------------------------------------- |
|
||||
| UserId | Currently Unused. This is an anonymous UUID intended for use in future features. |
|
||||
| ClientId | This is an anonymous UUID created when Waveterm is first launched. It is used for telemetry and sending prompts to Open AI. |
|
||||
| AppType | This is used to differentiate the current version of waveterm from the legacy app. |
|
||||
| AutoUpdateEnabled | Whether or not auto update is turned on. |
|
||||
| AutoUpdateChannel | The type of auto update in use. This specifically refers to whether a latest or beta channel is selected. |
|
||||
| CurDay | The current day (in your time zone) when telemetry is sent. It does not include the time of day. |
|
||||
|
||||
---
|
||||
|
||||
## When Telemetry is Turned Off
|
||||
|
||||
When a user disables telemetry, Waveterm sends a notification that their anonymous _ClientId_ has had its telemetry disabled. This is done with the `wcloud.NoTelemetryInputType` type in the source code. Beyond that, no further information is sent unless telemetry is turned on again. If it is turned on again, the previous 30 days of telemetry will be sent.
|
||||
|
||||
---
|
||||
|
||||
## A Note on IP Addresses
|
||||
|
||||
Telemetry is uploaded via https, which means your IP address is known to the telemetry server. We **do not** store your IP address in our telemetry table and **do not** associate it with your _ClientId_.
|
||||
|
||||
---
|
||||
|
||||
## Previously Collected Telemetry Data
|
||||
|
||||
While we believe the data we collect with telemetry is fairly minimal, we cannot make that decision for every user. If you ever change your mind about what has been collected previously, you may request that your data be deleted by emailing us at [support@waveterm.dev](mailto:support@waveterm.dev). If you do, we will need your _ClientId_ to remove it.
|
||||
|
||||
---
|
||||
|
||||
## Privacy Policy
|
||||
|
||||
For a summary of the above, you can take a look at our [Privacy Policy](https://www.waveterm.dev/privacy).
|
137
docs/docs/widgets.mdx
Normal file
@ -0,0 +1,137 @@
|
||||
---
|
||||
sidebar_position: 5
|
||||
id: "widgets"
|
||||
title: "Widgets"
|
||||
---
|
||||
|
||||
import { Kbd } from "@site/src/components/kbd.tsx";
|
||||
import { PlatformProvider, PlatformSelectorButton } from "@site/src/components/platformcontext.tsx";
|
||||
|
||||
<PlatformProvider>
|
||||
|
||||
# Widgets
|
||||
|
||||
Every individual Component is contained in its own widget. These can be added, removed, moved and resized. Each widget has its own header which can be right clicked to reveal more operations you can do with that widget.
|
||||
|
||||
<PlatformSelectorButton />
|
||||
|
||||
### How to Add a Widget
|
||||
|
||||
Adding a widget can be done using the widget bar on the right hand side of the window. This will add a widget of the selected type to the current tab.
|
||||
|
||||
### How to Close a Widget
|
||||
|
||||
Widgets can be closed by clicking the **<code><i className="fa-solid fa-sharp fa-xmark"/></code>** button on the right side of the header. Alternatively, the currently focused widget can be closed by pressing <Kbd k="Cmd:w"/>
|
||||
|
||||
### How to Navigate Widgets
|
||||
|
||||
At most, it is possible to have one widget be focused. Depending on the type of widget, this allows you to directly interact with the content in that widget. A focused widget is always outlined with a distinct border. A widget may be focused by clicking on it. Alternatively, you can change the focused widget by pressing <Kbd k="Ctrl:Shift:Arrows"/> (Ctrl + Shift + Arrow Keys) to navigate relative to the currently selected widget.
|
||||
|
||||
### How to Magnify Widgets
|
||||
|
||||
Magnifying a widget will pop the widget out in front of everything else. You can magnify using the header icon, or with <Kbd k="Cmd:m"/>.
|
||||
|
||||
### How to Reorganize Widgets
|
||||
|
||||
By dragging and dropping their headers, widgets can be moved to different locations in the layout. This effectively allows you to reorganize your screen however you see fit. When dragging, you will see a preview of the widget that is being dragged. When the widget is over a valid drop point, the area where it would be moved to will turn green. Releasing the click will place the widget there and reflow the other widgets around it. If you see a green box cover half of two different widgets, the drop will place the widget between the two. If you see the green box cover half of one widget at the edge of the screen, the widget will be placed between that widget and the edge of the screen. If you see the green box cover one widget entirely, the two widgets will swap locations.
|
||||
|
||||
See [Tab Layout System](./layout#move-a-block) for more information.
|
||||
|
||||
### How to Resize Widgets
|
||||
|
||||
Hovering the mouse between two widgets changes your cursor to <i className="fa-sharp fa-arrows-left-right"/> or <i className="fa-sharp fa-arrows-up-down"/>; and reveals a green line dividing the widgets. By dragging and dropping this green line, you are able to resize the widgets adjacent to it.
|
||||
|
||||
See [Tab Layout System](./layout#resize-a-block) for more information.
|
||||
|
||||
## Types of Widgets
|
||||
|
||||
### Term
|
||||
|
||||
The usual terminal you know and love. We add a few plugins via the `wsh` command that you can read more about further below.
|
||||
|
||||
### Preview
|
||||
|
||||
Preview is the generic type of widget used for viewing files. This can take many different forms based on the type of file being viewed.
|
||||
You can use \`wsh view [path]\` from any Wave terminal window to open a preview widget with the contents of the specified path (e.g. `wsh view .` or `wsh view ~/myimage.jpg`).
|
||||
|
||||
#### Directory
|
||||
|
||||
When looking at a directory, preview will show a file viewer much like MacOS' _Finder_ application or Windows' _File Explorer_ application. This variant is slightly more geared toward software development with the focus on seeing what is shown by the `ls -alh` command.
|
||||
|
||||
##### View a New File
|
||||
|
||||
The simplest way to view a new file is to double click its row in the file viewer. Alternatively, while the widget is focused, you can use the <Kbd k="ArrowUp" /> and <Kbd k="ArrowDown" /> arrow keys to select a row and press enter to preview the associated file.
|
||||
|
||||
##### View the Parent Directory
|
||||
|
||||
In the directory view, this is as simple as opening the `..` file as if it were a regular file. This can be done with the method above. You can also use the keyboard shortcut <Kbd k="Cmd:ArrowUp"/>.
|
||||
|
||||
##### Navigate Back and Forward
|
||||
|
||||
When looking at a file, you can navigate back by clicking the back button in the widget header or the keyboard shortcut <Kbd k="Cmd:ArrowLeft" />. You can always navigate back and forward using <Kbd k="Cmd:ArrowLeft" /> and <Kbd k="Cmd:ArrowRight" />.
|
||||
|
||||
##### Filter the List of Files
|
||||
|
||||
While the widget is focused, you can filter by filename by typing a substring of the filename you're looking for. To clear the filter, you can click the **<code><i className="fa-solid fa-sharp fa-xmark"/></code>** on the filter dropdown or press <Kbd k="Escape" />.
|
||||
|
||||
##### Sort by a File Column
|
||||
|
||||
To sort a file by a specific column, click on the header for that column. If you click the header again, it will reverse the sort order.
|
||||
|
||||
##### Hide and Show Hidden Files
|
||||
|
||||
At the right of the widget header, there is an **<code><i className="fa fa-sharp fa-solid fa-eye"/></code>** button. Clicking this button hides and shows hidden files.
|
||||
|
||||
##### Refresh the Directory
|
||||
|
||||
At the right of the widget header, there is a refresh button **<code><i className="fa fa-sharp fa-solid fa-arrows-rotate" /></code>**. Clicking this button refreshes the directory contents.
|
||||
|
||||
##### Navigate to Common Directories
|
||||
|
||||
At the left of the widget header, there is a file icon **<code><i className="fa fa-sharp fa-solid fa-folder-open"/></code>**. Clicking and holding on this icon opens a menu where you can select a common folder to navigate to. The available options are _Home_, _Desktop_, _Downloads_, and _Root_.
|
||||
|
||||
##### Open a New Terminal in the Current Directory
|
||||
|
||||
If you right click the header of the widget (alternatively, click the gear icon **<code><i className="fa fa-sharp fa-solid fa-cog"/></code>**), one of the menu items listed is **Open Terminal in New Widget**. This will create a new terminal widget at your current directory.
|
||||
|
||||
##### Open a New Terminal in a Child Directory
|
||||
|
||||
If you want to open a terminal for a child directory instead, you can right click on that file's row to get the **Open Terminal in New Widget** option. Clicking this will open a terminal at that directory. Note that this option is only available for children that are directories.
|
||||
|
||||
##### Open a New Preview for a Child
|
||||
|
||||
To open a new Preview Widget for a Child, you can right click on that file's row and select the **Open Preview in New Widget** option.
|
||||
|
||||
##### Quick Look (MacOS only)
|
||||
|
||||
On a MacOS host, it is possible to use the Quick Look feature from the directory preview. To do this, select the file you wish to view and press <Kbd k="Space" />. This will open a preview of your file in a separate window. This preview can then be closed by pressing <Kbd k="Space" /> again. This currently supports the filetypes that can be accessed by the `qlmanage` command.
|
||||
|
||||
#### Markdown
|
||||
|
||||
Opening a markdown file will bring up a view of the rendered markdown. These files cannot be edited in the preview at this time.
|
||||
|
||||
#### Images/Media
|
||||
|
||||
Opening a picture will bring up the image of that picture. Opening a video will bring up a player that lets you watch the video.
|
||||
|
||||
### Codeedit
|
||||
|
||||
Opening most text files will open Codeedit to either view or edit the file. It is technically part of the Preview widget, but it is important enough to be singled out.
|
||||
After opening a Codeedit widget, it is often useful to magnify it (<Kbd k="Cmd:m" />) to get a larger view. You can then use the hotkeys below to switch to edit mode, make your edits, save, and then use <Kbd k="Cmd:w" /> to close the widget (all without using the mouse!).
|
||||
|
||||
#### Switch to Edit Mode
|
||||
|
||||
To switch to edit mode, click the edit button to the right of the header. This lets you edit the file contents with a regular Monaco editor.
|
||||
You can also switch to edit mode by pressing <Kbd k="Cmd:e" />.
|
||||
|
||||
#### Save an Edit
|
||||
|
||||
Once an edit has been made in **edit mode**, click the save button to the right of the header to save the contents.
|
||||
You can also save by pressing <Kbd k="Cmd:s" />.
|
||||
|
||||
#### Exit Edit Mode Without Saving
|
||||
|
||||
To exit **edit mode** without saving, click the cancel button to the right of the header.
|
||||
You can also exit without saving by pressing <Kbd k="Cmd:r" />.
|
||||
|
||||
</PlatformProvider>
|
519
docs/docs/wsh.mdx
Normal file
@ -0,0 +1,519 @@
|
||||
---
|
||||
sidebar_position: 4
|
||||
id: "wsh"
|
||||
title: "wsh command"
|
||||
---
|
||||
|
||||
import { Kbd } from "@site/src/components/kbd.tsx";
|
||||
import { PlatformProvider, PlatformSelectorButton } from "@site/src/components/platformcontext.tsx";
|
||||
|
||||
<PlatformProvider>
|
||||
|
||||
# wsh command
|
||||
|
||||
The `wsh` command is always available from Wave blocks. It is a powerful tool for interacting with Wave blocks and can bridge data between your CLI and the widget GUIs.
|
||||
|
||||
---
|
||||
|
||||
## view
|
||||
|
||||
You can open a preview block with the contents of any file or directory by running:
|
||||
|
||||
```
|
||||
wsh view [path]
|
||||
```
|
||||
|
||||
You can use this command to easily preview images, markdown files, and directories. For code/text files this will open
|
||||
a codeedit block which you can use to quickly edit the file using Wave's embedded graphical editor.
|
||||
|
||||
---
|
||||
|
||||
## edit
|
||||
|
||||
```
|
||||
wsh edit [path]
|
||||
```
|
||||
|
||||
This will open up codeedit for the specified file. This is useful for quickly editing files on a local or remote machine in our graphical editor. This command will wait until the file is closed before exiting (unlike \`view\`) so you can set your \`$EDITOR\` to \`wsh editor\` for a seamless experience. You can combine this with a \`-m\` flag to open the editor in magnified mode.
|
||||
|
||||
---
|
||||
|
||||
## getmeta
|
||||
|
||||
You can view the metadata of any block or tab by running:
|
||||
|
||||
```
|
||||
# get the metadata for the current terminal block
|
||||
wsh getmeta
|
||||
|
||||
# get the metadata for block num 2 (see block numbers by holidng down Ctrl+Shift)
|
||||
wsh getmeta -b 2
|
||||
|
||||
# get the metadata for a blockid (get block ids by right clicking any block header "Copy Block Id")
|
||||
wsh getmeta -b [blockid]
|
||||
|
||||
# get the metadata for a tab
|
||||
wsh getmeta -b tab
|
||||
|
||||
# dump a single metadata key
|
||||
wsh getmeta [-b [blockid]] [key]
|
||||
|
||||
# dump a set of keys with a certain prefix
|
||||
wsh getmeta -b tab "bg:*"
|
||||
|
||||
# dump a set of keys with prefix (and include the 'clear' key)
|
||||
wsh getmeta -b tab --clear-prefix "bg:*"
|
||||
```
|
||||
|
||||
This is especially useful for preview and web blocks as you can see the file or url that they are pointing to and use that in your CLI scripts.
|
||||
|
||||
blockid format:
|
||||
|
||||
- `this` -- the current block (this is also the default)
|
||||
- `tab` -- the id of the current tab
|
||||
- `d6ff4966-231a-4074-b78a-20acc7226b41` -- a full blockid is a UUID
|
||||
- `a67f55a3` -- blockids may be truncated to the first 8 characters
|
||||
- `5` -- if a number less than 100 is given, it is a block number. blocks are numbered sequentially in the current tab from the top-left to bottom-right. holding <Kbd k="Ctrl:Shift"/> will show a block number overlay.
|
||||
|
||||
---
|
||||
|
||||
## setmeta
|
||||
|
||||
You can update any metadata key value pair for blocks (and tabs) by using the setmeta command. The setmeta command takes the same `-b` arguments as getmeta.
|
||||
|
||||
```
|
||||
wsh setmeta -b [blockid] [key]=[value]
|
||||
wsh setmeta -b [blockid] file=~/myfile.txt
|
||||
wsh setmeta -b [blockid] url=https://waveterm.dev/
|
||||
|
||||
# set the metadata for the current tab using the given json file
|
||||
wsh setmeta -b tab --json [jsonfile]
|
||||
|
||||
# set the metadata for the current tab using a json file read from stdin
|
||||
wsh setmeta -b tab --json
|
||||
```
|
||||
|
||||
You can get block and tab ids by right clicking on the appropriate block and selecting "Copy BlockId" (or use the block number via Ctrl:Shift). When you
|
||||
update the metadata for a preview or web block you'll see the changes reflected instantly in the block.
|
||||
|
||||
Other useful metadata values to override block titles, icons, colors, themes, etc.
|
||||
|
||||
Here's a complex command that will copy the background (bg:\* keys) from one tab to the current tab:
|
||||
|
||||
```
|
||||
wsh getmeta -b [other-tab-id] "bg:*" --clear-prefix | wsh setmeta -b tab --json -
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ai
|
||||
|
||||
Send messages to new or existing AI blocks directly from the CLI. `-f` passes a file. note that there is a maximum size of 10k for messages and files, so use a tail/grep to cut down file sizes before passing. The `-f` option works great for small files though like shell scripts or `.zshrc` etc.
|
||||
|
||||
By default the messages get sent to the first AI block (by blocknum). If no AI block exists, then a new one will be created. Use `-n` to force creation of a new AI block. Use `-b` to target a specific AI block.
|
||||
|
||||
```
|
||||
wsh ai "how do i write an ls command that sorts files in reverse size order"
|
||||
wsh ai -f <(tail -n 20 "my.log") -- "any idea what these error messages mean"
|
||||
wsh ai -f README.md "help me update this readme file"
|
||||
|
||||
# creates a new AI block
|
||||
wsh ai -n "tell me a story"
|
||||
|
||||
# targets block number 5
|
||||
wsh ai -b 5 "tell me more"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## editconfig
|
||||
|
||||
You can easily open up any of Wave's config files using this command.
|
||||
|
||||
```
|
||||
wsh editconfig [config-file-name]
|
||||
|
||||
# opens the default settings.json file
|
||||
wsh editconfig
|
||||
|
||||
# opens presets.json
|
||||
wsh editconfig presets.json
|
||||
|
||||
# opens widgets.json
|
||||
wsh editconfig widgets.json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## deleteblock
|
||||
|
||||
```
|
||||
wsh deleteblock -b [blockid]
|
||||
```
|
||||
|
||||
This will delete the block with the specified id.
|
||||
|
||||
---
|
||||
|
||||
## ssh
|
||||
|
||||
```
|
||||
wsh ssh [user@host]
|
||||
```
|
||||
|
||||
This will use Wave's internal ssh implementation to connect to the specified remote machine.
|
||||
|
||||
---
|
||||
|
||||
## wsl
|
||||
|
||||
```
|
||||
wsh wsl [-d <distribution name>]
|
||||
```
|
||||
|
||||
This will connect to a WSL distribution on the local machine. It will use the default if no distribution is provided.
|
||||
|
||||
---
|
||||
|
||||
## web
|
||||
|
||||
You can search for a given url using:
|
||||
|
||||
```
|
||||
wsh web <url>
|
||||
```
|
||||
|
||||
Alternatively, you can search with the configured search engine using:
|
||||
|
||||
```
|
||||
wsh web <search query>
|
||||
```
|
||||
|
||||
Both of these commands will open a new web block with the desired page.
|
||||
|
||||
---
|
||||
|
||||
## conn
|
||||
|
||||
This has several subcommands which all perform various features related to connections.
|
||||
|
||||
### status
|
||||
|
||||
```
|
||||
wsh conn status
|
||||
```
|
||||
|
||||
This command gives the status of all connections made since waveterm started.
|
||||
|
||||
### reinstall
|
||||
|
||||
For ssh connections,
|
||||
|
||||
```
|
||||
wsh conn reinstall [user@host]
|
||||
```
|
||||
|
||||
For wsl connections,
|
||||
|
||||
```
|
||||
wsh conn reinstall [wsl://<distribution name>]
|
||||
```
|
||||
|
||||
This command reinstalls the Wave Shell Extensions on the specified connection.
|
||||
|
||||
### disconnect
|
||||
|
||||
For ssh connections,
|
||||
|
||||
```
|
||||
wsh conn disconnect [user@host]
|
||||
```
|
||||
|
||||
For wsl connections,
|
||||
|
||||
```
|
||||
wsh conn disconnect [wsl://<distribution name>]
|
||||
```
|
||||
|
||||
This command completely disconnects the specified connection. This will apply to all blocks where the connection is being used
|
||||
|
||||
### connect
|
||||
|
||||
For ssh connections,
|
||||
|
||||
```
|
||||
wsh conn connect [user@host]
|
||||
```
|
||||
|
||||
For wsl connections,
|
||||
|
||||
```
|
||||
wsh conn connect [wsl://<distribution name>]
|
||||
```
|
||||
|
||||
This command connects to the specified connection but does not create a block for it.
|
||||
|
||||
### ensure
|
||||
|
||||
For ssh connections,
|
||||
|
||||
```
|
||||
wsh conn ensure [user@host]
|
||||
```
|
||||
|
||||
For wsl connections,
|
||||
|
||||
```
|
||||
wsh conn ensure [wsl://<distribution name>]
|
||||
```
|
||||
|
||||
This command connects to the specified connection if it isn't already connected.
|
||||
|
||||
---
|
||||
|
||||
## setconfig
|
||||
|
||||
```
|
||||
wsh setconfig [config name]=[config value]
|
||||
```
|
||||
|
||||
This allows setting various options in the `config/settings.json` file. It will check to be sure a valid config option was provided.
|
||||
|
||||
---
|
||||
|
||||
## file
|
||||
|
||||
The `file` command provides a set of subcommands for managing files stored in Wave blocks. Files are referenced using `wavefile://` URLs which specify the zone where the file is stored (e.g., `wavefile://block/mydocs.md` or `wavefile://global/myfile.txt`).
|
||||
|
||||
### cat
|
||||
|
||||
```bash
|
||||
wsh file cat wavefile://global/filename
|
||||
```
|
||||
|
||||
Display the contents of a wave file. For example:
|
||||
|
||||
```bash
|
||||
wsh file cat wavefile://block/config.txt
|
||||
wsh file cat wavefile://client/settings.json
|
||||
```
|
||||
|
||||
### write
|
||||
|
||||
```bash
|
||||
wsh file write wavefile://tab/filename
|
||||
```
|
||||
|
||||
Write data from stdin to a wave file. The maximum file size is 10MB. For example:
|
||||
|
||||
```bash
|
||||
echo "hello" | wsh file write wavefile://block/greeting.txt
|
||||
cat config.json | wsh file write wavefile://client/settings.json
|
||||
```
|
||||
|
||||
### append
|
||||
|
||||
```bash
|
||||
wsh file append wavefile://global/filename
|
||||
```
|
||||
|
||||
Append data from stdin to an existing wave file, respecting the 10MB total file size limit. This is useful for log files or accumulating data. For example:
|
||||
|
||||
```bash
|
||||
tail -f app.log | wsh file append wavefile://block/logs.txt
|
||||
echo "new line" | wsh file append wavefile://client/notes.txt
|
||||
```
|
||||
|
||||
### rm
|
||||
|
||||
```bash
|
||||
wsh file rm wavefile://client/filename
|
||||
```
|
||||
|
||||
Remove a wave file. For example:
|
||||
|
||||
```bash
|
||||
wsh file rm wavefile://block/old-config.txt
|
||||
wsh file rm wavefile://client/temp.json
|
||||
```
|
||||
|
||||
### info
|
||||
|
||||
```bash
|
||||
wsh file info wavefile://client/filename
|
||||
```
|
||||
|
||||
Display information about a wave file including size, creation time, modification time, and metadata. For example:
|
||||
|
||||
```bash
|
||||
wsh file info wavefile://block/config.txt
|
||||
wsh file info wavefile://client/settings.json
|
||||
```
|
||||
|
||||
### cp
|
||||
|
||||
```bash
|
||||
wsh file cp source destination
|
||||
```
|
||||
|
||||
Copy files between wave storage and the local filesystem. Exactly one of the source or destination must be a wavefile:// URL. For example:
|
||||
|
||||
```bash
|
||||
# Copy from wave storage to local filesystem
|
||||
wsh file cp wavefile://block/config.txt ./local-config.txt
|
||||
|
||||
# Copy from local filesystem to wave storage
|
||||
wsh file cp ./local-config.txt wavefile://client/config.txt
|
||||
```
|
||||
|
||||
### ls
|
||||
|
||||
```bash
|
||||
wsh file ls [flags] [wavefile://zone/path]
|
||||
```
|
||||
|
||||
List wave files in a zone. If no path is specified, lists all files in `wavefile://client/`.
|
||||
|
||||
Examples:
|
||||
|
||||
```bash
|
||||
# List all files in client zone
|
||||
wsh file ls
|
||||
|
||||
# List files in a specific zone
|
||||
wsh file ls wavefile://block/
|
||||
wsh file ls wavefile://workspace/configs/
|
||||
|
||||
# Show detailed file information
|
||||
wsh file ls -l wavefile://client/
|
||||
|
||||
# List files recursively
|
||||
wsh file ls -r wavefile://block/
|
||||
|
||||
# List one file per line (good for scripting)
|
||||
wsh file ls -1 wavefile://client/
|
||||
```
|
||||
|
||||
Flags:
|
||||
|
||||
- `-l, --long` - use long listing format showing size, timestamps, and metadata
|
||||
- `-r, --recursive` - list subdirectories recursively
|
||||
- `-1, --one` - list one file per line
|
||||
- `-f, --files` - list only files (no directories)
|
||||
|
||||
When output is piped to another command, automatically switches to one-file-per-line format:
|
||||
|
||||
```bash
|
||||
# Easy to process with grep, awk, etc.
|
||||
wsh file ls wavefile://client/ | grep ".json$"
|
||||
```
|
||||
|
||||
:::info
|
||||
|
||||
Note: Wave file locations can be:
|
||||
|
||||
- `wavefile://block/...` - stored in the current block ("this" is also an alias for "block")
|
||||
- `wavefile://tab/...` - stored in the current tab
|
||||
- `wavefile://workspace/...` - stored in the current workspace ("ws" is also an alias for "workspace")
|
||||
- `wavefile://client/...` - stored globally for the client ("global" is also an alias for "client")
|
||||
- `wavefile://temp/...` - stored globally, but removed on startup/shutdown
|
||||
- `wavefile://[uuid]/...` - an entity id (can be a block, tab, workspace, etc.)
|
||||
|
||||
All file operations respect a maximum file size of 10MB.
|
||||
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## getvar/setvar
|
||||
|
||||
Wave Terminal provides commands for managing persistent variables at different scopes (block, tab, workspace, or client-wide).
|
||||
|
||||
### setvar
|
||||
|
||||
```bash
|
||||
wsh setvar [flags] KEY=VALUE...
|
||||
```
|
||||
|
||||
Set one or more variables. By default, variables are set at the client (global) level. Use `-l` for block-local variables.
|
||||
|
||||
Examples:
|
||||
|
||||
```bash
|
||||
# Set a single variable
|
||||
wsh setvar API_KEY=abc123
|
||||
|
||||
# Set multiple variables at once
|
||||
wsh setvar HOST=localhost PORT=8080 DEBUG=true
|
||||
|
||||
# Set a block-local variable
|
||||
wsh setvar -l BLOCK_SPECIFIC=value
|
||||
|
||||
# Remove variables
|
||||
wsh setvar -r API_KEY PORT
|
||||
```
|
||||
|
||||
Flags:
|
||||
|
||||
- `-l, --local` - set variables local to the current block
|
||||
- `-r, --remove` - remove the specified variables instead of setting them
|
||||
- `--varfile string` - use a different variable file (default "var")
|
||||
- `-b [blockid]` - used to set a specific zone (block, tab, workspace, client, or UUID)
|
||||
|
||||
### getvar
|
||||
|
||||
```bash
|
||||
wsh getvar [flags] [key]
|
||||
```
|
||||
|
||||
Get the value of a variable. Returns exit code 0 if the variable exists, 1 if it doesn't. This allows for shell scripting like:
|
||||
|
||||
```bash
|
||||
# Check if a variable exists
|
||||
if wsh getvar API_KEY >/dev/null; then
|
||||
echo "API key is set"
|
||||
fi
|
||||
|
||||
# Use a variable in a command
|
||||
curl -H "Authorization: $(wsh getvar API_KEY)" https://api.example.com
|
||||
|
||||
# Get a block-local variable
|
||||
wsh getvar -l BLOCK_SPECIFIC
|
||||
|
||||
# List all variables
|
||||
wsh getvar --all
|
||||
|
||||
# List all variables with null terminators (for scripting)
|
||||
wsh getvar --all -0
|
||||
```
|
||||
|
||||
Flags:
|
||||
|
||||
- `-l, --local` - get variables local to the current block
|
||||
- `--all` - list all variables
|
||||
- `-0, --null` - use null terminators in output instead of newlines
|
||||
- `--varfile string` - use a different variable file (default "var")
|
||||
|
||||
Variables can be accessed at different scopes using the `-b` flag:
|
||||
|
||||
```bash
|
||||
# Get/set at block level
|
||||
wsh getvar -b block MYVAR
|
||||
wsh setvar -b block MYVAR=value
|
||||
|
||||
# Get/set at tab level
|
||||
wsh getvar -b tab MYVAR
|
||||
wsh setvar -b tab MYVAR=value
|
||||
|
||||
# Get/set at workspace level
|
||||
wsh getvar -b workspace MYVAR
|
||||
wsh setvar -b workspace MYVAR=value
|
||||
|
||||
# Get/set at client (global) level
|
||||
wsh getvar -b client MYVAR
|
||||
wsh setvar -b client MYVAR=value
|
||||
```
|
||||
|
||||
Variables set with these commands persist across sessions and can be used to store configuration values, secrets, or any other string data that needs to be accessible across blocks or tabs.
|
||||
|
||||
</PlatformProvider>
|
181
docs/docusaurus.config.ts
Normal file
@ -0,0 +1,181 @@
|
||||
import type { Config } from "@docusaurus/types";
|
||||
import { docOgRenderer } from "./src/renderer/image-renderers";
|
||||
|
||||
const baseUrl = process.env.EMBEDDED ? "/docsite/" : "/";
|
||||
|
||||
const config: Config = {
|
||||
title: "Wave Terminal Documentation",
|
||||
tagline: "Level Up Your Terminal With Graphical Widgets",
|
||||
favicon: "img/logo/wave-logo_appicon.svg",
|
||||
|
||||
// Set the production url of your site here
|
||||
url: "https://docs.waveterm.dev/",
|
||||
// Set the /<baseUrl>/ pathname under which your site is served
|
||||
// For GitHub pages deployment, it is often '/<projectName>/'
|
||||
baseUrl,
|
||||
|
||||
// GitHub pages deployment config.
|
||||
// If you aren't using GitHub pages, you don't need these.
|
||||
organizationName: "wavetermdev", // Usually your GitHub org/user name.
|
||||
projectName: "waveterm-docs", // Usually your repo name.
|
||||
deploymentBranch: "main",
|
||||
|
||||
onBrokenAnchors: "ignore",
|
||||
onBrokenLinks: "throw",
|
||||
onBrokenMarkdownLinks: "warn",
|
||||
trailingSlash: false,
|
||||
|
||||
// Even if you don't use internationalization, you can use this field to set
|
||||
// useful metadata like html lang. For example, if your site is Chinese, you
|
||||
// may want to replace "en" with "zh-Hans".
|
||||
i18n: {
|
||||
defaultLocale: "en",
|
||||
locales: ["en"],
|
||||
},
|
||||
plugins: [
|
||||
[
|
||||
"content-docs",
|
||||
{
|
||||
path: "docs",
|
||||
routeBasePath: "/",
|
||||
exclude: ["features/**"],
|
||||
editUrl: !process.env.EMBEDDED ? "https://github.com/wavetermdev/waveterm/edit/main/docs/" : undefined,
|
||||
} as import("@docusaurus/plugin-content-docs").Options,
|
||||
],
|
||||
"ideal-image",
|
||||
[
|
||||
"@docusaurus/plugin-sitemap",
|
||||
{
|
||||
changefreq: "daily",
|
||||
filename: "sitemap.xml",
|
||||
},
|
||||
],
|
||||
!process.env.EMBEDDED && [
|
||||
"@waveterm/docusaurus-og",
|
||||
{
|
||||
path: "./preview-images", // relative to the build directory
|
||||
imageRenderers: {
|
||||
"docusaurus-plugin-content-docs": docOgRenderer,
|
||||
},
|
||||
},
|
||||
],
|
||||
].filter((v) => v),
|
||||
themes: [
|
||||
["classic", { customCss: "src/css/custom.css" }],
|
||||
!process.env.EMBEDDED && "@docusaurus/theme-search-algolia",
|
||||
].filter((v) => v),
|
||||
themeConfig: {
|
||||
docs: {
|
||||
sidebar: {
|
||||
hideable: false,
|
||||
autoCollapseCategories: false,
|
||||
},
|
||||
},
|
||||
colorMode: {
|
||||
defaultMode: "light",
|
||||
disableSwitch: false,
|
||||
respectPrefersColorScheme: true,
|
||||
},
|
||||
navbar: {
|
||||
logo: {
|
||||
src: "img/logo/wave-light.png",
|
||||
srcDark: "img/logo/wave-dark.png",
|
||||
href: "https://www.waveterm.dev/",
|
||||
},
|
||||
hideOnScroll: true,
|
||||
items: [
|
||||
{
|
||||
type: "doc",
|
||||
position: "left",
|
||||
docId: "index",
|
||||
label: "Docs",
|
||||
},
|
||||
!process.env.EMBEDDED
|
||||
? [
|
||||
{
|
||||
position: "left",
|
||||
href: "https://docs.waveterm.dev/storybook",
|
||||
label: "Storybook",
|
||||
},
|
||||
{
|
||||
href: "https://discord.gg/zUeP2aAjaP",
|
||||
position: "right",
|
||||
className: "header-link-custom custom-icon-discord",
|
||||
"aria-label": "Discord invite",
|
||||
},
|
||||
{
|
||||
href: "https://github.com/wavetermdev/waveterm",
|
||||
position: "right",
|
||||
className: "header-link-custom custom-icon-github",
|
||||
"aria-label": "GitHub repository",
|
||||
},
|
||||
]
|
||||
: [],
|
||||
].flat(),
|
||||
},
|
||||
metadata: [
|
||||
{
|
||||
name: "keywords",
|
||||
content:
|
||||
"terminal, developer, development, command, line, wave, linux, macos, windows, connection, ssh, cli, waveterm, documentation, docs, ai, graphical, widgets, remote, open, source, open-source, go, golang, react, typescript, javascript",
|
||||
},
|
||||
{
|
||||
name: "og:type",
|
||||
content: "website",
|
||||
},
|
||||
{
|
||||
name: "og:site_name",
|
||||
content: "Wave Terminal Documentation",
|
||||
},
|
||||
{
|
||||
name: "application-name",
|
||||
content: "Wave Terminal Documentation",
|
||||
},
|
||||
{
|
||||
name: "apple-mobile-web-app-title",
|
||||
content: "Wave Terminal Documentation",
|
||||
},
|
||||
],
|
||||
footer: {
|
||||
copyright: `Copyright © ${new Date().getFullYear()} Command Line Inc. Built with Docusaurus.`,
|
||||
},
|
||||
algolia: {
|
||||
appId: "B6A8512SN4",
|
||||
apiKey: "e879cd8663f109b2822cd004d9cd468c",
|
||||
indexName: "waveterm",
|
||||
},
|
||||
},
|
||||
headTags: [
|
||||
{
|
||||
tagName: "link",
|
||||
attributes: {
|
||||
rel: "preload",
|
||||
as: "font",
|
||||
type: "font/woff2",
|
||||
"data-next-font": "size-adjust",
|
||||
href: `${baseUrl}fontawesome/webfonts/fa-sharp-regular-400.woff2`,
|
||||
},
|
||||
},
|
||||
{
|
||||
tagName: "link",
|
||||
attributes: {
|
||||
rel: "sitemap",
|
||||
type: "application/xml",
|
||||
title: "Sitemap",
|
||||
href: `${baseUrl}sitemap.xml`,
|
||||
},
|
||||
},
|
||||
!process.env.EMBEDDED && {
|
||||
tagName: "script",
|
||||
attributes: {
|
||||
defer: "true",
|
||||
"data-domain": "docs.waveterm.dev",
|
||||
src: "https://plausible.io/js/script.file-downloads.outbound-links.tagged-events.js",
|
||||
},
|
||||
},
|
||||
].filter((v) => v),
|
||||
stylesheets: [`${baseUrl}fontawesome/css/fontawesome.min.css`, `${baseUrl}fontawesome/css/sharp-regular.min.css`],
|
||||
staticDirectories: ["static", "storybook"],
|
||||
};
|
||||
|
||||
export default config;
|
27
docs/eslint.config.js
Normal file
@ -0,0 +1,27 @@
|
||||
// @ts-check
|
||||
|
||||
import eslint from "@eslint/js";
|
||||
import eslintConfigPrettier from "eslint-config-prettier";
|
||||
import * as mdx from "eslint-plugin-mdx";
|
||||
import tseslint from "typescript-eslint";
|
||||
|
||||
const baseConfig = tseslint.config(
|
||||
eslint.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
mdx.flat,
|
||||
mdx.flatCodeBlocks
|
||||
);
|
||||
|
||||
const customConfig = {
|
||||
...baseConfig,
|
||||
overrides: [
|
||||
{
|
||||
files: ["emain/emain.ts", "electron.vite.config.ts"],
|
||||
env: {
|
||||
node: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default [customConfig, eslintConfigPrettier];
|
78
docs/package.json
Normal file
@ -0,0 +1,78 @@
|
||||
{
|
||||
"name": "waveterm-docs",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"docusaurus": "docusaurus",
|
||||
"start": "docusaurus start",
|
||||
"build": "USE_SIMPLE_CSS_MINIFIER=true docusaurus build",
|
||||
"build-embedded": "EMBEDDED=true run build",
|
||||
"swizzle": "docusaurus swizzle",
|
||||
"deploy": "docusaurus deploy",
|
||||
"clear": "docusaurus clear",
|
||||
"serve": "docusaurus serve",
|
||||
"write-translations": "docusaurus write-translations",
|
||||
"write-heading-ids": "docusaurus write-heading-ids",
|
||||
"typecheck": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "^3.6.3",
|
||||
"@docusaurus/plugin-content-docs": "^3.6.3",
|
||||
"@docusaurus/plugin-debug": "^3.6.3",
|
||||
"@docusaurus/plugin-ideal-image": "^3.6.3",
|
||||
"@docusaurus/plugin-sitemap": "^3.6.3",
|
||||
"@docusaurus/theme-classic": "^3.6.3",
|
||||
"@docusaurus/theme-search-algolia": "^3.6.3",
|
||||
"@mdx-js/react": "^3.0.0",
|
||||
"@waveterm/docusaurus-og": "https://github.com/wavetermdev/docusaurus-og",
|
||||
"clsx": "^2.1.1",
|
||||
"prism-react-renderer": "^2.3.0",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"remark-typescript-code-import": "^1.0.1",
|
||||
"ua-parser-js": "^2.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@docusaurus/module-type-aliases": "3.6.3",
|
||||
"@docusaurus/tsconfig": "3.6.3",
|
||||
"@docusaurus/types": "3.6.3",
|
||||
"@eslint/js": "^9.15.0",
|
||||
"@mdx-js/typescript-plugin": "^0.0.6",
|
||||
"@types/eslint": "^9.6.1",
|
||||
"@types/eslint-config-prettier": "^6.11.3",
|
||||
"@types/ua-parser-js": "^0.7.39",
|
||||
"eslint": "^9.15.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-mdx": "^3.1.5",
|
||||
"prettier": "^3.3.3",
|
||||
"prettier-plugin-jsdoc": "^1.3.0",
|
||||
"prettier-plugin-organize-imports": "^4.1.0",
|
||||
"remark-cli": "^12.0.1",
|
||||
"remark-frontmatter": "^5.0.0",
|
||||
"remark-mdx": "^3.1.0",
|
||||
"remark-preset-lint-consistent": "^6.0.0",
|
||||
"remark-preset-lint-recommended": "^7.0.0",
|
||||
"typescript": "^5.7.2",
|
||||
"typescript-eslint": "^8.15.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"path-to-regexp@npm:2.2.1": "^3",
|
||||
"cookie@0.6.0": "^0.7.0"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.5%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 3 chrome version",
|
||||
"last 3 firefox version",
|
||||
"last 5 safari version"
|
||||
]
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0"
|
||||
},
|
||||
"packageManager": "yarn@4.4.1"
|
||||
}
|
12
docs/prettier.config.cjs
Normal file
@ -0,0 +1,12 @@
|
||||
/** @type {import("prettier").Config} */
|
||||
module.exports = {
|
||||
plugins: ["prettier-plugin-jsdoc", "prettier-plugin-organize-imports"],
|
||||
printWidth: 120,
|
||||
trailingComma: "es5",
|
||||
useTabs: false,
|
||||
singleQuote: false,
|
||||
jsdocVerticalAlignment: true,
|
||||
jsdocSeparateReturnsFromParam: true,
|
||||
jsdocSeparateTagGroups: true,
|
||||
jsdocPreferCodeFences: true,
|
||||
};
|
64
docs/src/components/card.css
Normal file
@ -0,0 +1,64 @@
|
||||
.card-group {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-template-rows: auto;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 450px) {
|
||||
.card-group {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 451px) and (max-width: 995px) {
|
||||
.card-group {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 996px) {
|
||||
.card-group {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
display: grid;
|
||||
grid-template-columns: 1.5rem 1rem 1fr;
|
||||
grid-template-rows: subgrid;
|
||||
grid-column: span 1;
|
||||
grid-row: span 2;
|
||||
padding: 1rem;
|
||||
|
||||
.icon {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
font-size: 1.5rem;
|
||||
line-height: 1.5rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
grid-column: 3;
|
||||
grid-row: 1;
|
||||
font-weight: bold;
|
||||
font-size: 1.2rem;
|
||||
line-height: 1.5rem;
|
||||
}
|
||||
|
||||
.description {
|
||||
color: var(--ifm-font-color-base);
|
||||
grid-column: span 3;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
border: 2px solid var(--ifm-color-primary-lightest);
|
||||
transition: transform 0.1s ease;
|
||||
transform-origin: 50% 50%;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
text-decoration: none;
|
||||
transform: translateZ(0) scale(1.024);
|
||||
-webkit-transform: translateZ(0) scale(1.024);
|
||||
}
|
23
docs/src/components/card.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import clsx from "clsx";
|
||||
import "./card.css";
|
||||
|
||||
interface CardProps {
|
||||
icon: string;
|
||||
title: string;
|
||||
description: string;
|
||||
href: string;
|
||||
}
|
||||
|
||||
export function Card({ icon, title, description, href }: CardProps) {
|
||||
return (
|
||||
<a className="card" href={href}>
|
||||
<div className={clsx("icon", "fa-sharp fa-regular", icon)} />
|
||||
<div className="title">{title}</div>
|
||||
<div className="description">{description}</div>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
export function CardGroup({ children }) {
|
||||
return <div className="card-group">{children}</div>;
|
||||
}
|
43
docs/src/components/kbd.css
Normal file
@ -0,0 +1,43 @@
|
||||
@font-face {
|
||||
font-family: "JetBrains Mono";
|
||||
src: url("/static/fonts/JetBrainsMono-Regular.woff2") format("woff2");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "JetBrains Mono";
|
||||
src: url("/static/fonts/JetBrainsMono-Bold.woff2") format("woff2");
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.kbd-group {
|
||||
display: inline-flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
kbd {
|
||||
background-color: var(--ifm-color-primary-contrast-background);
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--ifm-color-secondary-darker);
|
||||
color: var(--ifm-color-primary-contrast-foreground);
|
||||
padding: 2px 6px;
|
||||
font-size: 0.8em;
|
||||
font-family: "JetBrains Mono", monospace;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 24px;
|
||||
line-height: 24px;
|
||||
|
||||
.spaced {
|
||||
letter-spacing: 0.2em;
|
||||
}
|
||||
}
|
||||
|
||||
.kbd-group kbd.symbol {
|
||||
font-size: 0.8em;
|
||||
line-height: 24px;
|
||||
}
|