Merge branch 'main' into evan/react-19

This commit is contained in:
Evan Simkowitz 2025-01-21 13:48:24 -08:00
commit 6c403bea61
No known key found for this signature in database
342 changed files with 6555 additions and 4589 deletions

View File

@ -8,98 +8,136 @@ updates:
time: "09:00"
timezone: "America/Los_Angeles"
groups:
dev-dependencies:
dev-dependencies-patch:
dependency-type: "development"
exclude-patterns:
- "*storybook*"
- "*electron*"
- "jotai"
- "react"
- "@types/react"
- "*react-dom"
- "*docusaurus*"
update-types:
- "minor"
- "patch"
dev-dependencies-major:
dev-dependencies-minor:
dependency-type: "development"
exclude-patterns:
- "*storybook*"
- "*electron*"
- "@types/react"
update-types:
- "major"
prod-dependencies:
dependency-type: "production"
exclude-patterns:
- "*electron*"
- "jotai"
- "react"
- "@types/react"
- "*react-dom"
- "*docusaurus*"
update-types:
- "minor"
- "patch"
prod-dependencies-major:
prod-dependencies-patch:
dependency-type: "production"
exclude-patterns:
- "*storybook*"
- "*electron*"
- "jotai"
- "react"
- "@types/react"
- "*react-dom"
- "*docusaurus*"
update-types:
- "major"
- "patch"
prod-dependencies-minor:
dependency-type: "production"
exclude-patterns:
- "*storybook*"
- "*electron*"
- "jotai"
- "react"
- "@types/react"
- "*react-dom"
- "*docusaurus*"
update-types:
- "minor"
storybook:
storybook-patch:
patterns:
- "*storybook*"
update-types:
- "patch"
storybook-minor:
patterns:
- "*storybook*"
update-types:
- "minor"
- "patch"
storybook-major:
patterns:
- "*storybook*"
update-types:
- "major"
electron:
electron-patch:
patterns:
- "*electron*"
update-types:
- "patch"
electron-minor:
patterns:
- "*electron*"
update-types:
- "minor"
- "patch"
electron-major:
patterns:
- "*electron*"
update-types:
- "major"
docusaurus:
docusaurus-patch:
patterns:
- "*docusaurus*"
update-types:
- "patch"
docusaurus-minor:
patterns:
- "*docusaurus*"
update-types:
- "minor"
- "patch"
docusaurus-major:
patterns:
- "*docusaurus*"
update-types:
- "major"
react:
react-patch:
patterns:
- "react"
- "@types/react"
- "*react-dom"
update-types:
- "patch"
react-minor:
patterns:
- "react"
- "@types/react"
- "*react-dom"
update-types:
- "minor"
- "patch"
react-major:
patterns:
- "react"
- "@types/react"
- "*react-dom"
update-types:
- "major"
jotai:
jotai-patch:
patterns:
- "jotai"
update-types:
- "patch"
jotai-minor:
patterns:
- "jotai"
update-types:
- "minor"
- "patch"
jotai-major:
patterns:
- "jotai"

View File

@ -24,7 +24,7 @@ jobs:
- platform: "linux"
runner: "ubuntu-latest"
- platform: "linux"
runner: ubuntu-24.04-arm64-16core
runner: ubuntu-24.04-arm
- platform: "windows"
runner: "windows-latest"
# - platform: "windows"
@ -38,7 +38,9 @@ jobs:
sudo apt-get update
sudo apt-get install --no-install-recommends -y libarchive-tools libopenjp2-tools rpm squashfs-tools
sudo snap install snapcraft --classic
sudo snap install zig --classic --beta # We use Zig instead of glibc for cgo compilation as it is more-easily statically linked
- name: Install Zig (not Mac)
if: matrix.platform != 'darwin'
uses: mlugg/setup-zig@v1
# The pre-installed version of the AWS CLI has a segfault problem so we'll install it via Homebrew instead.
- name: Upgrade AWS CLI (Mac only)

View File

@ -16,6 +16,11 @@ on:
branches: ["main"]
pull_request:
branches: ["main"]
types:
- opened
- synchronize
- reopened
- ready_for_review
schedule:
- cron: "36 5 * * 5"
@ -31,6 +36,7 @@ jobs:
# - https://gh.io/supported-runners-and-hardware-resources
# - https://gh.io/using-larger-runners
# Consider using larger runners for possible analysis time improvements.
if: github.event.pull_request.draft == false
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }}
permissions:

View File

@ -14,6 +14,11 @@ on:
pull_request:
branches:
- main
types:
- opened
- synchronize
- reopened
- ready_for_review
paths:
- "docs/**"
- "storybook/**"
@ -26,6 +31,7 @@ jobs:
build:
name: Build Docsite
runs-on: ubuntu-latest
if: github.event.pull_request.draft == false
steps:
- uses: actions/checkout@v4
with:

View File

@ -6,10 +6,16 @@ on:
branches:
- main
- master
types:
- opened
- synchronize
- reopened
- ready_for_review
jobs:
merge-gatekeeper:
runs-on: ubuntu-latest
if: github.event.pull_request.draft == false
# Restrict permissions of the GITHUB_TOKEN.
# Docs: https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs
permissions:
@ -23,4 +29,4 @@ jobs:
uses: upsidr/merge-gatekeeper@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
ignored: Build for TestDriver.ai, Analyze (go), Analyze (javascript-typescript), License Compliance, CodeRabbit
ignored: Build for TestDriver.ai, TestDriver.ai Run, Analyze (go), Analyze (javascript-typescript), License Compliance, CodeRabbit

View File

@ -7,17 +7,22 @@ on:
tags:
- "v[0-9]+.[0-9]+.[0-9]+*"
pull_request:
branches:
- main
paths-ignore:
- "docs/**"
- ".storybook/**"
- ".vscode/**"
- ".editorconfig"
- ".gitignore"
- ".prettierrc"
- ".eslintrc.js"
- "**/*.md"
# branches:
# - main
# paths-ignore:
# - "docs/**"
# - ".storybook/**"
# - ".vscode/**"
# - ".editorconfig"
# - ".gitignore"
# - ".prettierrc"
# - ".eslintrc.js"
# - "**/*.md"
types:
- opened
- synchronize
- reopened
- ready_for_review
schedule:
- cron: 0 21 * * *
workflow_dispatch: null
@ -34,6 +39,7 @@ jobs:
build_and_upload:
name: Build for TestDriver.ai
runs-on: windows-latest
if: github.event.pull_request.draft == false
steps:
- uses: actions/checkout@v4
@ -57,6 +63,8 @@ jobs:
with:
version: 3.x
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Install Zig
uses: mlugg/setup-zig@v1
- name: Build
run: task package

View File

@ -11,14 +11,58 @@ env:
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
pull-requests: write
checks: write
actions: read
jobs:
context:
runs-on: ubuntu-22.04
steps:
- name: Dump GitHub context
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT"
- name: Dump job context
env:
JOB_CONTEXT: ${{ toJson(job) }}
run: echo "$JOB_CONTEXT"
- name: Dump steps context
env:
STEPS_CONTEXT: ${{ toJson(steps) }}
run: echo "$STEPS_CONTEXT"
- name: Dump runner context
env:
RUNNER_CONTEXT: ${{ toJson(runner) }}
run: echo "$RUNNER_CONTEXT"
- name: Dump strategy context
env:
STRATEGY_CONTEXT: ${{ toJson(strategy) }}
run: echo "$STRATEGY_CONTEXT"
- name: Dump matrix context
env:
MATRIX_CONTEXT: ${{ toJson(matrix) }}
run: echo "$MATRIX_CONTEXT"
run_testdriver:
name: Run TestDriver.ai
runs-on: windows-latest
if: github.event.workflow_run.conclusion == 'success'
steps:
- name: Create Check Run
id: create-check
uses: actions/github-script@v7
with:
script: |
const check = await github.rest.checks.create({
owner: context.repo.owner,
repo: context.repo.repo,
name: 'TestDriver.ai Run',
head_sha: '${{ github.event.workflow_run.head_sha }}',
status: 'in_progress'
});
return check.data.id;
- uses: testdriverai/action@main
id: testdriver
env:
@ -110,3 +154,23 @@ jobs:
prompt: |
1. /run testdriver/onboarding.yml
- name: Update Check Run
if: always()
uses: actions/github-script@v7
with:
script: |
const checkId = steps['create-check'].outputs.result;
await github.rest.checks.update({
owner: context.repo.owner,
repo: context.repo.repo,
check_run_id: checkId,
status: 'completed',
conclusion: steps.testdriver.outcome === 'success' ? 'success' : 'failure',
output: {
title: 'TestDriver.ai Results',
summary: steps.testdriver.outcome === 'success'
? '✅ All tests passed'
: '❌ Tests failed'
}
});

View File

@ -50,11 +50,9 @@ For packaging, the following additional packages are required:
#### 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.
You will need the [Zig](https://ziglang.org/) compiler for statically linking CGO.
The easiest way to install this is using MSYS2: https://www.msys2.org/
If you prefer an alternative method, you can find other methods here: https://www.mingw-w64.org/downloads/
You can find installation instructions for Zig on Windows [here](https://ziglang.org/learn/getting-started/#managers).
### Task
@ -103,6 +101,14 @@ or
git clone https://github.com/wavetermdev/waveterm.git
```
## Install code dependencies
The first time you clone the repo, you'll need to run the following to load the dependencies. If you ever have issues building the app, try running this again:
```sh
task init
```
## Build and Run
All the methods below will install Node and Go dependencies when they run the first time. All these should be run from within the Git repository.

View File

@ -53,3 +53,31 @@ Guidelines:
- Develop features on a branch - do not work on the main branch
- For anything but minor fixes, please submit tests and documentation
- Please reference the issue in the pull request
## Project Structure
The project is broken into four main components: frontend, emain, wavesrv, and wsh. This section is a work-in-progress as our codebase is constantly changing.
### Frontend
Our frontend can be found in the [`/frontend`](./frontend/) directory. It is written in React Typescript. The main entrypoint is [`wave.ts`](./frontend/wave.ts) and the root for the React VDOM is [`app.tsx`](./frontend/app/app.tsx). If you are using `task dev` to run your dev instance of the app, the frontend will be loaded using Vite, which allows for Hot Module Reloading. This should work for most styling and simple component changes, but anything that affects the state of the app (the Jotai or layout code, for instance) may put the frontend into a bad state. If this happens, you can force reload the frontend using `Cmd:Shift:R` or `Ctrl:Shift:R`.
We also have a Storybook project configured for testing our component library. We're still working to fill out the test cases for this, but it is useful for testing components in isolation. You can run this using `task storybook`.
### emain
emain can be found at [`/emain`](./emain/). It is the main NodeJS process and is first thing that is run when you start up the app and it forks off the process for the wavesrv backend and manages all the Electron interfaces, such as window and view management, context menus, and native UI calls. Its main entrypoint is [`emain.ts`](./emain/emain.ts). This process does not hot-reload, you will need to manually kill the dev instance and rerun it to apply changes.
The frontend and emain communicate using the [Electron IPC mechanism](https://www.electronjs.org/docs/latest/tutorial/ipc). All exposed functions between the two are defined twice, once in [`preload.ts`](./emain/preload.ts) and once in [`custom.d.ts`](./frontend/types/custom.d.ts). On the frontend, you call the exposed function by calling `getApi().<function>()`.
### wavesrv
wavesrv can be found at [`/cmd/server`](./cmd/server), with most business logic located in [`/pkg`](./pkg/). It is the primary Go backend for our app and manages the database and all communications with remote hosts. Its main entrypoint is [`main-server.go`](./cmd/server/main-server.go). This process does not hot-reload, you will need to manually kill the dev instance and rerun it to apply changes.
Communication between the wavesrv and the frontend and emain is handled by both HTTP services (found at [`/pkg/service`](./pkg/service/)) and wshrpc via WebSocket (found at [`/pkg/wshrpc`](./pkg/wshrpc/)).
### wsh
wsh can be found at [`/cmd/wsh`](./cmd/wsh/). It serves two purposes: it functions as a CLI tool for controlling Wave from the command line and it functions as a server on remote machines to facilitate multiplexing terminal sessions over a single connection and streaming files between the remote host and the local host. This process does not hot-reload, you will need to manually kill the dev instance and rerun it to apply changes.
Communication between wavesrv and wsh is handled by wshrpc via either forwarded domain socket or WebSocket, depending on what the remote host supports.

View File

@ -186,7 +186,7 @@ APPENDIX: How to apply the Apache License to your work.
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2024 Command Line Inc.
Copyright 2025 Command Line Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

2
NOTICE
View File

@ -1 +1 @@
Copyright 2024, Command Line Inc.
Copyright 2025, Command Line Inc.

View File

@ -53,6 +53,12 @@ The WSH helper runs on the following platforms:
- Windows 10 or later (arm64, x64)
- Linux Kernel 2.6.32 or later (x64), Linux Kernel 3.1 or later (arm64)
## Roadmap
Wave is constantly improving! Our roadmap will be continuously updated with our goals for each release. You can find it [here](./ROADMAP.md).
Want to provide input to our future releases? Connect with us on [Discord](https://discord.gg/XfvZ334gwU) or open a [Feature Request](https://github.com/wavetermdev/waveterm/issues/new/choose)!
## Links
- Homepage &mdash; https://www.waveterm.dev

View File

@ -4,34 +4,47 @@ This roadmap outlines major upcoming features and improvements for Wave Terminal
Want input on the roadmap? Join the discussion on [Discord](https://discord.gg/XfvZ334gwU).
## v0.11
Targeting a release during the week of Jan 13th 2024, betas possible at the end of the prior week.
Legend: ✅ Done | 🔧 In Progress | 🔷 Planned | 🤞 Stretch Goal
- 🔷 File/Directory Preview improvements
- 🔷 Remote S3 bucket browsing (directory + files)
- 🔷 Drag & drop between preview blocks
- 🔷 Drag into a preview directory from the native file browser or desktop to copy a file
## v0.11.0
Targeting first betas at then end of the week of Jan 6th, 2025 (or early the following week). Release at the end of the week or early the following week.
- 🔧 File/Directory Preview improvements
- 🔧 Remote S3 bucket browsing (directory + files)
- ✅ EC-TIME timeout when transferring large files
- 🤞 log viewer
- 🤞 binary viewer
- 🔷 Wave Apps (Go SDK)
- 🔷 Fixes for reducing 2FA requests on connect
- 🔷 Frontend Only Widgets, React + Babel Transpiling in an iframe/webview
- 🔧 Fixes for reducing 2FA requests on connect
- ✅ WebLinks in the terminal working again
- 🔧 Search in Web Views
- 🔷 Search in the Terminal
- ✅ Search in Web Views
- ✅ Search in the Terminal
- 🔷 Custom init files for widgets and terminal blocks
- 🔷 Multi-Input between terminal blocks on the same tab
- Multi-Input between terminal blocks on the same tab
- ✅ Gemini AI support
- 🔷 Monaco Theming
- 🤞 Blockcontroller fixes for terminal escape sequences
- 🤞 Explore VSCode Extension Compatibility with standalone Monaco Editor (language servers)
- 🔷 Various Connection Bugs + Improvements
- 🔧 Various Connection Bugs + Improvements
- 🔧 More Connection Config Options
## Future Releases
## v0.11.1
Check back soon for longer-term roadmap items.
Likely to follow v0.11 by 1 week.
- 🔧 Reduce main-line 2FA requests to 1 per connection
- 🔷 Frontend Only Widgets, React + Babel Transpiling in an iframe/webview
- 🔷 Monaco Theming
- 🔷 Drag & drop between preview blocks
- 🔷 Drag into a preview directory from the native file browser or desktop to copy a file
- 🔷 Wave Apps (Go SDK)
- 🤞 Explore VSCode Extension Compatibility with standalone Monaco Editor (language servers)
## v0.12
- 🔷 Import/Export Tab Layouts and Widgets
- 🔷 log viewer
- 🔷 binary viewer
## Planned (Unscheduled)
- 🔷 Customizable Keybindings
- 🔷 Launch widgets with custom keybindings
- 🔷 Re-assign system keybindings
- 🔷 Command Palette
- 🔷 AI Context

View File

@ -150,6 +150,8 @@ tasks:
vars:
ARCHS:
sh: echo {{if eq "arm" ARCH}}arm64{{else}}{{ARCH}}{{end}}
GO_ENV_VARS:
sh: echo "{{if eq "amd64" ARCH}}CC=\"zig cc -target x86_64-windows-gnu\"{{else}}CC=\"zig cc -target aarch64-windows-gnu\"{{end}}"
build:server:linux:
desc: Build the wavesrv component for Linux platforms (only generates artifacts for the current architecture).
@ -162,7 +164,7 @@ tasks:
ARCHS:
sh: echo {{if eq "arm" ARCH}}arm64{{else}}{{ARCH}}{{end}}
GO_ENV_VARS:
sh: echo "{{if eq "amd64" ARCH}}CC=\"zig cc -target x86_64-linux-gnu.2.28\"{{end}}"
sh: echo "{{if eq "amd64" ARCH}}CC=\"zig cc -target x86_64-linux-gnu.2.28\"{{else}}CC=\"zig cc -target aarch64-linux-gnu.2.28\"{{end}}"
build:server:internal:
requires:
@ -321,6 +323,13 @@ tasks:
- task: dev:cleardata:linux
- task: dev:cleardata:macos
init:
desc: Initialize the project for development.
cmds:
- yarn
- go mod tidy
- cd docs && yarn
dev:cleardata:windows:
internal: true
platforms: [windows]

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package main

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package main
@ -31,7 +31,7 @@ func generateTypesFile(tsTypesMap map[reflect.Type]string) error {
return fmt.Errorf("error generating wsh server types: %w", err)
}
var buf bytes.Buffer
fmt.Fprintf(&buf, "// Copyright 2024, Command Line Inc.\n")
fmt.Fprintf(&buf, "// Copyright 2025, Command Line Inc.\n")
fmt.Fprintf(&buf, "// SPDX-License-Identifier: Apache-2.0\n\n")
fmt.Fprintf(&buf, "// generated by cmd/generate/main-generatets.go\n\n")
fmt.Fprintf(&buf, "declare global {\n\n")
@ -66,7 +66,7 @@ func generateServicesFile(tsTypesMap map[reflect.Type]string) error {
fileName := "frontend/app/store/services.ts"
var buf bytes.Buffer
fmt.Fprintf(os.Stderr, "generating services file to %s\n", fileName)
fmt.Fprintf(&buf, "// Copyright 2024, Command Line Inc.\n")
fmt.Fprintf(&buf, "// Copyright 2025, Command Line Inc.\n")
fmt.Fprintf(&buf, "// SPDX-License-Identifier: Apache-2.0\n\n")
fmt.Fprintf(&buf, "// generated by cmd/generate/main-generatets.go\n\n")
fmt.Fprintf(&buf, "import * as WOS from \"./wos\";\n\n")
@ -89,7 +89,7 @@ func generateWshClientApiFile(tsTypeMap map[reflect.Type]string) error {
var buf bytes.Buffer
declMap := wshrpc.GenerateWshCommandDeclMap()
fmt.Fprintf(os.Stderr, "generating wshclientapi file to %s\n", fileName)
fmt.Fprintf(&buf, "// Copyright 2024, Command Line Inc.\n")
fmt.Fprintf(&buf, "// Copyright 2025, Command Line Inc.\n")
fmt.Fprintf(&buf, "// SPDX-License-Identifier: Apache-2.0\n\n")
fmt.Fprintf(&buf, "// generated by cmd/generate/main-generatets.go\n\n")
fmt.Fprintf(&buf, "import { WshClient } from \"./wshclient\";\n\n")

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package main
@ -17,6 +17,7 @@ import (
"github.com/wavetermdev/waveterm/pkg/authkey"
"github.com/wavetermdev/waveterm/pkg/blockcontroller"
"github.com/wavetermdev/waveterm/pkg/blocklogger"
"github.com/wavetermdev/waveterm/pkg/filestore"
"github.com/wavetermdev/waveterm/pkg/panichandler"
"github.com/wavetermdev/waveterm/pkg/remote/conncontroller"
@ -34,7 +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/wslconn"
"github.com/wavetermdev/waveterm/pkg/wstore"
)
@ -144,7 +145,7 @@ func beforeSendActivityUpdate(ctx context.Context) {
activity.Blocks, _ = wstore.DBGetBlockViewCounts(ctx)
activity.NumWindows, _ = wstore.DBGetCount[*waveobj.Window](ctx)
activity.NumSSHConn = conncontroller.GetNumSSHHasConnected()
activity.NumWSLConn = wsl.GetNumWSLHasConnected()
activity.NumWSLConn = wslconn.GetNumWSLHasConnected()
activity.NumWSNamed, activity.NumWS, _ = wstore.DBGetWSCounts(ctx)
err := telemetry.UpdateActivity(ctx, activity)
if err != nil {
@ -297,6 +298,7 @@ func main() {
go stdinReadWatch()
go telemetryLoop()
configWatcher()
blocklogger.InitBlockLogger()
webListener, err := web.MakeTCPListener("web")
if err != nil {
log.Printf("error creating web listener: %v\n", err)

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package main

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package cmd

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package cmd
@ -142,8 +142,8 @@ func aiRun(cmd *cobra.Command, args []string) (rtnErr error) {
if message.Len() == 0 {
return fmt.Errorf("message is empty")
}
if message.Len() > 10*1024 {
return fmt.Errorf("current max message size is 10k")
if message.Len() > 50*1024 {
return fmt.Errorf("current max message size is 50k")
}
messageData := wshrpc.AiMessageData{

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package cmd
@ -30,7 +30,6 @@ var connStatusCmd = &cobra.Command{
var connReinstallCmd = &cobra.Command{
Use: "reinstall CONNECTION",
Short: "reinstall wsh on a connection",
Args: cobra.ExactArgs(1),
RunE: connReinstallRun,
PreRunE: preRunSetupRpcClient,
}
@ -87,18 +86,26 @@ func validateConnectionName(name string) error {
return nil
}
func connStatusRun(cmd *cobra.Command, args []string) error {
func getAllConnStatus() ([]wshrpc.ConnStatus, error) {
var allResp []wshrpc.ConnStatus
sshResp, err := wshclient.ConnStatusCommand(RpcClient, nil)
if err != nil {
return fmt.Errorf("getting ssh connection status: %w", err)
return nil, 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)
return nil, fmt.Errorf("getting wsl connection status: %w", err)
}
allResp = append(allResp, wslResp...)
return allResp, nil
}
func connStatusRun(cmd *cobra.Command, args []string) error {
allResp, err := getAllConnStatus()
if err != nil {
return err
}
if len(allResp) == 0 {
WriteStdout("no connections\n")
return nil
@ -116,11 +123,21 @@ func connStatusRun(cmd *cobra.Command, args []string) error {
}
func connReinstallRun(cmd *cobra.Command, args []string) error {
if len(args) != 1 {
if RpcContext.Conn == "" {
return fmt.Errorf("no connection specified")
}
args = []string{RpcContext.Conn}
}
connName := args[0]
if err := validateConnectionName(connName); err != nil {
return err
}
err := wshclient.ConnReinstallWshCommand(RpcClient, connName, &wshrpc.RpcOpts{Timeout: 60000})
data := wshrpc.ConnExtData{
ConnName: connName,
LogBlockId: RpcContext.BlockId,
}
err := wshclient.ConnReinstallWshCommand(RpcClient, data, &wshrpc.RpcOpts{Timeout: 60000})
if err != nil {
return fmt.Errorf("reinstalling connection: %w", err)
}
@ -142,21 +159,19 @@ func connDisconnectRun(cmd *cobra.Command, args []string) error {
}
func connDisconnectAllRun(cmd *cobra.Command, args []string) error {
resp, err := wshclient.ConnStatusCommand(RpcClient, nil)
allConns, err := getAllConnStatus()
if err != nil {
return fmt.Errorf("getting connection status: %w", err)
return err
}
if len(resp) == 0 {
return nil
}
for _, conn := range resp {
if conn.Status == "connected" {
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)
}
for _, conn := range allConns {
if conn.Status != "connected" {
continue
}
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
@ -167,7 +182,11 @@ func connConnectRun(cmd *cobra.Command, args []string) error {
if err := validateConnectionName(connName); err != nil {
return err
}
err := wshclient.ConnConnectCommand(RpcClient, wshrpc.ConnRequest{Host: connName}, &wshrpc.RpcOpts{Timeout: 60000})
data := wshrpc.ConnRequest{
Host: connName,
LogBlockId: RpcContext.BlockId,
}
err := wshclient.ConnConnectCommand(RpcClient, data, &wshrpc.RpcOpts{Timeout: 60000})
if err != nil {
return fmt.Errorf("connecting connection: %w", err)
}
@ -180,7 +199,11 @@ func connEnsureRun(cmd *cobra.Command, args []string) error {
if err := validateConnectionName(connName); err != nil {
return err
}
err := wshclient.ConnEnsureCommand(RpcClient, connName, &wshrpc.RpcOpts{Timeout: 60000})
data := wshrpc.ConnExtData{
ConnName: connName,
LogBlockId: RpcContext.BlockId,
}
err := wshclient.ConnEnsureCommand(RpcClient, data, &wshrpc.RpcOpts{Timeout: 60000})
if err != nil {
return fmt.Errorf("ensuring connection: %w", err)
}

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package cmd
@ -11,7 +11,9 @@ import (
"net"
"os"
"path/filepath"
"strings"
"sync/atomic"
"syscall"
"time"
"github.com/spf13/cobra"
@ -33,9 +35,11 @@ var serverCmd = &cobra.Command{
}
var connServerRouter bool
var singleServerRouter bool
func init() {
serverCmd.Flags().BoolVar(&connServerRouter, "router", false, "run in local router mode")
serverCmd.Flags().BoolVar(&singleServerRouter, "single", false, "run in local single mode")
rootCmd.AddCommand(serverCmd)
}
@ -121,14 +125,10 @@ func runListener(listener net.Listener, router *wshutil.WshRouter) {
}
}
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")
}
func setupConnServerRpcClientWithRouter(router *wshutil.WshRouter, jwtToken string) (*wshutil.WshRpc, error) {
rpcCtx, err := wshutil.ExtractUnverifiedRpcContext(jwtToken)
if err != nil {
return nil, fmt.Errorf("error extracting rpc context from %s: %v", wshutil.WaveJwtTokenVarName, err)
return nil, fmt.Errorf("error extracting rpc context from JWT token: %v", err)
}
authRtn, err := router.HandleProxyAuth(jwtToken)
if err != nil {
@ -143,7 +143,7 @@ func setupConnServerRpcClientWithRouter(router *wshutil.WshRouter) (*wshutil.Wsh
return connServerClient, nil
}
func serverRunRouter() error {
func serverRunRouter(jwtToken string) error {
router := wshutil.NewWshRouter()
termProxy := wshutil.MakeRpcProxy()
rawCh := make(chan []byte, wshutil.DefaultOutputChSize)
@ -176,7 +176,7 @@ func serverRunRouter() error {
if err != nil {
return fmt.Errorf("cannot create unix listener: %v", err)
}
client, err := setupConnServerRpcClientWithRouter(router)
client, err := setupConnServerRpcClientWithRouter(router, jwtToken)
if err != nil {
return fmt.Errorf("error setting up connserver rpc client: %v", err)
}
@ -186,8 +186,41 @@ func serverRunRouter() error {
select {}
}
func serverRunNormal() error {
err := setupRpcClient(&wshremote.ServerImpl{LogWriter: os.Stdout})
func checkForUpdate() error {
remoteInfo := wshutil.GetInfo()
needsRestartRaw, err := RpcClient.SendRpcRequest(wshrpc.Command_ConnUpdateWsh, remoteInfo, &wshrpc.RpcOpts{Timeout: 60000})
if err != nil {
return fmt.Errorf("could not update: %w", err)
}
needsRestart, ok := needsRestartRaw.(bool)
if !ok {
return fmt.Errorf("wrong return type from update")
}
if needsRestart {
// run the restart command here
// how to get the correct path?
return syscall.Exec("~/.waveterm/bin/wsh", []string{"wsh", "connserver", "--single"}, []string{})
}
return nil
}
func serverRunSingle(jwtToken string) error {
err := setupRpcClient(&wshremote.ServerImpl{LogWriter: os.Stdout}, jwtToken)
if err != nil {
return err
}
WriteStdout("running wsh connserver (%s)\n", RpcContext.Conn)
err = checkForUpdate()
if err != nil {
return err
}
go wshremote.RunSysInfoLoop(RpcClient, RpcContext.Conn)
select {} // run forever
}
func serverRunNormal(jwtToken string) error {
err := setupRpcClient(&wshremote.ServerImpl{LogWriter: os.Stdout}, jwtToken)
if err != nil {
return err
}
@ -196,10 +229,40 @@ func serverRunNormal() error {
select {} // run forever
}
func askForJwtToken() (string, error) {
// if it already exists in the environment, great, use it
jwtToken := os.Getenv(wavebase.WaveJwtTokenVarName)
if jwtToken != "" {
fmt.Printf("HAVE-JWT\n")
return jwtToken, nil
}
// otherwise, ask for it
fmt.Printf("%s\n", wavebase.NeedJwtConst)
// read a single line from stdin
var line string
_, err := fmt.Fscanln(os.Stdin, &line)
if err != nil {
return "", fmt.Errorf("failed to read JWT token from stdin: %w", err)
}
return strings.TrimSpace(line), nil
}
func serverRun(cmd *cobra.Command, args []string) error {
if connServerRouter {
return serverRunRouter()
installErr := wshutil.InstallRcFiles()
if installErr != nil {
log.Printf("error installing rc files: %v", installErr)
}
jwtToken, err := askForJwtToken()
if err != nil {
return err
}
if singleServerRouter {
return serverRunSingle(jwtToken)
} else if connServerRouter {
return serverRunRouter(jwtToken)
} else {
return serverRunNormal()
return serverRunNormal(jwtToken)
}
}

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package cmd

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package cmd

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package cmd

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package cmd

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package cmd

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package cmd

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package cmd

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package cmd

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package cmd

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package cmd

View File

@ -1,14 +1,11 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package cmd
import (
"path/filepath"
"github.com/spf13/cobra"
"github.com/wavetermdev/waveterm/pkg/util/shellutil"
"github.com/wavetermdev/waveterm/pkg/wavebase"
"github.com/wavetermdev/waveterm/pkg/wshutil"
)
func init() {
@ -20,12 +17,9 @@ var rcfilesCmd = &cobra.Command{
Hidden: true,
Short: "Generate the rc files needed for various shells",
Run: func(cmd *cobra.Command, args []string) {
home := wavebase.GetHomeDir()
waveDir := filepath.Join(home, wavebase.RemoteWaveHomeDirName)
winBinDir := filepath.Join(waveDir, wavebase.RemoteWshBinDirName)
err := shellutil.InitRcFiles(waveDir, winBinDir)
err := wshutil.InstallRcFiles()
if err != nil {
WriteStderr(err.Error())
WriteStderr("%s\n", err.Error())
return
}
},

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package cmd

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package cmd
@ -10,6 +10,7 @@ import (
"runtime/debug"
"github.com/spf13/cobra"
"github.com/wavetermdev/waveterm/pkg/util/shellutil"
"github.com/wavetermdev/waveterm/pkg/waveobj"
"github.com/wavetermdev/waveterm/pkg/wshrpc"
"github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient"
@ -81,7 +82,14 @@ func OutputHelpMessage(cmd *cobra.Command) {
}
func preRunSetupRpcClient(cmd *cobra.Command, args []string) error {
err := setupRpcClient(nil)
jwtToken := os.Getenv(wshutil.WaveJwtTokenVarName)
if jwtToken == "" {
wshutil.SetTermRawModeAndInstallShutdownHandlers(true)
UsingTermWshMode = true
RpcClient, WrappedStdin = wshutil.SetupTerminalRpcClient(nil)
return nil
}
err := setupRpcClient(nil, jwtToken)
if err != nil {
return err
}
@ -127,15 +135,28 @@ func resolveBlockArg() (*waveobj.ORef, error) {
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)
if jwtToken == "" {
wshutil.SetTermRawModeAndInstallShutdownHandlers(true)
UsingTermWshMode = true
RpcClient, WrappedStdin = wshutil.SetupTerminalRpcClient(serverImpl)
return nil
func setupRpcClientWithToken(swapTokenStr string) (wshrpc.CommandAuthenticateRtnData, error) {
var rtn wshrpc.CommandAuthenticateRtnData
token, err := shellutil.UnpackSwapToken(swapTokenStr)
if err != nil {
return rtn, fmt.Errorf("error unpacking token: %w", err)
}
if token.SockName == "" {
return rtn, fmt.Errorf("no sockname in token")
}
if token.RpcContext == nil {
return rtn, fmt.Errorf("no rpccontext in token")
}
RpcContext = *token.RpcContext
RpcClient, err = wshutil.SetupDomainSocketRpcClient(token.SockName, nil)
if err != nil {
return rtn, fmt.Errorf("error setting up domain socket rpc client: %w", err)
}
return wshclient.AuthenticateTokenCommand(RpcClient, wshrpc.CommandAuthenticateTokenData{Token: token.Token}, nil)
}
// returns the wrapped stdin and a new rpc client (that wraps the stdin input and stdout output)
func setupRpcClient(serverImpl wshutil.ServerImpl, jwtToken string) error {
rpcCtx, err := wshutil.ExtractUnverifiedRpcContext(jwtToken)
if err != nil {
return fmt.Errorf("error extracting rpc context from %s: %v", wshutil.WaveJwtTokenVarName, err)

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package cmd

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package cmd
@ -162,7 +162,7 @@ func setBgRun(cmd *cobra.Command, args []string) (rtnErr error) {
if err != nil {
return fmt.Errorf("error formatting metadata: %v", err)
}
WriteStdout(string(jsonBytes) + "\n")
WriteStdout("%s\n", string(jsonBytes))
return nil
}

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package cmd

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package cmd

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package cmd

View File

@ -1,6 +1,6 @@
//go:build !windows
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package cmd

View File

@ -1,6 +1,6 @@
//go:build windows
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package cmd

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package cmd
@ -39,12 +39,13 @@ func sshRun(cmd *cobra.Command, args []string) (rtnErr error) {
}
// first, make a connection independent of the block
connOpts := wshrpc.ConnRequest{
Host: sshArg,
Host: sshArg,
LogBlockId: blockId,
Keywords: wshrpc.ConnKeywords{
SshIdentityFile: identityFiles,
},
}
wshclient.ConnConnectCommand(RpcClient, connOpts, nil)
wshclient.ConnConnectCommand(RpcClient, connOpts, &wshrpc.RpcOpts{Timeout: 60000})
// now, with that made, it will be straightforward to connect
data := wshrpc.CommandSetMetaData{

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package cmd

View File

@ -0,0 +1,48 @@
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"github.com/wavetermdev/waveterm/pkg/util/shellutil"
)
var tokenCmd = &cobra.Command{
Use: "token [token] [shell-type]",
Short: "exchange token for shell initialization script",
RunE: tokenCmdRun,
Hidden: true,
}
func init() {
rootCmd.AddCommand(tokenCmd)
}
func tokenCmdRun(cmd *cobra.Command, args []string) (rtnErr error) {
defer func() {
sendActivity("token", rtnErr == nil)
}()
if len(args) != 2 {
OutputHelpMessage(cmd)
return fmt.Errorf("wsh token requires exactly 2 arguments, got %d", len(args))
}
tokenStr, shellType := args[0], args[1]
if tokenStr == "" || shellType == "" {
OutputHelpMessage(cmd)
return fmt.Errorf("wsh token requires non-empty arguments")
}
rtnData, err := setupRpcClientWithToken(tokenStr)
if err != nil {
return fmt.Errorf("error setting up rpc client: %w", err)
}
envScriptText, err := shellutil.EncodeEnvVarsForShell(shellType, rtnData.Env)
if err != nil {
return fmt.Errorf("error encoding env vars: %w", err)
}
WriteStdout("%s\n", envScriptText)
WriteStdout("%s\n", rtnData.InitScriptText)
return nil
}

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package cmd

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package cmd

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package cmd

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package cmd

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package cmd

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package cmd

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package main

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package db

View File

@ -1 +0,0 @@
nodeLinker: node-modules

View File

@ -47,6 +47,7 @@ wsh editconfig
| term:scrollback | int | size of terminal scrollback buffer, max is 10000 |
| term:theme | string | preset name of terminal theme to apply by default (default is "default-dark") |
| term:transparency | float64 | set the background transparency of terminal theme (default 0.5, 0 = not transparent, 1.0 = fully transparent) |
| term:allowbracketedpaste | bool | allow bracketed paste mode in terminal (default false) |
| editor:minimapenabled | bool | set to false to disable editor minimap |
| editor:stickyscrollenabled | bool | enables monaco editor's stickyScroll feature (pinning headers of current context, e.g. class names, method names, etc.), defaults to false |
| editor:wordwrap | bool | set to true to enable word wrapping in the editor (defaults to false) |

View File

@ -45,6 +45,7 @@ This `WidgetConfigType` is shared between all types of widgets. That is to say,
| "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. |
| "magnified" | (optional) A boolean indicating whether or not the widget should launch magnfied. It is false 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" />

View File

@ -10,10 +10,12 @@ 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 combines traditional terminal features with graphical capabilities like file previews, web browsing, and AI assistance. It runs on MacOS, Linux, and Windows ([Getting Started](./gettingstarted)).
Wave is an [open-source](https://github.com/wavetermdev/waveterm) terminal that combines traditional terminal features with graphical capabilities like file previews, web browsing, and AI assistance. It runs on MacOS, Linux, and Windows.
Modern development involves constantly switching between terminals and browsers - checking documentation, previewing files, monitoring systems, and using AI tools. Wave brings these graphical tools directly into the terminal, letting you control them from the command line. This means you can stay in your terminal workflow while still having access to the visual interfaces you need.
Check out [Getting Started](./gettingstarted) for installation instructions.
![Wave Screenshot](./img/wave-screenshot.webp)
<CardGroup>
@ -76,6 +78,12 @@ Other References:
</div>
## Roadmap
Wave is constantly improving! Our roadmap will be continuously updated with our goals for each release. You can find it [here](https://github.com/wavetermdev/waveterm/blob/main/ROADMAP.md).
Want to provide input to our future releases? Connect with us on [Discord](https://discord.gg/XfvZ334gwU) or open a [Feature Request](https://github.com/wavetermdev/waveterm/issues/new/choose)!
## Links
- **Homepage** https://waveterm.dev

View File

@ -1,89 +1,10 @@
---
sidebar_position: 3.5
sidebar_class_name: hidden
id: "layout"
title: "Tab Layout System"
---
import { PlatformProvider, PlatformSelectorButton } from "@site/src/components/platformcontext.tsx";
import { Kbd } from "@site/src/components/kbd.tsx";
import { Redirect } from "@docusaurus/router";
<PlatformProvider>
<PlatformSelectorButton/>
<Redirect to="/tabs#tab-layout-system" />
![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-reference#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>
<!-- The contents of this page has moved to the tabs.mdx file under the "Tab Layout System" section. -->

134
docs/docs/tabs.mdx Normal file
View File

@ -0,0 +1,134 @@
---
sidebar_position: 3.2
id: "tabs"
title: "Tabs"
---
import { PlatformProvider, PlatformSelectorButton } from "@site/src/components/platformcontext.tsx";
import { Kbd } from "@site/src/components/kbd.tsx";
<PlatformProvider>
Tabs are collections of [Widgets](./widgets) that can be arranged into tiled dashboards. You can create as many tabs as you want within a given workspace to help organize your workflows.
## Tab Bar
The tab bar is located at the top of the window and shows all tabs within a given workspace. You can click on a tab to switch to it. When switching tabs, any commands in the previous tab will continue running and any unsaved work will be persisted until you return to it. If you close the window or switch workspaces within the same window, that work will be lost.
<PlatformSelectorButton />
### Creating a new tab
You can create a new tab by clicking the <i className="fa-sharp fa-plus" title="plus"/> button to the right of the tabs in the tab bar, or by pressing <Kbd k="Cmd:t"/> on the keyboard. This will also focus you to the new tab.
### Closing a tab
You can close a tab by hovering over it and clicking the <i className="fa-sharp fa-xmark-large" title="x"/> button that appears, or by pressing <Kbd k="Cmd:Shift:w"/> on the keyboard. You can also close a tab by [closing all the blocks](#delete-a-block) within it.
Closing a block is a destructive action that will stop any running processes and discard any unsaved work. This cannot be undone.
### Rearranging tabs
You can rearrange tabs by dragging them around within the tab bar.
### Switching tabs
You can switch to an existing tab by clicking on it in the tab bar. You can also use the following shortcuts:
| Key | Function |
| ------------------ | -------------------- |
| <Kbd k="Cmd:1-9"/> | Switch to tab number |
| <Kbd k="Cmd:["/> | Switch tab left |
| <Kbd k="Cmd:]"/> | Switch tab right |
### Pinning a tab
Pinning a tab makes it harder to close accidentally. You can pin a tab by right-clicking on it and selecting "Pin Tab" from the context menu that appears. You can also pin a tab by dragging it to a lesser index than an existing pinned tab. When a tab is pinned, the <i className="fa-sharp fa-xmark-large" title="x"/> button for the tab will be replaced with a <i className="fa-sharp fa-thumbtack" title="pin"/> button. Clicking this button will unpin the tab. You can also unpin a tab by dragging it to an index higher than an existing unpinned tab.
## Tab Layout System
The tabs are comprised of tiled blocks. The contents of each block is a single widget. You can move blocks around and arrange them into layouts that best-suit your workflow. You can also magnify blocks to focus on a specific widget.
![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
<PlatformSelectorButton />
#### 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" title="x"/> 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-reference#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" title="left/right arrows"/> or <i className="fa-sharp fa-arrows-up-down" title="up/down arrows"/> 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.
### Magnify a block
You can magnify a block by clicking the <i className="custom-icon-inline custom-icon-magnify-disabled" title="magnify"/> button or by pressing <Kbd k="Cmd:m"/> on the keyboard. You can then un-magnify a block by clicking the <i className="custom-icon-inline custom-icon-magnify-enabled" title="un-magnify"/> button or by pressing <Kbd k="Cmd:m"/> again.
### 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>

View File

@ -59,6 +59,8 @@ const config: Config = {
},
},
],
"docusaurus-plugin-sass",
"@docusaurus/plugin-svgr",
].filter((v) => v),
themes: [
["classic", { customCss: "src/css/custom.css" }],

View File

@ -15,34 +15,37 @@
"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",
"@docusaurus/core": "^3.7.0",
"@docusaurus/plugin-content-docs": "^3.7.0",
"@docusaurus/plugin-debug": "^3.7.0",
"@docusaurus/plugin-ideal-image": "^3.7.0",
"@docusaurus/plugin-sitemap": "^3.7.0",
"@docusaurus/plugin-svgr": "^3.7.0",
"@docusaurus/theme-classic": "^3.7.0",
"@docusaurus/theme-search-algolia": "^3.7.0",
"@mdx-js/react": "^3.0.0",
"@waveterm/docusaurus-og": "https://github.com/wavetermdev/docusaurus-og",
"@waveterm/docusaurus-og": "https://github.com/wavetermdev/docusaurus-og.git#commit=243b4a7feffde0cbb7c0bf29fe7a2f74ee655c0c",
"clsx": "^2.1.1",
"docusaurus-plugin-sass": "^0.2.6",
"prism-react-renderer": "^2.3.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"remark-gfm": "^4.0.0",
"remark-typescript-code-import": "^1.0.1",
"sass": "^1.83.4",
"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.16.0",
"@docusaurus/module-type-aliases": "3.7.0",
"@docusaurus/tsconfig": "3.7.0",
"@docusaurus/types": "3.7.0",
"@eslint/js": "^9.18.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.16.0",
"eslint-config-prettier": "^9.1.0",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-mdx": "^3.1.5",
"prettier": "^3.4.2",
"prettier-plugin-jsdoc": "^1.3.0",
@ -52,8 +55,8 @@
"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.18.2"
"typescript": "^5.7.3",
"typescript-eslint": "^8.21.0"
},
"resolutions": {
"path-to-regexp@npm:2.2.1": "^3",
@ -75,6 +78,5 @@
},
"engines": {
"node": ">=18.0"
},
"packageManager": "yarn@4.4.1"
}
}

View File

@ -81,6 +81,20 @@ body .markdown h2 {
-webkit-mask: url(/img/workspace.svg) no-repeat center / contain;
}
.custom-icon-magnify-enabled:before {
content: "";
mask: url(/img/magnify-enabled.svg) no-repeat center / contain;
-webkit-mask: url(/img/magnify-enabled.svg) no-repeat center / contain;
margin-bottom: -2px;
}
.custom-icon-magnify-disabled:before {
content: "";
mask: url(/img/magnify-disabled.svg) no-repeat center / contain;
-webkit-mask: url(/img/magnify-disabled.svg) no-repeat center / contain;
margin-bottom: -2px;
}
img[src*="#left"] {
float: left;
margin: 0 10px 10px 0;
@ -95,3 +109,7 @@ img[src*="#center"] {
display: block;
margin: auto;
}
.hidden {
display: none;
}

24
docs/static/img/magnify-disabled.svg vendored Normal file
View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
id="svg2"
version="1.1"
fill="none"
viewBox="0 0 16 16"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g2"
transform="matrix(1 0 0 1 -4 -4)"
fill="#000">
<path
id="arrow1"
class="arrow"
d="m 6.65255,18.09625 c -0.4262,0 -0.7723,-0.34592 -0.7723,-0.77195 v -5.4527 c 0,-0.42604 0.3461,-0.77195 0.7723,-0.77195 0.42619,0 0.77227,0.34592 0.77227,0.77195 v 4.6825 l 4.6832,0.0017 c 0.4262,0 0.77228,0.34592 0.77228,0.77185 0,0.42604 -0.34608,0.77195 -0.77228,0.77195 h -5.4554 v -0.0034 z" />
<path
id="arrow2"
class="arrow"
d="m 17.32733,5.90413 c 0.42624,0 0.77233,0.34581 0.77233,0.77184 v 5.4527 c 0,0.426 -0.34609,0.77191 -0.77233,0.77191 -0.42614,0 -0.77223,-0.34591 -0.77223,-0.77191 v -4.6826 l -4.6832,-0.0017 c -0.42625,0 -0.77223,-0.34591 -0.77223,-0.77187 0,-0.42603 0.34599,-0.77195 0.77223,-0.77195 h 5.4554 v 0.0035 z" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

9
docs/static/img/magnify-enabled.svg vendored Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="svg2" version="1.1" fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<g id="g2" transform="matrix(1 0 0 1 -4 -4)" fill="#000">
<path id="arrow1" class="arrow"
d="m10.208 13.003c0.4262 0 0.7723 0.34592 0.7723 0.77195v5.4527c0 0.42604-0.3461 0.77195-0.7723 0.77195-0.42619 0-0.77227-0.34592-0.77227-0.77195v-4.6825l-4.6832-0.0017c-0.4262 0-0.77228-0.34592-0.77228-0.77185 0-0.42604 0.34608-0.77195 0.77228-0.77195h5.4554v0.0034z" />
<path id="arrow2" class="arrow"
d="m13.772 10.997c-0.42624 0-0.77233-0.34581-0.77233-0.77184v-5.4527c0-0.426 0.34609-0.77191 0.77233-0.77191 0.42614 0 0.77223 0.34591 0.77223 0.77191v4.6826l4.6832 0.0017c0.42625 0 0.77223 0.34591 0.77223 0.77187 0 0.42603-0.34599 0.77195-0.77223 0.77195h-5.4554v-0.0035z" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 831 B

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import react from "@vitejs/plugin-react-swc";

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { ipcMain } from "electron";

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
// for activity updates

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { FileService } from "@/app/store/services";

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import * as electron from "electron";

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import * as electron from "electron";
@ -14,6 +14,7 @@ import {
getWaveDataDir,
getWaveSrvCwd,
getWaveSrvPath,
getXdgCurrentDesktop,
WaveConfigHomeVarName,
WaveDataHomeVarName,
} from "./platform";
@ -53,6 +54,10 @@ export function runWaveSrv(handleWSEvent: (evtMsg: WSEventType) => void): Promis
pReject = argReject;
});
const envCopy = { ...process.env };
const xdgCurrentDesktop = getXdgCurrentDesktop();
if (xdgCurrentDesktop != null) {
envCopy["XDG_CURRENT_DESKTOP"] = xdgCurrentDesktop;
}
envCopy[WaveAppPathVarName] = getElectronAppUnpackedBasePath();
envCopy[WaveAuthKeyEnv] = AuthKey;
envCopy[WaveDataHomeVarName] = getWaveDataDir();

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { ipcMain, webContents, WebContents } from "electron";

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { ClientService, FileService, ObjectService, WindowService, WorkspaceService } from "@/app/store/services";

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { FileService, WindowService } from "@/app/store/services";

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { RpcApi } from "@/app/store/wshclientapi";
@ -47,6 +47,7 @@ import { getLaunchSettings } from "./launchsettings";
import { log } from "./log";
import { makeAppMenu } from "./menu";
import {
callWithOriginalXdgCurrentDesktopAsync,
checkIfRunningUnderARM64Translation,
getElectronAppBasePath,
getElectronAppUnpackedBasePath,
@ -121,9 +122,13 @@ function handleWSEvent(evtMsg: WSEventType) {
// Listen for the open-external event from the renderer process
electron.ipcMain.on("open-external", (event, url) => {
if (url && typeof url === "string") {
electron.shell.openExternal(url).catch((err) => {
console.error(`Failed to open URL ${url}:`, err);
});
fireAndForget(() =>
callWithOriginalXdgCurrentDesktopAsync(() =>
electron.shell.openExternal(url).catch((err) => {
console.error(`Failed to open URL ${url}:`, err);
})
)
);
} else {
console.error("Invalid URL received in open-external event:", url);
}
@ -347,9 +352,11 @@ electron.ipcMain.on("quicklook", (event, filePath: string) => {
electron.ipcMain.on("open-native-path", (event, filePath: string) => {
console.log("open-native-path", filePath);
fireAndForget(() =>
electron.shell.openPath(filePath).then((excuse) => {
if (excuse) console.error(`Failed to open ${filePath} in native application: ${excuse}`);
})
callWithOriginalXdgCurrentDesktopAsync(() =>
electron.shell.openPath(filePath).then((excuse) => {
if (excuse) console.error(`Failed to open ${filePath} in native application: ${excuse}`);
})
)
);
});

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { waveEventSubscribe } from "@/app/store/wps";

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { fireAndForget } from "@/util/util";
@ -194,13 +194,76 @@ ipcMain.on("get-config-dir", (event) => {
event.returnValue = getWaveConfigDir();
});
/**
* Gets the value of the XDG_CURRENT_DESKTOP environment variable. If ORIGINAL_XDG_CURRENT_DESKTOP is set, it will be returned instead.
* This corrects for a strange behavior in Electron, where it sets its own value for XDG_CURRENT_DESKTOP to improve Chromium compatibility.
* @see https://www.electronjs.org/docs/latest/api/environment-variables#original_xdg_current_desktop
* @returns The value of the XDG_CURRENT_DESKTOP environment variable, or ORIGINAL_XDG_CURRENT_DESKTOP if set, or undefined if neither are set.
*/
function getXdgCurrentDesktop(): string {
if (process.env.ORIGINAL_XDG_CURRENT_DESKTOP) {
return process.env.ORIGINAL_XDG_CURRENT_DESKTOP;
} else if (process.env.XDG_CURRENT_DESKTOP) {
return process.env.XDG_CURRENT_DESKTOP;
} else {
return undefined;
}
}
/**
* Calls the given callback with the value of the XDG_CURRENT_DESKTOP environment variable set to ORIGINAL_XDG_CURRENT_DESKTOP if it is set.
* @see https://www.electronjs.org/docs/latest/api/environment-variables#original_xdg_current_desktop
* @param callback The callback to call.
*/
function callWithOriginalXdgCurrentDesktop(callback: () => void) {
const currXdgCurrentDesktopDefined = "XDG_CURRENT_DESKTOP" in process.env;
const currXdgCurrentDesktop = process.env.XDG_CURRENT_DESKTOP;
const originalXdgCurrentDesktop = getXdgCurrentDesktop();
if (originalXdgCurrentDesktop) {
process.env.XDG_CURRENT_DESKTOP = originalXdgCurrentDesktop;
}
callback();
if (originalXdgCurrentDesktop) {
if (currXdgCurrentDesktopDefined) {
process.env.XDG_CURRENT_DESKTOP = currXdgCurrentDesktop;
} else {
delete process.env.XDG_CURRENT_DESKTOP;
}
}
}
/**
* Calls the given async callback with the value of the XDG_CURRENT_DESKTOP environment variable set to ORIGINAL_XDG_CURRENT_DESKTOP if it is set.
* @see https://www.electronjs.org/docs/latest/api/environment-variables#original_xdg_current_desktop
* @param callback The async callback to call.
*/
async function callWithOriginalXdgCurrentDesktopAsync(callback: () => Promise<void>) {
const currXdgCurrentDesktopDefined = "XDG_CURRENT_DESKTOP" in process.env;
const currXdgCurrentDesktop = process.env.XDG_CURRENT_DESKTOP;
const originalXdgCurrentDesktop = getXdgCurrentDesktop();
if (originalXdgCurrentDesktop) {
process.env.XDG_CURRENT_DESKTOP = originalXdgCurrentDesktop;
}
await callback();
if (originalXdgCurrentDesktop) {
if (currXdgCurrentDesktopDefined) {
process.env.XDG_CURRENT_DESKTOP = currXdgCurrentDesktop;
} else {
delete process.env.XDG_CURRENT_DESKTOP;
}
}
}
export {
callWithOriginalXdgCurrentDesktop,
callWithOriginalXdgCurrentDesktopAsync,
getElectronAppBasePath,
getElectronAppUnpackedBasePath,
getWaveConfigDir,
getWaveDataDir,
getWaveSrvCwd,
getWaveSrvPath,
getXdgCurrentDesktop,
isDev,
isDevVite,
unameArch,

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
const { ipcRenderer } = require("electron");

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { contextBridge, ipcRenderer, WebviewTag } from "electron";

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { dialog, ipcMain, Notification } from "electron";

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { getWebServerEndpoint } from "@/util/endpoints";

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { Workspace } from "@/app/workspace/workspace";

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
id="svg2"
version="1.1"
fill="none"
viewBox="0 0 16 16"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="g2"
transform="matrix(1 0 0 1 -4 -4)"
fill="#000">
<path
id="arrow1"
class="arrow"
d="m 6.65255,18.09625 c -0.4262,0 -0.7723,-0.34592 -0.7723,-0.77195 v -5.4527 c 0,-0.42604 0.3461,-0.77195 0.7723,-0.77195 0.42619,0 0.77227,0.34592 0.77227,0.77195 v 4.6825 l 4.6832,0.0017 c 0.4262,0 0.77228,0.34592 0.77228,0.77185 0,0.42604 -0.34608,0.77195 -0.77228,0.77195 h -5.4554 v -0.0034 z" />
<path
id="arrow2"
class="arrow"
d="m 17.32733,5.90413 c 0.42624,0 0.77233,0.34581 0.77233,0.77184 v 5.4527 c 0,0.426 -0.34609,0.77191 -0.77233,0.77191 -0.42614,0 -0.77223,-0.34591 -0.77223,-0.77191 v -4.6826 l -4.6832,-0.0017 c -0.42625,0 -0.77223,-0.34591 -0.77223,-0.77187 0,-0.42603 0.34599,-0.77195 0.77223,-0.77195 h 5.4554 v 0.0035 z" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -309,7 +309,6 @@
width: 100%;
font: var(--base-font);
color: var(--secondary-text-color);
gap: 12px;
.connstatus-status-icon-wrapper {
display: flex;
@ -338,7 +337,6 @@
width: 100%;
.connstatus-status-text {
@include mixins.ellipsis();
max-width: 100%;
font-size: 11px;
font-style: normal;
@ -349,13 +347,36 @@
}
.connstatus-error {
@include mixins.ellipsis();
width: 94%;
font-size: 11px;
font-style: normal;
font-weight: 400;
line-height: 15px;
letter-spacing: 0.11px;
text-wrap: wrap;
max-height: 80px;
border-radius: 8px;
padding: 5px;
padding-left: 0;
position: relative;
.copy-button {
visibility: hidden;
display: flex;
position: sticky;
top: 0;
right: 4px;
float: right;
border-radius: 4px;
backdrop-filter: blur(8px);
padding: 0.286em;
align-items: center;
justify-content: flex-end;
gap: 0.286em;
}
&:hover .copy-button {
visibility: visible;
}
}
}
}

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import {
@ -9,7 +9,6 @@ import {
FullSubBlockProps,
SubBlockProps,
} from "@/app/block/blocktypes";
import { PlotView } from "@/app/view/plotview/plotview";
import { PreviewModel, PreviewView, makePreviewModel } from "@/app/view/preview/preview";
import { SysinfoView, SysinfoViewModel, makeSysinfoViewModel } from "@/app/view/sysinfo/sysinfo";
import { VDomView, makeVDomModel } from "@/app/view/vdom/vdom";
@ -88,9 +87,6 @@ function getViewElem(
/>
);
}
if (blockView === "plot") {
return <PlotView key={blockId} />;
}
if (blockView === "web") {
return <WebView key={blockId} blockId={blockId} model={viewModel as WebViewModel} />;
}

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import {
@ -23,10 +23,10 @@ import {
getSettingsKeyAtom,
getUserName,
globalStore,
refocusNode,
useBlockAtom,
WOS,
} from "@/app/store/global";
import { globalRefocusWithTimeout } from "@/app/store/keymodel";
import { RpcApi } from "@/app/store/wshclientapi";
import { TabRpcClient } from "@/app/store/wshrpcutil";
import { ErrorBoundary } from "@/element/errorboundary";
@ -38,8 +38,10 @@ import * as keyutil from "@/util/keyutil";
import * as util from "@/util/util";
import clsx from "clsx";
import * as jotai from "jotai";
import { OverlayScrollbarsComponent } from "overlayscrollbars-react";
import * as React from "react";
import { JSX } from "react";
import { CopyButton } from "../element/copybutton";
import { BlockFrameProps } from "./blocktypes";
const NumActiveConnColors = 8;
@ -293,7 +295,7 @@ const HeaderTextElem = React.memo(({ elem, preview }: { elem: HeaderElem; previe
);
} else if (elem.elemtype == "textbutton") {
return (
<Button className={elem.className} onClick={(e) => elem.onClick(e)}>
<Button className={elem.className} onClick={(e) => elem.onClick(e)} title={elem.title}>
{elem.text}
</Button>
);
@ -357,7 +359,11 @@ const ConnStatusOverlay = React.memo(
}, [width, connStatus, setShowError]);
const handleTryReconnect = React.useCallback(() => {
const prtn = RpcApi.ConnConnectCommand(TabRpcClient, { host: connName }, { timeout: 60000 });
const prtn = RpcApi.ConnConnectCommand(
TabRpcClient,
{ host: connName, logblockid: nodeModel.blockId },
{ timeout: 60000 }
);
prtn.catch((e) => console.log("error reconnecting", connName, e));
}, [connName]);
@ -417,6 +423,21 @@ const ConnStatusOverlay = React.memo(
setShowWshError(showWshErrorTemp);
}, [connStatus, wshConfigEnabled]);
const handleCopy = React.useCallback(
async (e: React.MouseEvent) => {
const errTexts = [];
if (showError) {
errTexts.push(`error: ${connStatus.error}`);
}
if (showWshError) {
errTexts.push(`unable to use wsh: ${connStatus.wsherror}`);
}
const textToCopy = errTexts.join("\n");
await navigator.clipboard.writeText(textToCopy);
},
[showError, showWshError, connStatus.error, connStatus.wsherror]
);
if (!showWshError && (isLayoutMode || connStatus.status == "connected" || connModalOpen)) {
return null;
}
@ -428,10 +449,16 @@ const ConnStatusOverlay = React.memo(
{showIcon && <i className="fa-solid fa-triangle-exclamation"></i>}
<div className="connstatus-status">
<div className="connstatus-status-text">{statusText}</div>
{showError ? <div className="connstatus-error">error: {connStatus.error}</div> : null}
{showWshError ? (
<div className="connstatus-error">unable to use wsh: {connStatus.wsherror}</div>
) : null}
{(showError || showWshError) && (
<OverlayScrollbarsComponent
className="connstatus-error"
options={{ scrollbars: { autoHide: "leave" } }}
>
<CopyButton className="copy-button" onClick={handleCopy} title="Copy" />
{showError ? <div>error: {connStatus.error}</div> : null}
{showWshError ? <div>unable to use wsh: {connStatus.wsherror}</div> : null}
</OverlayScrollbarsComponent>
)}
{showWshError && (
<Button className={reconClassName} onClick={handleDisableWsh}>
always disable wsh
@ -542,7 +569,11 @@ const BlockFrame_Default_Component = (props: BlockFrameProps) => {
const connName = blockData?.meta?.connection;
if (!util.isBlank(connName)) {
console.log("ensure conn", nodeModel.blockId, connName);
RpcApi.ConnEnsureCommand(TabRpcClient, connName, { timeout: 60000 }).catch((e) => {
RpcApi.ConnEnsureCommand(
TabRpcClient,
{ connname: connName, logblockid: nodeModel.blockId },
{ timeout: 60000 }
).catch((e) => {
console.log("error ensuring connection", nodeModel.blockId, connName, e);
});
}
@ -692,7 +723,11 @@ const ChangeConnectionBlockModal = React.memo(
meta: { connection: connName, file: newCwd },
});
try {
await RpcApi.ConnEnsureCommand(TabRpcClient, connName, { timeout: 60000 });
await RpcApi.ConnEnsureCommand(
TabRpcClient,
{ connname: connName, logblockid: blockId },
{ timeout: 60000 }
);
} catch (e) {
console.log("error connecting", blockId, connName, e);
}
@ -757,7 +792,7 @@ const ChangeConnectionBlockModal = React.memo(
onSelect: async (_: string) => {
const prtn = RpcApi.ConnConnectCommand(
TabRpcClient,
{ host: connStatus.connection },
{ host: connStatus.connection, logblockid: blockId },
{ timeout: 60000 }
);
prtn.catch((e) => console.log("error reconnecting", connStatus.connection, e));
@ -880,12 +915,13 @@ const ChangeConnectionBlockModal = React.memo(
} else {
changeConnection(rowItem.value);
globalStore.set(changeConnModalAtom, false);
globalRefocusWithTimeout(10);
}
}
if (keyutil.checkKeyPressed(waveEvent, "Escape")) {
globalStore.set(changeConnModalAtom, false);
setConnSelected("");
refocusNode(blockId);
globalRefocusWithTimeout(10);
return true;
}
if (keyutil.checkKeyPressed(waveEvent, "ArrowUp")) {
@ -917,6 +953,7 @@ const ChangeConnectionBlockModal = React.memo(
onSelect={(selected: string) => {
changeConnection(selected);
globalStore.set(changeConnModalAtom, false);
globalRefocusWithTimeout(10);
}}
selectIndex={rowIndex}
autoFocus={isNodeFocused}

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { NodeModel } from "@/layout/index";

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { NumActiveConnColors } from "@/app/block/blockframe";

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import type { Meta, StoryObj } from "@storybook/react";

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { memo } from "react";

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import type { Meta, StoryObj } from "@storybook/react";

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import clsx from "clsx";

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { Meta, StoryObj } from "@storybook/react";

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import clsx from "clsx";

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { clsx } from "clsx";

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import type { Meta, StoryObj } from "@storybook/react";

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { type Placement } from "@floating-ui/react";

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import React, { ReactNode } from "react";

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { Meta, StoryObj } from "@storybook/react";

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line
// Copyright 2025, Command Line
// SPDX-License-Identifier: Apache-2.0
import { clsx } from "clsx";

View File

@ -1,4 +1,4 @@
// Copyright 2024, Command Line Inc.
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { FloatingPortal, type Placement, useDismiss, useFloating, useInteractions } from "@floating-ui/react";

Some files were not shown because too many files have changed in this diff Show More