Merge branch 'main' of github.com:wavetermdev/waveterm into red/chatview

This commit is contained in:
Red Adaya 2024-10-05 21:59:39 +08:00
commit f04774391c
49 changed files with 1319 additions and 862 deletions

2
.gitattributes vendored
View File

@ -1 +1 @@
* text=lf
* text=auto

View File

@ -1,31 +0,0 @@
---
name: Bug Report
about: Create a bug report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. MacOS/Linux, x64 or arm64]
- Version [e.g. v0.5.0]
**Additional context**
Add any other context about the problem here.

87
.github/ISSUE_TEMPLATE/bug-report.yml vendored Normal file
View File

@ -0,0 +1,87 @@
name: Bug Report
description: Create a bug report to help us improve.
title: "[Bug]: "
labels: ["bug", "triage"]
body:
- type: markdown
attributes:
value: |
## Bug description
- type: textarea
attributes:
label: Current Behavior
description: A concise description of what you're experiencing.
validations:
required: true
- type: textarea
attributes:
label: Expected Behavior
description: A concise description of what you expected to happen.
validations:
required: true
- type: textarea
attributes:
label: Steps To Reproduce
description: Steps to reproduce the behavior.
placeholder: |
1. In this environment...
2. With this config...
3. Run '...'
4. See error...
validations:
required: true
- type: markdown
attributes:
value: |
## Environment details
We require that you provide us the version of Wave you're running so we can track issues across versions. To find the Wave version, go to the app menu (this always visible on macOS, for Windows and Linux, click the `...` button) and navigate to `Wave -> About Wave Terminal`. This will bring up the About modal. Copy the client version and paste it below.
- type: input
attributes:
label: Wave Version
description: The version of Wave you are running
placeholder: v0.8.8
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
validations:
required: false
- type: dropdown
attributes:
label: Architecture
description: The architecture of the computer where you are running Wave
options:
- arm64
- x64
validations:
required: false
- type: markdown
attributes:
value: |
## Extra details
- type: textarea
attributes:
label: Anything else?
description: |
Links? References? Anything that will give us more context about the issue you are encountering!
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
validations:
required: false
- type: checkboxes
attributes:
label: Questionnaire
description: "If you feel up to the challenge, please check one of the boxes below:"
options:
- label: I'm interested in fixing this myself but don't know where to start
required: false
- label: I would like to fix and I have a solution
required: false
- label: I don't have time to fix this right now, but maybe later
required: false

View File

@ -1,14 +0,0 @@
---
name: Feature Request
about: Suggest a new idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.

View File

@ -0,0 +1,26 @@
name: Feature Request
description: Suggest a new idea for this project.
title: "[Feature]: "
labels: ["enhancement", "triage"]
body:
- type: textarea
attributes:
label: Feature description
description: Describe the issue in detail and why we should add it. To help us out, please poke through our issue tracker and make sure it's not a duplicate issue. Ex. As a user, I can do [...]
validations:
required: true
- type: textarea
attributes:
label: Implementation Suggestion
description: If you have any suggestions on how to design this feature, list them here.
validations:
required: false
- type: textarea
attributes:
label: Anything else?
description: |
Links? References? Anything that will give us more context about how to deliver your feature!
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
validations:
required: false

View File

@ -11,6 +11,7 @@ on:
env:
GO_VERSION: "1.22"
NODE_VERSION: "20"
STATIC_DOCSITE_PATH: docsite
jobs:
runbuild:
permissions:
@ -47,7 +48,7 @@ jobs:
# 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)
if: matrix.platform == 'darwin'
run: brew update && brew install awscli
run: brew install awscli
# The version of FPM that comes bundled with electron-builder doesn't include a Linux ARM target. Installing Gems onto the runner is super quick so we'll just do this for all targets.
- name: Install FPM (not Windows)
@ -110,6 +111,15 @@ 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 (not Windows)
if: matrix.platform != 'windows'
@ -121,6 +131,7 @@ jobs:
APPLE_ID: ${{ matrix.platform == 'darwin' && secrets.PROD_MACOS_NOTARIZATION_APPLE_ID_2 }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ matrix.platform == 'darwin' && secrets.PROD_MACOS_NOTARIZATION_PWD_2 }}
APPLE_TEAM_ID: ${{ matrix.platform == 'darwin' && secrets.PROD_MACOS_NOTARIZATION_TEAM_ID_2 }}
STATIC_DOCSITE_PATH: ${{env.STATIC_DOCSITE_PATH}}
- name: Build (Windows only)
if: matrix.platform == 'windows'
run: task package
@ -128,6 +139,7 @@ jobs:
USE_SYSTEM_FPM: true # Ensure that the installed version of FPM is used rather than the bundled one.
CSC_LINK: ${{ steps.variables.outputs.SM_CLIENT_CERT_FILE }}
CSC_KEY_PASSWORD: ${{ secrets.SM_CLIENT_CERT_PASSWORD }}
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
- name: Upload to S3 staging
run: task artifacts:upload

View File

@ -202,7 +202,7 @@ func main() {
return
}
defer func() {
err = waveLock.Unlock()
err = waveLock.Close()
if err != nil {
log.Printf("error releasing wave lock: %v\n", err)
}

View File

@ -0,0 +1,54 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
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 createBlockMagnified bool
var createBlockCmd = &cobra.Command{
Use: "createblock viewname key=value ...",
Short: "create a new block",
Args: cobra.MinimumNArgs(1),
RunE: createBlockRun,
PreRunE: preRunSetupRpcClient,
Hidden: true,
}
func init() {
createBlockCmd.Flags().BoolVarP(&createBlockMagnified, "magnified", "m", false, "create block in magnified mode")
rootCmd.AddCommand(createBlockCmd)
}
func createBlockRun(cmd *cobra.Command, args []string) error {
viewName := args[0]
var metaSetStrs []string
if len(args) > 1 {
metaSetStrs = args[1:]
}
meta, err := parseMetaSets(metaSetStrs)
if err != nil {
return err
}
meta["view"] = viewName
data := wshrpc.CommandCreateBlockData{
BlockDef: &waveobj.BlockDef{
Meta: meta,
},
Magnified: createBlockMagnified,
}
oref, err := wshclient.CreateBlockCommand(RpcClient, data, nil)
if err != nil {
return fmt.Errorf("create block failed: %v", err)
}
fmt.Printf("created block %s\n", oref.OID)
return nil
}

View File

@ -34,6 +34,7 @@ const config = {
},
asarUnpack: [
"dist/bin/**/*", // wavesrv and wsh binaries
"dist/docsite/**/*", // the static docsite
],
mac: {
target: [
@ -66,6 +67,7 @@ const config = {
Keywords: "developer;terminal;emulator;",
category: "Development;Utility;",
},
executableArgs: ["--enable-features", "UseOzonePlatform", "--ozone-platform-hint", "auto"], // Hint Electron to use Ozone abstraction layer for native Wayland support
},
deb: {
afterInstall: "build/deb-postinstall.tpl",
@ -85,6 +87,14 @@ const config = {
provider: "generic",
url: "https://dl.waveterm.dev/releases-w2",
},
beforePack: () => {
const staticSourcePath = process.env.STATIC_DOCSITE_PATH;
const staticDestPath = "dist/docsite";
if (staticSourcePath) {
console.log(`Static docsite path is specified, copying from "${staticSourcePath}" to "${staticDestPath}"`);
fs.cpSync(staticSourcePath, staticDestPath, { recursive: true });
}
},
afterPack: (context) => {
// This is a workaround to restore file permissions to the wavesrv binaries on macOS after packaging the universal binary.
if (context.electronPlatformName === "darwin" && context.arch === Arch.universal) {

View File

@ -38,6 +38,7 @@ export default defineConfig({
rollupOptions: {
input: {
index: "emain/preload.ts",
"preload-webview": "emain/preload-webview.ts",
},
output: {
format: "cjs",

27
emain/docsite.ts Normal file
View File

@ -0,0 +1,27 @@
import { getWebServerEndpoint } from "@/util/endpoints";
import { fetch } from "@/util/fetchutil";
import { ipcMain } from "electron";
const docsiteWebUrl = "https://docs.waveterm.dev/";
let docsiteUrl: string;
ipcMain.on("get-docsite-url", (event) => {
event.returnValue = docsiteUrl;
});
export async function initDocsite() {
const docsiteEmbeddedUrl = getWebServerEndpoint() + "/docsite/";
try {
const response = await fetch(docsiteEmbeddedUrl);
if (response.ok) {
console.log("Embedded docsite is running, using embedded version for help view");
docsiteUrl = docsiteEmbeddedUrl;
} else {
console.log("Embedded docsite is not running, using web version for help view", response);
docsiteUrl = docsiteWebUrl;
}
} catch (error) {
console.log("Failed to fetch docsite url, using web version for help view", error);
docsiteUrl = docsiteWebUrl;
}
}

View File

@ -9,6 +9,7 @@ import * as path from "path";
import { PNG } from "pngjs";
import * as readline from "readline";
import { sprintf } from "sprintf-js";
import { Readable } from "stream";
import { debounce } from "throttle-debounce";
import * as util from "util";
import winston from "winston";
@ -20,12 +21,13 @@ import { fetch } from "../frontend/util/fetchutil";
import * as keyutil from "../frontend/util/keyutil";
import { fireAndForget } from "../frontend/util/util";
import { AuthKey, AuthKeyEnv, configureAuthKeyRequestInjection } from "./authkey";
import { initDocsite } from "./docsite";
import { ElectronWshClient, initElectronWshClient } from "./emain-wsh";
import { getLaunchSettings } from "./launchsettings";
import { getAppMenu } from "./menu";
import {
getElectronAppBasePath,
getGoAppBasePath,
getElectronAppUnpackedBasePath,
getWaveHomeDir,
getWaveSrvCwd,
getWaveSrvPath,
@ -95,7 +97,7 @@ console.log(
"waveterm-app starting, WAVETERM_HOME=%s, electronpath=%s gopath=%s arch=%s/%s",
waveHome,
getElectronAppBasePath(),
getGoAppBasePath(),
getElectronAppUnpackedBasePath(),
unamePlatform,
unameArch
)
@ -155,7 +157,7 @@ function runWaveSrv(): Promise<boolean> {
pReject = argReject;
});
const envCopy = { ...process.env };
envCopy[WaveAppPathVarName] = getGoAppBasePath();
envCopy[WaveAppPathVarName] = getElectronAppUnpackedBasePath();
envCopy[WaveSrvReadySignalPidVarName] = process.pid.toString();
envCopy[AuthKeyEnv] = AuthKey;
const waveSrvCmd = getWaveSrvPath();
@ -580,6 +582,110 @@ electron.ipcMain.on("open-external", (event, url) => {
}
});
type UrlInSessionResult = {
stream: Readable;
mimeType: string;
fileName: string;
};
function getSingleHeaderVal(headers: Record<string, string | string[]>, key: string): string {
const val = headers[key];
if (val == null) {
return null;
}
if (Array.isArray(val)) {
return val[0];
}
return val;
}
function cleanMimeType(mimeType: string): string {
if (mimeType == null) {
return null;
}
const parts = mimeType.split(";");
return parts[0].trim();
}
function getFileNameFromUrl(url: string): string {
try {
const pathname = new URL(url).pathname;
const filename = pathname.substring(pathname.lastIndexOf("/") + 1);
return filename;
} catch (e) {
return null;
}
}
function getUrlInSession(session: Electron.Session, url: string): Promise<UrlInSessionResult> {
return new Promise((resolve, reject) => {
// Handle data URLs directly
if (url.startsWith("data:")) {
const parts = url.split(",");
if (parts.length < 2) {
return reject(new Error("Invalid data URL"));
}
const header = parts[0]; // Get the data URL header (e.g., data:image/png;base64)
const base64Data = parts[1]; // Get the base64 data part
const mimeType = header.split(";")[0].slice(5); // Extract the MIME type (after "data:")
const buffer = Buffer.from(base64Data, "base64");
const readable = Readable.from(buffer);
resolve({ stream: readable, mimeType, fileName: "image" });
return;
}
const request = electron.net.request({
url,
method: "GET",
session, // Attach the session directly to the request
});
const readable = new Readable({
read() {}, // No-op, we'll push data manually
});
request.on("response", (response) => {
const mimeType = cleanMimeType(getSingleHeaderVal(response.headers, "content-type"));
const fileName = getFileNameFromUrl(url) || "image";
response.on("data", (chunk) => {
readable.push(chunk); // Push data to the readable stream
});
response.on("end", () => {
readable.push(null); // Signal the end of the stream
resolve({ stream: readable, mimeType, fileName });
});
});
request.on("error", (err) => {
readable.destroy(err); // Destroy the stream on error
reject(err);
});
request.end();
});
}
electron.ipcMain.on("webview-image-contextmenu", (event: electron.IpcMainEvent, payload: { src: string }) => {
const menu = new electron.Menu();
const win = electron.BrowserWindow.fromWebContents(event.sender.hostWebContents);
if (win == null) {
return;
}
menu.append(
new electron.MenuItem({
label: "Save Image",
click: () => {
const resultP = getUrlInSession(event.sender.session, payload.src);
resultP
.then((result) => {
saveImageFileWithNativeDialog(result.fileName, result.mimeType, result.stream);
})
.catch((e) => {
console.log("error getting image", e);
});
},
})
);
const { x, y } = electron.screen.getCursorScreenPoint();
const windowPos = win.getPosition();
menu.popup({ window: win, x: x - windowPos[0], y: y - windowPos[1] });
});
electron.ipcMain.on("download", (event, payload) => {
const window = electron.BrowserWindow.fromWebContents(event.sender);
const streamingUrl = getWebServerEndpoint() + "/wave/stream-file?path=" + encodeURIComponent(payload.filePath);
@ -701,6 +807,57 @@ async function createNewWaveWindow(): Promise<void> {
newBrowserWindow.show();
}
function saveImageFileWithNativeDialog(defaultFileName: string, mimeType: string, readStream: Readable) {
if (defaultFileName == null || defaultFileName == "") {
defaultFileName = "image";
}
const window = electron.BrowserWindow.getFocusedWindow(); // Get the current window context
const mimeToExtension: { [key: string]: string } = {
"image/png": "png",
"image/jpeg": "jpg",
"image/gif": "gif",
"image/webp": "webp",
"image/bmp": "bmp",
"image/tiff": "tiff",
"image/heic": "heic",
};
function addExtensionIfNeeded(fileName: string, mimeType: string): string {
const extension = mimeToExtension[mimeType];
if (!path.extname(fileName) && extension) {
return `${fileName}.${extension}`;
}
return fileName;
}
defaultFileName = addExtensionIfNeeded(defaultFileName, mimeType);
electron.dialog
.showSaveDialog(window, {
title: "Save Image",
defaultPath: defaultFileName,
filters: [{ name: "Images", extensions: ["png", "jpg", "jpeg", "gif", "webp", "bmp", "tiff", "heic"] }],
})
.then((file) => {
if (file.canceled) {
return;
}
const writeStream = fs.createWriteStream(file.filePath);
readStream.pipe(writeStream);
writeStream.on("finish", () => {
console.log("saved file", file.filePath);
});
writeStream.on("error", (err) => {
console.log("error saving file (writeStream)", err);
readStream.destroy();
});
readStream.on("error", (err) => {
console.error("error saving file (readStream)", err);
writeStream.destroy(); // Stop the write stream
});
})
.catch((err) => {
console.log("error trying to save file", err);
});
}
electron.ipcMain.on("open-new-window", () => fireAndForget(createNewWaveWindow));
electron.ipcMain.on("contextmenu-show", (event, menuDefArr?: ElectronContextMenuItem[]) => {
@ -871,6 +1028,7 @@ async function appMain() {
await electronApp.whenReady();
configureAuthKeyRequestInjection(electron.session.defaultSession);
await relaunchBrowserWindows();
await initDocsite();
setTimeout(runActiveTimer, 5000); // start active timer, wait 5s just to be safe
try {
initElectronWshClient();

View File

@ -11,6 +11,16 @@ type AppMenuCallbacks = {
relaunchBrowserWindows: () => Promise<void>;
};
function getWindowWebContents(window: electron.BaseWindow): electron.WebContents {
if (window == null) {
return null;
}
if (window instanceof electron.BrowserWindow) {
return window.webContents;
}
return null;
}
function getAppMenu(callbacks: AppMenuCallbacks): Electron.Menu {
const fileMenu: Electron.MenuItemConstructorOptions[] = [
{
@ -30,7 +40,7 @@ function getAppMenu(callbacks: AppMenuCallbacks): Electron.Menu {
{
label: "About Wave Terminal",
click: (_, window) => {
window?.webContents.send("menu-item-about");
getWindowWebContents(window)?.send("menu-item-about");
},
},
{
@ -122,21 +132,29 @@ function getAppMenu(callbacks: AppMenuCallbacks): Electron.Menu {
label: "Actual Size",
accelerator: "CommandOrControl+0",
click: (_, window) => {
window.webContents.setZoomFactor(1);
getWindowWebContents(window)?.setZoomFactor(1);
},
},
{
label: "Zoom In",
accelerator: "CommandOrControl+=",
click: (_, window) => {
window.webContents.setZoomFactor(window.webContents.getZoomFactor() + 0.2);
const wc = getWindowWebContents(window);
if (wc == null) {
return;
}
wc.setZoomFactor(wc.getZoomFactor() + 0.2);
},
},
{
label: "Zoom In (hidden)",
accelerator: "CommandOrControl+Shift+=",
click: (_, window) => {
window.webContents.setZoomFactor(window.webContents.getZoomFactor() + 0.2);
const wc = getWindowWebContents(window);
if (wc == null) {
return;
}
wc.setZoomFactor(wc.getZoomFactor() + 0.2);
},
visible: false,
acceleratorWorksWhenHidden: true,
@ -145,7 +163,11 @@ function getAppMenu(callbacks: AppMenuCallbacks): Electron.Menu {
label: "Zoom Out",
accelerator: "CommandOrControl+-",
click: (_, window) => {
window.webContents.setZoomFactor(window.webContents.getZoomFactor() - 0.2);
const wc = getWindowWebContents(window);
if (wc == null) {
return;
}
wc.setZoomFactor(wc.getZoomFactor() - 0.2);
},
},
{

View File

@ -36,6 +36,9 @@ ipcMain.on("get-user-name", (event) => {
ipcMain.on("get-host-name", (event) => {
event.returnValue = os.hostname();
});
ipcMain.on("get-webview-preload", (event) => {
event.returnValue = path.join(getElectronAppBasePath(), "preload", "preload-webview.cjs");
});
// must match golang
function getWaveHomeDir() {
@ -50,7 +53,7 @@ function getElectronAppBasePath(): string {
return path.dirname(import.meta.dirname);
}
function getGoAppBasePath(): string {
function getElectronAppUnpackedBasePath(): string {
return getElectronAppBasePath().replace("app.asar", "app.asar.unpacked");
}
@ -59,10 +62,10 @@ const wavesrvBinName = `wavesrv.${unameArch}`;
function getWaveSrvPath(): string {
if (process.platform === "win32") {
const winBinName = `${wavesrvBinName}.exe`;
const appPath = path.join(getGoAppBasePath(), "bin", winBinName);
const appPath = path.join(getElectronAppUnpackedBasePath(), "bin", winBinName);
return `${appPath}`;
}
return path.join(getGoAppBasePath(), "bin", wavesrvBinName);
return path.join(getElectronAppUnpackedBasePath(), "bin", wavesrvBinName);
}
function getWaveSrvCwd(): string {
@ -71,7 +74,7 @@ function getWaveSrvCwd(): string {
export {
getElectronAppBasePath,
getGoAppBasePath,
getElectronAppUnpackedBasePath,
getWaveHomeDir,
getWaveSrvCwd,
getWaveSrvPath,

28
emain/preload-webview.ts Normal file
View File

@ -0,0 +1,28 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
const { ipcRenderer } = require("electron");
document.addEventListener("contextmenu", (event) => {
console.log("contextmenu event", event);
if (event.target == null) {
return;
}
const targetElement = event.target as HTMLElement;
// Check if the right-click is on an image
if (targetElement.tagName === "IMG") {
setTimeout(() => {
if (event.defaultPrevented) {
return;
}
event.preventDefault();
const imgElem = targetElement as HTMLImageElement;
const imageUrl = imgElem.src;
ipcRenderer.send("webview-image-contextmenu", { src: imageUrl });
}, 50);
return;
}
// do nothing
});
console.log("loaded wave preload-webview.ts");

View File

@ -11,6 +11,8 @@ contextBridge.exposeInMainWorld("api", {
getUserName: () => ipcRenderer.sendSync("get-user-name"),
getHostName: () => ipcRenderer.sendSync("get-host-name"),
getAboutModalDetails: () => ipcRenderer.sendSync("get-about-modal-details"),
getDocsiteUrl: () => ipcRenderer.sendSync("get-docsite-url"),
getWebviewPreload: () => ipcRenderer.sendSync("get-webview-preload"),
openNewWindow: () => ipcRenderer.send("open-new-window"),
showContextMenu: (menu, position) => ipcRenderer.send("contextmenu-show", menu, position),
onContextMenuClick: (callback) => ipcRenderer.on("contextmenu-click", (_event, id) => callback(id)),

View File

@ -16,6 +16,13 @@ import * as jotai from "jotai";
const simpleControlShiftAtom = jotai.atom(false);
const globalKeyMap = new Map<string, (waveEvent: WaveKeyboardEvent) => boolean>();
function getFocusedBlockInActiveTab() {
const activeTabId = globalStore.get(atoms.activeTabId);
const layoutModel = getLayoutModelForTabById(activeTabId);
const focusedNode = globalStore.get(layoutModel.focusedNode);
return focusedNode.data?.blockId;
}
function getSimpleControlShiftAtom() {
return simpleControlShiftAtom;
}
@ -161,12 +168,6 @@ function appHandleKeyDown(waveEvent: WaveKeyboardEvent): boolean {
const blockId = focusedNode?.data?.blockId;
if (blockId != null && shouldDispatchToBlock(waveEvent)) {
const bcm = getBlockComponentModel(blockId);
if (bcm.openSwitchConnection != null) {
if (keyutil.checkKeyPressed(waveEvent, "Cmd:g")) {
bcm.openSwitchConnection();
return true;
}
}
const viewModel = bcm?.viewModel;
if (viewModel?.keyDownHandler) {
const handledByBlock = viewModel.keyDownHandler(waveEvent);
@ -262,6 +263,13 @@ function registerGlobalKeys() {
switchBlockInDirection(tabId, NavigateDirection.Right);
return true;
});
globalKeyMap.set("Cmd:g", () => {
const bcm = getBlockComponentModel(getFocusedBlockInActiveTab());
if (bcm.openSwitchConnection != null) {
bcm.openSwitchConnection();
return true;
}
});
for (let idx = 1; idx <= 9; idx++) {
globalKeyMap.set(`Cmd:${idx}`, () => {
switchTabAbs(idx);
@ -282,6 +290,11 @@ function registerGlobalKeys() {
getApi().registerGlobalWebviewKeys(allKeys);
}
function getAllGlobalKeyBindings(): string[] {
const allKeys = Array.from(globalKeyMap.keys());
return allKeys;
}
// these keyboard events happen *anywhere*, even if you have focus in an input or somewhere else.
function handleGlobalWaveKeyboardEvents(waveEvent: WaveKeyboardEvent): boolean {
for (const key of globalKeyMap.keys()) {
@ -297,6 +310,7 @@ function handleGlobalWaveKeyboardEvents(waveEvent: WaveKeyboardEvent): boolean {
export {
appHandleKeyDown,
getAllGlobalKeyBindings,
getSimpleControlShiftAtom,
registerControlShiftStateUpdateHandler,
registerElectronReinjectKeyHandler,

View File

@ -3,15 +3,10 @@
.help-view {
width: 100%;
padding: 0 5px;
.title {
font-weight: bold;
}
code {
font: var(--fixed-font);
background-color: var(--highlight-bg-color);
padding: 0 5px;
height: 100%;
.docsite-webview {
width: 100%;
height: 100%;
border: 0;
}
}

View File

@ -1,208 +1,15 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { Markdown } from "@/app/element/markdown";
import { globalStore } from "@/app/store/global";
import { Atom, atom, PrimitiveAtom } from "jotai";
import { getApi } from "@/app/store/global";
import { useState } from "react";
import "./helpview.less";
const helpText = `
For more up-to-date documentation, please visit [our docs site](http://docs.waveterm.dev)
## Blocks
Every individual Component is contained in its own block. These can be added, removed, moved and resized. Each block has its own header which can be right clicked to reveal more operations you can do with that block.
### How to Add a Block
Adding a block can be done using the widget bar on the right hand side of the window. This will add a block of the selected type to the current tab.
### How to Close a Block
Blocks can be closed by clicking the &#x2715; button on the right side of the header. Alternatively, the currently focused block can be closed by pressing \`Cmd + w\`.
### How to Navigate Blocks
At most, it is possible to have one block be focused. Depending on the type of block, this allows you to directly interact with the content in that block. A focused block is always outlined with a distinct border. A block may be focused by clicking on it. Alternatively, you can change the focused block by pressing <code>Ctrl + Shift + &uarr;</code>, <code>Ctrl + Shift + &darr;</code>, <code>Ctrl + Shift + &larr;</code>, or <code>Ctrl + Shift + &rarr;</code>to navigate relative to the currently selected block.
1
### How to Magnify Blocks
Magnifying a block will pop the block out in front of everything else. You can magnify using the header icon, or with \`Cmd + m\`.
### How to Reorganize Blocks
By dragging and dropping their headers, blocks 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 block that is being dragged. When the block is over a valid drop point, the area where it would be moved to will turn green. Releasing the click will place the block there and reflow the other blocks around it. If you see a green box cover half of two different blocks, the drop will place the block between the two. If you see the green box cover half of one block at the edge of the screen, the block will be placed between that block and the edge of the screen. If you see the green box cover one block entirely, the two blocks will swap locations.
### How to Resize Blocks
Hovering the mouse between two blocks changes your cursor to &harr; and reveals a green line dividing the blocks. By dragging and dropping this green line, you are able to resize the blocks adjacent to it.
## Types of Blocks
### 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 block 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 block 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 block is focused, you can use the &uarr; and &darr; 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 \`Cmd + ArrowUp\`.
##### Navigate Back and Forward
When looking at a file, you can navigate back by clicking the back button in the block header or the keyboard shortcut \`Cmd + ArrowLeft\`. You can always navigate back and forward using \`Cmd + ArrowLeft\` and \`Cmd + ArrowRight\`.
##### Filter the List of Files
While the block is focused, you can filter by filename by typing a substring of the filename you're working for. To clear the filter, you can click the &#x2715; on the filter dropdown or press esc.
##### 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 block header, there is an &#128065;&#65039; button. Clicking this button hides and shows hidden files.
##### Refresh the Directory
At the right of the block header, there is a refresh button. Clicking this button refreshes the directory contents.
##### Navigate to Common Directories
At the left of the block header, there is a file icon. 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 block (alternatively, click the gear icon), one of the menu items listed is **Open Terminal in New Block**. This will create a new terminal block 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 Block** 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 Block for a Child, you can right click on that file's row and select the **Open Preview in New Block** option.
#### 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 block, but it is important enough to be singled out.
After opening a codeedit block, it is often useful to magnify it (\`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 \`Cmd + w\` to close the block (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 \`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 \`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 \`Cmd + r\`.
### AI
#### How to Ask an LLM a Question
Asking a question is as simple as typing a message in the prompt and pressing enter. By default, we forward messages to the *gpt-4o-mini* model through our server.
#### How To Change The Model
See *settings help* for more info on how to configure your model.
### Web
The Web block is basically a simple web browser. The forward and backwards navigation have been added to the header.
You can use \`wsh\` to interact with the web block's URL (see the wsh section below).
### Cpu %
A small plot displaying the % of CPU in use over time. This is an example of a block that is capable of plotting streamed data. We plan to make this more generic in the future.
## Tabs
Tabs are ways to organize your blocks into separate screens. They mostly work the way you're used to in other apps.
### Create a New Tab
A tab can be created by clicking the plus button to the right of your currently existing tabs
### Delete a Tab
Hovering a tab reveals an &#x2715; button to the right side of it. Clicking it removes the tab. Note that this will also remove the instances of the blocks it contains.
### Change a Tab Name
Double clicking the current tab name makes it possible to change the name of your tab. You are limited to 10 glyphs in your tab name. Note that we say glyphs because it is possible to use multiple-character glyphs including emojis in your tab name.
### Reorganize Tabs
Tabs can be reorganized by dragging and dropping them to the left and right of other tabs.
## Theming
It is possible to style each tab individually. This is most-easily done by right clicking on your tab and going to the background menu. From there, you can select from five different pre-made styles.
It is possible to get more fine-grained control of the styles as well. See *settings help* for more info.
## wsh command
The wsh command is always available from wave terminal windows. 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 editor [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 edit\` 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 by running:
\`\`\`
wsh getmeta [blockid]
\`\`\`
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.
### setmeta
You can update any metadata key value pair for blocks (and tabs) by using the setmeta command:
\`\`\`
wsh setmeta [blockid] [key]=[value]
wsh setmeta [blockid] file=~/myfile.txt
wsh setmeta [blockid] url=https://waveterm.dev/
\`\`\`
You can get block and tab ids by right clicking on the appropriate block and selecting "Copy BlockId". 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. can be found in the documentation.
`;
class HelpViewModel implements ViewModel {
viewType: string;
showTocAtom: PrimitiveAtom<boolean>;
endIconButtons: Atom<IconButtonDecl[]>;
constructor() {
this.viewType = "help";
this.showTocAtom = atom(false);
this.endIconButtons = atom([
{
elemtype: "iconbutton",
icon: "book",
title: "Table of Contents",
click: () => this.showTocToggle(),
},
] as IconButtonDecl[]);
}
showTocToggle() {
globalStore.set(this.showTocAtom, !globalStore.get(this.showTocAtom));
}
}
@ -210,8 +17,13 @@ function makeHelpViewModel() {
return new HelpViewModel();
}
function HelpView({ model }: { model: HelpViewModel }) {
return <Markdown text={helpText} showTocAtom={model.showTocAtom} className="help-view" />;
function HelpView({}: { model: HelpViewModel }) {
const [url] = useState(() => getApi().getDocsiteUrl());
return (
<div className="help-view">
<webview className="docsite-webview" src={url} />
</div>
);
}
export { HelpView, HelpViewModel, makeHelpViewModel };

View File

@ -7,18 +7,38 @@
display: flex;
flex-direction: column;
height: 100%;
--min-row-width: 35rem;
.dir-table {
height: 100%;
min-width: 600px;
width: 100%;
--col-size-size: 0.2rem;
border-radius: 3px;
display: flex;
flex-direction: column;
font: var(--base-font);
&:not([data-scroll-height="0"]) .dir-table-head::after {
background: rgb(from var(--block-bg-color) r g b / 0.2);
}
.dir-table-head::after {
content: "";
z-index: -1;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
backdrop-filter: blur(4px);
}
.dir-table-head {
position: sticky;
top: 0;
z-index: 10;
width: 100%;
border-bottom: 1px solid var(--border-color);
.dir-table-head-row {
display: flex;
border-bottom: 1px solid var(--border-color);
min-width: var(--min-row-width);
padding: 4px 6px;
font-size: 0.75rem;
@ -68,10 +88,8 @@
}
.dir-table-body {
flex: 1 1 auto;
display: flex;
flex-direction: column;
overflow: hidden;
.dir-table-body-search-display {
display: flex;
border-radius: 3px;
@ -94,6 +112,7 @@
align-items: center;
border-radius: 5px;
padding: 0 6px;
min-width: var(--min-row-width);
&.focused {
background-color: rgb(from var(--accent-color) r g b / 0.5);

View File

@ -3,10 +3,10 @@
import { ContextMenuModel } from "@/app/store/contextmenu";
import { atoms, createBlock, getApi } from "@/app/store/global";
import { FileService } from "@/app/store/services";
import type { PreviewModel } from "@/app/view/preview/preview";
import * as services from "@/store/services";
import * as keyutil from "@/util/keyutil";
import * as util from "@/util/util";
import { checkKeyPressed, isCharacterKeyEvent } from "@/util/keyutil";
import { base64ToString, isBlank } from "@/util/util";
import {
Column,
Row,
@ -19,14 +19,11 @@ import {
} from "@tanstack/react-table";
import clsx from "clsx";
import dayjs from "dayjs";
import * as jotai from "jotai";
import { OverlayScrollbarsComponent } from "overlayscrollbars-react";
import { useAtom, useAtomValue } from "jotai";
import { OverlayScrollbarsComponent, OverlayScrollbarsComponentRef } from "overlayscrollbars-react";
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { quote as shellQuote } from "shell-quote";
import { OverlayScrollbars } from "overlayscrollbars";
import { useDimensionsWithExistingRef } from "@/app/hook/useDimensions";
import { debounce } from "throttle-debounce";
import "./directorypreview.less";
interface DirectoryTableProps {
@ -95,7 +92,7 @@ function getLastModifiedTime(unixMillis: number, column: Column<FileInfo, number
const iconRegex = /^[a-z0-9- ]+$/;
function isIconValid(icon: string): boolean {
if (util.isBlank(icon)) {
if (isBlank(icon)) {
return false;
}
return icon.match(iconRegex) != null;
@ -134,11 +131,11 @@ function DirectoryTable({
setSelectedPath,
setRefreshVersion,
}: DirectoryTableProps) {
const fullConfig = jotai.useAtomValue(atoms.fullConfigAtom);
const fullConfig = useAtomValue(atoms.fullConfigAtom);
const getIconFromMimeType = useCallback(
(mimeType: string): string => {
while (mimeType.length > 0) {
let icon = fullConfig.mimetypes?.[mimeType]?.icon ?? null;
const icon = fullConfig.mimetypes?.[mimeType]?.icon ?? null;
if (isIconValid(icon)) {
return `fa fa-solid fa-${icon} fa-fw`;
}
@ -149,10 +146,7 @@ function DirectoryTable({
[fullConfig.mimetypes]
);
const getIconColor = useCallback(
(mimeType: string): string => {
let iconColor = fullConfig.mimetypes?.[mimeType]?.color ?? "inherit";
return iconColor;
},
(mimeType: string): string => fullConfig.mimetypes?.[mimeType]?.color ?? "inherit",
[fullConfig.mimetypes]
);
const columns = useMemo(
@ -261,8 +255,25 @@ function DirectoryTable({
return colSizes;
}, [table.getState().columnSizingInfo]);
const osRef = useRef<OverlayScrollbarsComponentRef>();
const bodyRef = useRef<HTMLDivElement>();
const [scrollHeight, setScrollHeight] = useState(0);
const onScroll = useCallback(
debounce(2, () => {
setScrollHeight(osRef.current.osInstance().elements().viewport.scrollTop);
}),
[]
);
return (
<div className="dir-table" style={{ ...columnSizeVars }}>
<OverlayScrollbarsComponent
options={{ scrollbars: { autoHide: "leave" } }}
events={{ scroll: onScroll }}
className="dir-table"
style={{ ...columnSizeVars }}
ref={osRef}
data-scroll-height={scrollHeight}
>
<div className="dir-table-head">
{table.getHeaderGroups().map((headerGroup) => (
<div className="dir-table-head-row" key={headerGroup.id}>
@ -295,6 +306,7 @@ function DirectoryTable({
</div>
{table.getState().columnSizingInfo.isResizingColumn ? (
<MemoizedTableBody
bodyRef={bodyRef}
model={model}
data={data}
table={table}
@ -304,9 +316,11 @@ function DirectoryTable({
setSearch={setSearch}
setSelectedPath={setSelectedPath}
setRefreshVersion={setRefreshVersion}
osRef={osRef.current}
/>
) : (
<TableBody
bodyRef={bodyRef}
model={model}
data={data}
table={table}
@ -316,13 +330,15 @@ function DirectoryTable({
setSearch={setSearch}
setSelectedPath={setSelectedPath}
setRefreshVersion={setRefreshVersion}
osRef={osRef.current}
/>
)}
</div>
</OverlayScrollbarsComponent>
);
}
interface TableBodyProps {
bodyRef: React.RefObject<HTMLDivElement>;
model: PreviewModel;
data: Array<FileInfo>;
table: Table<FileInfo>;
@ -332,48 +348,32 @@ interface TableBodyProps {
setSearch: (_: string) => void;
setSelectedPath: (_: string) => void;
setRefreshVersion: React.Dispatch<React.SetStateAction<number>>;
osRef: OverlayScrollbarsComponentRef;
}
function TableBody({
bodyRef,
model,
data,
table,
search,
focusIndex,
setFocusIndex,
setSearch,
setSelectedPath,
setRefreshVersion,
osRef,
}: TableBodyProps) {
const [bodyHeight, setBodyHeight] = useState(0);
const dummyLineRef = useRef<HTMLDivElement>(null);
const parentRef = useRef<HTMLDivElement>(null);
const warningBoxRef = useRef<HTMLDivElement>(null);
const osInstanceRef = useRef<OverlayScrollbars>(null);
const dummyLineRef = useRef<HTMLDivElement>();
const warningBoxRef = useRef<HTMLDivElement>();
const rowRefs = useRef<HTMLDivElement[]>([]);
const domRect = useDimensionsWithExistingRef(parentRef, 30);
const parentHeight = domRect?.height ?? 0;
const conn = jotai.useAtomValue(model.connection);
const conn = useAtomValue(model.connection);
useEffect(() => {
if (dummyLineRef.current && data && parentRef.current) {
const rowHeight = dummyLineRef.current.offsetHeight;
const fullTBodyHeight = rowHeight * data.length;
const warningBoxHeight = warningBoxRef.current?.offsetHeight ?? 0;
const maxHeightLessHeader = parentHeight - warningBoxHeight;
const tbodyHeight = Math.min(maxHeightLessHeader, fullTBodyHeight);
setBodyHeight(tbodyHeight);
}
}, [data, parentHeight]);
useEffect(() => {
if (focusIndex !== null && rowRefs.current[focusIndex] && parentRef.current) {
const viewport = osInstanceRef.current.elements().viewport;
if (focusIndex !== null && rowRefs.current[focusIndex] && bodyRef.current && osRef) {
const viewport = osRef.osInstance().elements().viewport;
const viewportHeight = viewport.offsetHeight;
const rowElement = rowRefs.current[focusIndex];
const rowRect = rowElement.getBoundingClientRect();
const parentRect = parentRef.current.getBoundingClientRect();
const parentRect = bodyRef.current.getBoundingClientRect();
const viewportScrollTop = viewport.scrollTop;
const rowTopRelativeToViewport = rowRect.top - parentRect.top + viewportScrollTop;
@ -387,7 +387,7 @@ function TableBody({
viewport.scrollTo({ top: rowBottomRelativeToViewport - viewportHeight });
}
}
}, [focusIndex, parentHeight]);
}, [focusIndex]);
const handleFileContextMenu = useCallback(
(e: any, path: string, mimetype: string) => {
@ -455,7 +455,7 @@ function TableBody({
menu.push({
label: "Delete File",
click: async () => {
await services.FileService.DeleteFile(conn, path).catch((e) => console.log(e));
await FileService.DeleteFile(conn, path).catch((e) => console.log(e));
setRefreshVersion((current) => current + 1);
},
});
@ -492,12 +492,8 @@ function TableBody({
[setSearch, handleFileContextMenu, setFocusIndex, focusIndex]
);
const handleScrollbarInitialized = (instance) => {
osInstanceRef.current = instance;
};
return (
<div className="dir-table-body" ref={parentRef}>
<div className="dir-table-body" ref={bodyRef}>
{search !== "" && (
<div className="dir-table-body-search-display" ref={warningBoxRef}>
<span>Searching for "{search}"</span>
@ -507,18 +503,13 @@ function TableBody({
</div>
</div>
)}
<OverlayScrollbarsComponent
options={{ scrollbars: { autoHide: "leave" } }}
events={{ initialized: handleScrollbarInitialized }}
>
<div className="dir-table-body-scroll-box" style={{ height: bodyHeight }}>
<div className="dummy dir-table-body-row" ref={dummyLineRef}>
<div className="dir-table-body-cell">dummy-data</div>
</div>
{table.getTopRows().map(displayRow)}
{table.getCenterRows().map((row, idx) => displayRow(row, idx + table.getTopRows().length))}
<div className="dir-table-body-scroll-box">
<div className="dummy dir-table-body-row" ref={dummyLineRef}>
<div className="dir-table-body-cell">dummy-data</div>
</div>
</OverlayScrollbarsComponent>
{table.getTopRows().map(displayRow)}
{table.getCenterRows().map((row, idx) => displayRow(row, idx + table.getTopRows().length))}
</div>
</div>
);
}
@ -537,11 +528,11 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) {
const [focusIndex, setFocusIndex] = useState(0);
const [unfilteredData, setUnfilteredData] = useState<FileInfo[]>([]);
const [filteredData, setFilteredData] = useState<FileInfo[]>([]);
const fileName = jotai.useAtomValue(model.metaFilePath);
const showHiddenFiles = jotai.useAtomValue(model.showHiddenFiles);
const fileName = useAtomValue(model.metaFilePath);
const showHiddenFiles = useAtomValue(model.showHiddenFiles);
const [selectedPath, setSelectedPath] = useState("");
const [refreshVersion, setRefreshVersion] = jotai.useAtom(model.refreshVersion);
const conn = jotai.useAtomValue(model.connection);
const [refreshVersion, setRefreshVersion] = useAtom(model.refreshVersion);
const conn = useAtomValue(model.connection);
useEffect(() => {
model.refreshCallback = () => {
@ -554,8 +545,8 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) {
useEffect(() => {
const getContent = async () => {
const file = await services.FileService.ReadFile(conn, fileName);
const serializedContent = util.base64ToString(file?.data64);
const file = await FileService.ReadFile(conn, fileName);
const serializedContent = base64ToString(file?.data64);
const content: FileInfo[] = JSON.parse(serializedContent);
setUnfilteredData(content);
};
@ -574,19 +565,19 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) {
useEffect(() => {
model.directoryKeyDownHandler = (waveEvent: WaveKeyboardEvent): boolean => {
if (keyutil.checkKeyPressed(waveEvent, "Escape")) {
if (checkKeyPressed(waveEvent, "Escape")) {
setSearchText("");
return;
}
if (keyutil.checkKeyPressed(waveEvent, "ArrowUp")) {
if (checkKeyPressed(waveEvent, "ArrowUp")) {
setFocusIndex((idx) => Math.max(idx - 1, 0));
return true;
}
if (keyutil.checkKeyPressed(waveEvent, "ArrowDown")) {
if (checkKeyPressed(waveEvent, "ArrowDown")) {
setFocusIndex((idx) => Math.min(idx + 1, filteredData.length - 1));
return true;
}
if (keyutil.checkKeyPressed(waveEvent, "Enter")) {
if (checkKeyPressed(waveEvent, "Enter")) {
if (filteredData.length == 0) {
return;
}
@ -594,14 +585,14 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) {
setSearchText("");
return true;
}
if (keyutil.checkKeyPressed(waveEvent, "Backspace")) {
if (checkKeyPressed(waveEvent, "Backspace")) {
if (searchText.length == 0) {
return true;
}
setSearchText((current) => current.slice(0, -1));
return true;
}
if (keyutil.isCharacterKeyEvent(waveEvent)) {
if (isCharacterKeyEvent(waveEvent)) {
setSearchText((current) => current + waveEvent.key);
return true;
}

View File

@ -79,5 +79,5 @@
.full-preview-content {
flex-grow: 1;
overflow-y: hidden;
overflow: hidden;
}

View File

@ -1,6 +1,7 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { getAllGlobalKeyBindings } from "@/app/store/keymodel";
import { waveEventSubscribe } from "@/app/store/wps";
import { RpcApi } from "@/app/store/wshclientapi";
import { WindowRpcClient } from "@/app/store/wshrpcutil";
@ -265,6 +266,7 @@ const TerminalView = ({ blockId, model }: TerminalViewProps) => {
if (waveEvent.type != "keydown") {
return true;
}
// deal with terminal specific keybindings
if (keyutil.checkKeyPressed(waveEvent, "Cmd:Escape")) {
event.preventDefault();
event.stopPropagation();
@ -274,37 +276,20 @@ const TerminalView = ({ blockId, model }: TerminalViewProps) => {
});
return false;
}
if (
keyutil.checkKeyPressed(waveEvent, "Ctrl:Shift:ArrowLeft") ||
keyutil.checkKeyPressed(waveEvent, "Ctrl:Shift:ArrowRight") ||
keyutil.checkKeyPressed(waveEvent, "Ctrl:Shift:ArrowUp") ||
keyutil.checkKeyPressed(waveEvent, "Ctrl:Shift:ArrowDown")
) {
return false;
}
for (let i = 1; i <= 9; i++) {
if (
keyutil.checkKeyPressed(waveEvent, `Ctrl:Shift:c{Digit${i}}`) ||
keyutil.checkKeyPressed(waveEvent, `Ctrl:Shift:c{Numpad${i}}`)
) {
return false;
}
}
if (keyutil.checkKeyPressed(waveEvent, "Ctrl:Shift:v")) {
const p = navigator.clipboard.readText();
p.then((text) => {
termRef.current?.terminal.paste(text);
// termRef.current?.handleTermData(text);
});
event.preventDefault();
event.stopPropagation();
return true;
return false;
} else if (keyutil.checkKeyPressed(waveEvent, "Ctrl:Shift:c")) {
const sel = termRef.current?.terminal.getSelection();
navigator.clipboard.writeText(sel);
event.preventDefault();
event.stopPropagation();
return true;
return false;
}
if (shellProcStatusRef.current != "running" && keyutil.checkKeyPressed(waveEvent, "Enter")) {
// restart
@ -313,6 +298,12 @@ const TerminalView = ({ blockId, model }: TerminalViewProps) => {
prtn.catch((e) => console.log("error controller resync (enter)", blockId, e));
return false;
}
const globalKeys = getAllGlobalKeyBindings();
for (const key of globalKeys) {
if (keyutil.checkKeyPressed(waveEvent, key)) {
return false;
}
}
return true;
}
const fullConfig = globalStore.get(atoms.fullConfigAtom);

View File

@ -41,8 +41,6 @@ function promptToMsg(prompt: OpenAIPromptMessageType): ChatMessageType {
};
}
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
export class WaveAiModel implements ViewModel {
viewType: string;
blockId: string;
@ -102,14 +100,12 @@ export class WaveAiModel implements ViewModel {
// Add a typing indicator
set(this.addMessageAtom, typingMessage);
await sleep(1500);
const parts = userMessage.text.split(" ");
let currentPart = 0;
while (currentPart < parts.length) {
const part = parts[currentPart] + " ";
set(this.updateLastMessageAtom, part, true);
currentPart++;
await sleep(100);
}
set(this.updateLastMessageAtom, "", false);
});
@ -209,7 +205,6 @@ export class WaveAiModel implements ViewModel {
}
break;
}
await sleep(100);
}
globalStore.set(this.updateLastMessageAtom, "", false);
if (fullMsg != "") {

View File

@ -16,6 +16,19 @@ import * as jotai from "jotai";
import React, { memo, useEffect, useState } from "react";
import "./webview.less";
let webviewPreloadUrl = null;
function getWebviewPreloadUrl() {
if (webviewPreloadUrl == null) {
webviewPreloadUrl = getApi().getWebviewPreload();
console.log("webviewPreloadUrl", webviewPreloadUrl);
}
if (webviewPreloadUrl == null) {
return null;
}
return "file://" + webviewPreloadUrl;
}
export class WebViewModel implements ViewModel {
viewType: string;
blockId: string;
@ -501,6 +514,7 @@ const WebView = memo(({ model }: WebViewProps) => {
src={metaUrlInitial}
data-blockid={model.blockId}
data-webcontentsid={webContentsId} // needed for emain
preload={getWebviewPreloadUrl()}
// @ts-ignore This is a discrepancy between the React typing and the Chromium impl for webviewTag. Chrome webviewTag expects a string, while React expects a boolean.
allowpopups="true"
></webview>

View File

@ -80,20 +80,6 @@ async function handleWidgetSelect(blockDef: BlockDef) {
createBlock(blockDef);
}
function isIconValid(icon: string): boolean {
if (util.isBlank(icon)) {
return false;
}
return icon.match(iconRegex) != null;
}
function getIconClass(icon: string): string {
if (!isIconValid(icon)) {
return "fa fa-regular fa-browser fa-fw";
}
return `fa fa-solid fa-${icon} fa-fw`;
}
const Widget = React.memo(({ widget }: { widget: WidgetConfigType }) => {
return (
<div
@ -102,7 +88,7 @@ const Widget = React.memo(({ widget }: { widget: WidgetConfigType }) => {
title={widget.description || widget.label}
>
<div className="widget-icon" style={{ color: widget.color }}>
<i className={getIconClass(widget.icon)}></i>
<i className={util.makeIconClass(widget.icon, true, { defaultIcon: "browser" })}></i>
</div>
{!util.isBlank(widget.label) ? <div className="widget-label">{widget.label}</div> : null}
</div>

View File

@ -57,7 +57,9 @@ declare global {
getEnv: (varName: string) => string;
getUserName: () => string;
getHostName: () => string;
getWebviewPreload: () => string;
getAboutModalDetails: () => AboutModalDetails;
getDocsiteUrl: () => string;
showContextMenu: (menu?: ElectronContextMenuItem[]) => void;
onContextMenuClick: (callback: (id: string) => void) => void;
onNavigate: (callback: (url: string) => void) => void;

View File

@ -302,6 +302,7 @@ declare global {
"term:mode"?: string;
"term:theme"?: string;
"term:localshellpath"?: string;
"term:localshellopts"?: string[];
count?: number;
};
@ -419,6 +420,7 @@ declare global {
"term:fontfamily"?: string;
"term:disablewebgl"?: boolean;
"term:localshellpath"?: string;
"term:localshellopts"?: string[];
"editor:minimapenabled"?: boolean;
"editor:stickyscrollenabled"?: boolean;
"web:*"?: boolean;

View File

@ -47,16 +47,36 @@ function parseKeyDescription(keyDescription: string): KeyPressDecl {
let keys = keyDescription.replace(/[()]/g, "").split(":");
for (let key of keys) {
if (key == "Cmd") {
if (PLATFORM == PlatformMacOS) {
rtn.mods.Meta = true;
} else {
rtn.mods.Alt = true;
}
rtn.mods.Cmd = true;
} else if (key == "Shift") {
rtn.mods.Shift = true;
} else if (key == "Ctrl") {
rtn.mods.Ctrl = true;
} else if (key == "Option") {
if (PLATFORM == PlatformMacOS) {
rtn.mods.Alt = true;
} else {
rtn.mods.Meta = true;
}
rtn.mods.Option = true;
} else if (key == "Alt") {
if (PLATFORM == PlatformMacOS) {
rtn.mods.Option = true;
} else {
rtn.mods.Cmd = true;
}
rtn.mods.Alt = true;
} else if (key == "Meta") {
if (PLATFORM == PlatformMacOS) {
rtn.mods.Cmd = true;
} else {
rtn.mods.Option = true;
}
rtn.mods.Meta = true;
} else {
let { key: parsedKey, type: keyType } = parseKey(key);
@ -138,10 +158,10 @@ function isInputEvent(event: WaveKeyboardEvent): boolean {
function checkKeyPressed(event: WaveKeyboardEvent, keyDescription: string): boolean {
let keyPress = parseKeyDescription(keyDescription);
if (!keyPress.mods.Alt && notMod(keyPress.mods.Option, event.option)) {
if (notMod(keyPress.mods.Option, event.option)) {
return false;
}
if (!keyPress.mods.Meta && notMod(keyPress.mods.Cmd, event.cmd)) {
if (notMod(keyPress.mods.Cmd, event.cmd)) {
return false;
}
if (notMod(keyPress.mods.Shift, event.shift)) {
@ -150,10 +170,10 @@ function checkKeyPressed(event: WaveKeyboardEvent, keyDescription: string): bool
if (notMod(keyPress.mods.Ctrl, event.control)) {
return false;
}
if (keyPress.mods.Alt && !event.alt) {
if (notMod(keyPress.mods.Alt, event.alt)) {
return false;
}
if (keyPress.mods.Meta && !event.meta) {
if (notMod(keyPress.mods.Meta, event.meta)) {
return false;
}
let eventKey = "";

View File

@ -81,8 +81,11 @@ function jsonDeepEqual(v1: any, v2: any): boolean {
return false;
}
function makeIconClass(icon: string, fw: boolean, opts?: { spin: boolean }): string {
if (icon == null) {
function makeIconClass(icon: string, fw: boolean, opts?: { spin?: boolean; defaultIcon?: string }): string {
if (isBlank(icon)) {
if (opts?.defaultIcon != null) {
return makeIconClass(opts.defaultIcon, fw, { spin: opts?.spin });
}
return null;
}
if (icon.match(/^(solid@)?[a-z0-9-]+$/)) {
@ -95,6 +98,14 @@ function makeIconClass(icon: string, fw: boolean, opts?: { spin: boolean }): str
icon = icon.replace(/^regular@/, "");
return clsx(`fa fa-sharp fa-regular fa-${icon}`, fw ? "fa-fw" : null, opts?.spin ? "fa-spin" : null);
}
if (icon.match(/^brands@[a-z0-9-]+$/)) {
// strip off the "brands@" prefix if it exists
icon = icon.replace(/^brands@/, "");
return clsx(`fa fa-brands fa-${icon}`, fw ? "fa-fw" : null, opts?.spin ? "fa-spin" : null);
}
if (opts?.defaultIcon != null) {
return makeIconClass(opts.defaultIcon, fw, { spin: opts?.spin });
}
return null;
}

12
go.mod
View File

@ -14,19 +14,21 @@ require (
github.com/gorilla/websocket v1.5.3
github.com/jmoiron/sqlx v1.4.0
github.com/kevinburke/ssh_config v1.2.0
github.com/mattn/go-sqlite3 v1.14.23
github.com/mattn/go-sqlite3 v1.14.24
github.com/mitchellh/mapstructure v1.5.0
github.com/sashabaranov/go-openai v1.31.0
github.com/sawka/txwrap v0.2.0
github.com/shirou/gopsutil/v4 v4.24.8
github.com/shirou/gopsutil/v4 v4.24.9
github.com/skeema/knownhosts v1.3.0
github.com/spf13/cobra v1.8.1
github.com/wavetermdev/htmltoken v0.1.0
golang.org/x/crypto v0.27.0
golang.org/x/term v0.24.0
golang.org/x/crypto v0.28.0
golang.org/x/sys v0.26.0
golang.org/x/term v0.25.0
)
require (
github.com/ebitengine/purego v0.8.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
@ -34,14 +36,12 @@ require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.uber.org/atomic v1.7.0 // indirect
golang.org/x/net v0.29.0 // indirect
golang.org/x/sys v0.25.0 // indirect
)
replace github.com/kevinburke/ssh_config => github.com/wavetermdev/ssh_config v0.0.0-20240306041034-17e2087ebde2

26
go.sum
View File

@ -6,6 +6,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/ebitengine/purego v0.8.0 h1:JbqvnEzRvPpxhCJzJJ2y0RbiZ8nyjccVUrSM3q+GvvE=
github.com/ebitengine/purego v0.8.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
@ -43,8 +45,8 @@ github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.23 h1:gbShiuAP1W5j9UOksQ06aiiqPMxYecovVGwmTxWtuw0=
github.com/mattn/go-sqlite3 v1.14.23/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/photostorm/pty v1.1.19-0.20230903182454-31354506054b h1:cLGKfKb1uk0hxI0Q8L83UAJPpeJ+gSpn3cCU/tjd3eg=
@ -58,12 +60,8 @@ github.com/sashabaranov/go-openai v1.31.0 h1:rGe77x7zUeCjtS2IS7NCY6Tp4bQviXNMhkQ
github.com/sashabaranov/go-openai v1.31.0/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
github.com/sawka/txwrap v0.2.0 h1:V3LfvKVLULxcYSxdMguLwFyQFMEU9nFDJopg0ZkL+94=
github.com/sawka/txwrap v0.2.0/go.mod h1:wwQ2SQiN4U+6DU/iVPhbvr7OzXAtgZlQCIGuvOswEfA=
github.com/shirou/gopsutil/v4 v4.24.8 h1:pVQjIenQkIhqO81mwTaXjTzOMT7d3TZkf43PlVFHENI=
github.com/shirou/gopsutil/v4 v4.24.8/go.mod h1:wE0OrJtj4dG+hYkxqDH3QiBICdKSf04/npcvLLc/oRg=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
github.com/shirou/gopsutil/v4 v4.24.9 h1:KIV+/HaHD5ka5f570RZq+2SaeFsb/pq+fp2DGNWYoOI=
github.com/shirou/gopsutil/v4 v4.24.9/go.mod h1:3fkaHNeYsUFCGZ8+9vZVWtbyM1k2eRnlL+bWO8Bxa/Q=
github.com/skeema/knownhosts v1.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY=
github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
@ -87,8 +85,8 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -97,10 +95,10 @@ golang.org/x/sys v0.0.0-20220721230656-c6bc011c0c49/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM=
golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@ -7,7 +7,7 @@
"productName": "Wave",
"description": "Open-Source AI-Native Terminal Built for Seamless Workflows",
"license": "Apache-2.0",
"version": "0.8.7",
"version": "0.8.9-beta.1",
"homepage": "https://waveterm.dev",
"build": {
"appId": "dev.commandline.waveterm"
@ -30,21 +30,21 @@
"@chromatic-com/storybook": "^2.0.2",
"@eslint/js": "^9.11.1",
"@rollup/plugin-node-resolve": "^15.3.0",
"@storybook/addon-essentials": "^8.3.3",
"@storybook/addon-interactions": "^8.3.3",
"@storybook/addon-links": "^8.3.3",
"@storybook/blocks": "^8.3.3",
"@storybook/react": "^8.3.3",
"@storybook/react-vite": "^8.3.3",
"@storybook/test": "^8.3.3",
"@storybook/theming": "^8.3.3",
"@storybook/addon-essentials": "^8.3.5",
"@storybook/addon-interactions": "^8.3.5",
"@storybook/addon-links": "^8.3.5",
"@storybook/blocks": "^8.3.5",
"@storybook/react": "^8.3.5",
"@storybook/react-vite": "^8.3.5",
"@storybook/test": "^8.3.5",
"@storybook/theming": "^8.3.5",
"@types/css-tree": "^2",
"@types/debug": "^4",
"@types/electron": "^1.6.10",
"@types/node": "^22.7.4",
"@types/papaparse": "^5",
"@types/pngjs": "^6.0.5",
"@types/react": "^18.3.10",
"@types/react": "^18.3.11",
"@types/react-dom": "^18.3.0",
"@types/semver": "^7",
"@types/shell-quote": "^1",
@ -53,10 +53,10 @@
"@types/tinycolor2": "^1",
"@types/uuid": "^10.0.0",
"@types/ws": "^8",
"@vitejs/plugin-react-swc": "^3.7.0",
"@vitest/coverage-istanbul": "^2.1.1",
"@vitejs/plugin-react-swc": "^3.7.1",
"@vitest/coverage-istanbul": "^2.1.2",
"electron": "^32.1.2",
"electron-builder": "^25.0.5",
"electron-builder": "^25.1.7",
"electron-vite": "^2.3.0",
"eslint": "^9.11.1",
"eslint-config-prettier": "^9.1.0",
@ -66,19 +66,19 @@
"prettier-plugin-organize-imports": "^4.1.0",
"rollup-plugin-flow": "^1.1.1",
"semver": "^7.6.3",
"storybook": "^8.3.3",
"storybook": "^8.3.5",
"storybook-dark-mode": "^4.0.2",
"ts-node": "^10.9.2",
"tslib": "^2.6.3",
"tsx": "^4.19.1",
"typescript": "^5.6.2",
"typescript-eslint": "^8.7.0",
"typescript-eslint": "^8.8.0",
"vite": "^5.4.8",
"vite-plugin-image-optimizer": "^1.1.8",
"vite-plugin-static-copy": "^1.0.6",
"vite-plugin-svgr": "^4.2.0",
"vite-tsconfig-paths": "^5.0.1",
"vitest": "^2.1.1"
"vitest": "^2.1.2"
},
"dependencies": {
"@monaco-editor/loader": "^1.4.0",
@ -100,7 +100,7 @@
"css-tree": "^3.0.0",
"dayjs": "^1.11.13",
"debug": "^4.3.7",
"electron-updater": "6.3.4",
"electron-updater": "6.3.9",
"fast-average-color": "^9.4.0",
"htl": "^0.3.1",
"html-to-image": "^1.11.11",

View File

@ -302,6 +302,12 @@ func (bc *BlockController) DoRunShellCommand(rc *RunShellOpts, blockMeta waveobj
if blockMeta.GetString(waveobj.MetaKey_TermLocalShellPath, "") != "" {
cmdOpts.ShellPath = blockMeta.GetString(waveobj.MetaKey_TermLocalShellPath, "")
}
if len(settings.TermLocalShellOpts) > 0 {
cmdOpts.ShellOpts = append([]string{}, settings.TermLocalShellOpts...)
}
if len(blockMeta.GetStringList(waveobj.MetaKey_TermLocalShellOpts)) > 0 {
cmdOpts.ShellOpts = append([]string{}, blockMeta.GetStringList(waveobj.MetaKey_TermLocalShellOpts)...)
}
shellProc, err = shellexec.StartShellProc(rc.TermSize, cmdStr, cmdOpts)
if err != nil {
return err

29
pkg/docsite/docsite.go Normal file
View File

@ -0,0 +1,29 @@
package docsite
import (
"log"
"net/http"
"os"
"path/filepath"
"github.com/wavetermdev/waveterm/pkg/wavebase"
)
var docsiteStaticPath = filepath.Join(wavebase.GetWaveAppPath(), "docsite")
var docsiteHandler http.Handler
func GetDocsiteHandler() http.Handler {
stat, err := os.Stat(docsiteStaticPath)
if docsiteHandler == nil {
log.Println("Docsite is nil, initializing")
if err == nil && stat.IsDir() {
log.Printf("Found static site at %s, serving\n", docsiteStaticPath)
docsiteHandler = http.FileServer(http.Dir(docsiteStaticPath))
} else {
log.Println("Did not find static site, serving not found handler")
docsiteHandler = http.NotFoundHandler()
}
}
return docsiteHandler
}

View File

@ -35,6 +35,7 @@ type CommandOptsType struct {
Cwd string `json:"cwd,omitempty"`
Env map[string]string `json:"env,omitempty"`
ShellPath string `json:"shellPath,omitempty"`
ShellOpts []string `json:"shellOpts,omitempty"`
}
type ShellProc struct {
@ -159,6 +160,7 @@ func StartRemoteShellProc(termSize waveobj.TermSize, cmdStr string, cmdOpts Comm
log.Printf("error installing rc files: %v", err)
return nil, err
}
shellOpts = append(shellOpts, cmdOpts.ShellOpts...)
homeDir := remote.GetHomeDir(client)
@ -280,6 +282,7 @@ func StartShellProc(termSize waveobj.TermSize, cmdStr string, cmdOpts CommandOpt
if shellPath == "" {
shellPath = shellutil.DetectLocalShellPath()
}
shellOpts = append(shellOpts, cmdOpts.ShellOpts...)
if cmdStr == "" {
if isBashShell(shellPath) {
// add --rcfile

View File

@ -32,9 +32,6 @@ var userShellRegexp = regexp.MustCompile(`^UserShell: (.*)$`)
const DefaultShellPath = "/bin/bash"
const WaveAppPathVarName = "WAVETERM_APP_PATH"
const AppPathBinDir = "bin"
const (
ZshIntegrationDir = "shell/zsh"
BashIntegrationDir = "shell/bash"
@ -231,7 +228,7 @@ func GetWshBaseName(version string, goos string, goarch string) string {
}
func GetWshBinaryPath(version string, goos string, goarch string) string {
return filepath.Join(os.Getenv(WaveAppPathVarName), AppPathBinDir, GetWshBaseName(version, goos, goarch))
return filepath.Join(wavebase.GetWaveAppBinPath(), GetWshBaseName(version, goos, goarch))
}
func InitRcFiles(waveHome string, wshBinDir string) error {
@ -319,7 +316,7 @@ func initCustomShellStartupFilesInternal() error {
if err != nil {
return fmt.Errorf("error copying wsh binary to bin: %v", err)
}
log.Printf("wsh binary successfully %q copied to %q\n", wshBaseName, wshDstPath)
log.Printf("wsh binary successfully copied from %q to %q\n", wshBaseName, wshDstPath)
return nil
}

View File

@ -0,0 +1,30 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
//go:build !windows
package wavebase
import (
"log"
"os"
"path/filepath"
"golang.org/x/sys/unix"
)
func AcquireWaveLock() (FDLock, error) {
homeDir := GetWaveHomeDir()
lockFileName := filepath.Join(homeDir, WaveLockFile)
log.Printf("[base] acquiring lock on %s\n", lockFileName)
fd, err := os.OpenFile(lockFileName, os.O_RDWR|os.O_CREATE, 0600)
if err != nil {
return nil, err
}
err = unix.Flock(int(fd.Fd()), unix.LOCK_EX|unix.LOCK_NB)
if err != nil {
fd.Close()
return nil, err
}
return fd, nil
}

View File

@ -0,0 +1,29 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
//go:build windows
package wavebase
import (
"fmt"
"log"
"path/filepath"
"github.com/alexflint/go-filemutex"
)
func AcquireWaveLock() (FDLock, error) {
homeDir := GetWaveHomeDir()
lockFileName := filepath.Join(homeDir, WaveLockFile)
log.Printf("[base] acquiring lock on %s\n", lockFileName)
m, err := filemutex.New(lockFileName)
if err != nil {
return nil, fmt.Errorf("filemutex new error: %w", err)
}
err = m.TryLock()
if err != nil {
return nil, fmt.Errorf("filemutex trylock error: %w", err)
}
return m, nil
}

View File

@ -17,8 +17,6 @@ import (
"strings"
"sync"
"time"
"github.com/alexflint/go-filemutex"
)
// set by main-server.go
@ -35,14 +33,29 @@ const WaveDBDir = "db"
const JwtSecret = "waveterm" // TODO generate and store this
const ConfigDir = "config"
const WaveAppPathVarName = "WAVETERM_APP_PATH"
const AppPathBinDir = "bin"
var baseLock = &sync.Mutex{}
var ensureDirCache = map[string]bool{}
type FDLock interface {
Close() error
}
func IsDevMode() bool {
pdev := os.Getenv(WaveDevVarName)
return pdev != ""
}
func GetWaveAppPath() string {
return os.Getenv(WaveAppPathVarName)
}
func GetWaveAppBinPath() string {
return filepath.Join(GetWaveAppPath(), AppPathBinDir)
}
func GetHomeDir() string {
homeVar, err := os.UserHomeDir()
if err != nil {
@ -189,19 +202,6 @@ func DetermineLocale() string {
return strings.Replace(truncated, "_", "-", -1)
}
func AcquireWaveLock() (*filemutex.FileMutex, error) {
homeDir := GetWaveHomeDir()
lockFileName := filepath.Join(homeDir, WaveLockFile)
log.Printf("[base] acquiring lock on %s\n", lockFileName)
m, err := filemutex.New(lockFileName)
if err != nil {
return nil, err
}
err = m.TryLock()
return m, err
}
func ClientArch() string {
return fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH)
}

View File

@ -60,6 +60,7 @@ const (
MetaKey_TermMode = "term:mode"
MetaKey_TermTheme = "term:theme"
MetaKey_TermLocalShellPath = "term:localshellpath"
MetaKey_TermLocalShellOpts = "term:localshellopts"
MetaKey_Count = "count"
)

View File

@ -14,6 +14,24 @@ func (m MetaMapType) GetString(key string, def string) string {
return def
}
func (m MetaMapType) GetStringList(key string) []string {
v, ok := m[key]
if !ok {
return nil
}
varr, ok := v.([]any)
if !ok {
return nil
}
rtn := make([]string, 0)
for _, varrVal := range varr {
if s, ok := varrVal.(string); ok {
rtn = append(rtn, s)
}
}
return rtn
}
func (m MetaMapType) GetBool(key string, def bool) bool {
if v, ok := m[key]; ok {
if b, ok := v.(bool); ok {

View File

@ -54,12 +54,13 @@ type MetaTSType struct {
BgOpacity float64 `json:"bg:opacity,omitempty"`
BgBlendMode string `json:"bg:blendmode,omitempty"`
TermClear bool `json:"term:*,omitempty"`
TermFontSize int `json:"term:fontsize,omitempty"`
TermFontFamily string `json:"term:fontfamily,omitempty"`
TermMode string `json:"term:mode,omitempty"`
TermTheme string `json:"term:theme,omitempty"`
TermLocalShellPath string `json:"term:localshellpath,omitempty"` // matches settings
TermClear bool `json:"term:*,omitempty"`
TermFontSize int `json:"term:fontsize,omitempty"`
TermFontFamily string `json:"term:fontfamily,omitempty"`
TermMode string `json:"term:mode,omitempty"`
TermTheme string `json:"term:theme,omitempty"`
TermLocalShellPath string `json:"term:localshellpath,omitempty"` // matches settings
TermLocalShellOpts []string `json:"term:localshellopts,omitempty"` // matches settings
Count int `json:"count,omitempty"` // temp for cpu plot. will remove later
}

View File

@ -6,27 +6,85 @@
},
"bg@rainbow": {
"display:name": "Rainbow",
"display:order": 1,
"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
},
"bg@green": {
"display:name": "Green",
"display:order": 1.2,
"bg:*": true,
"bg": "green",
"bg:opacity": 0.3
},
"bg@blue": {
"display:name": "Blue",
"display:order": 1.1,
"bg:*": true,
"bg": "blue",
"bg:opacity": 0.3
},
"bg@red": {
"display:name": "Red",
"display:order": 1.3,
"bg:*": true,
"bg": "red",
"bg:opacity": 0.3
},
"bg@ocean-depths": {
"display:name": "Ocean Depths",
"display:order": 2.2,
"bg:*": true,
"bg": "linear-gradient(135deg, purple, blue, teal)",
"bg:opacity": 0.7
},
"bg@aqua-horizon": {
"display:name": "Aqua Horizon",
"display:order": 2.3,
"bg:*": true,
"bg": "linear-gradient(135deg, rgba(15, 30, 50, 1) 0%, rgba(40, 90, 130, 0.85) 30%, rgba(20, 100, 150, 0.75) 60%, rgba(0, 120, 160, 0.65) 80%, rgba(0, 140, 180, 0.55) 100%), linear-gradient(135deg, rgba(100, 80, 255, 0.4), rgba(0, 180, 220, 0.4)), radial-gradient(circle at 70% 70%, rgba(255, 255, 255, 0.05), transparent 70%)",
"bg:opacity": 0.85,
"bg:blendmode": "overlay"
},
"bg@sunset": {
"display:name": "Sunset",
"display:order": 2.4,
"bg:*": true,
"bg": "linear-gradient(135deg, rgba(128, 0, 0, 1), rgba(255, 69, 0, 0.8), rgba(75, 0, 130, 1))",
"bg:opacity": 0.8,
"bg:blendmode": "normal"
},
"bg@enchantedforest": {
"display:name": "Enchanted Forest",
"display:order": 2.7,
"bg:*": true,
"bg": "linear-gradient(145deg, rgba(0,50,0,1), rgba(34,139,34,0.7) 20%, rgba(0,100,0,0.5) 40%, rgba(0,200,100,0.3) 60%, rgba(34,139,34,0.8) 80%, rgba(0,50,0,1)), radial-gradient(circle at 30% 30%, rgba(255,255,255,0.1), transparent 80%), radial-gradient(circle at 70% 70%, rgba(255,255,255,0.1), transparent 80%)",
"bg:opacity": 0.8,
"bg:blendmode": "soft-light"
},
"bg@twilight-mist": {
"display:name": "Twilight Mist",
"display:order": 2.9,
"bg:*": true,
"bg": "linear-gradient(180deg, rgba(60,60,90,1) 0%, rgba(90,110,140,0.8) 40%, rgba(120,140,160,0.6) 70%, rgba(60,60,90,1) 100%), radial-gradient(circle at 30% 40%, rgba(255,255,255,0.15), transparent 60%), radial-gradient(circle at 70% 70%, rgba(255,255,255,0.1), transparent 70%)",
"bg:opacity": 0.9,
"bg:blendmode": "soft-light"
},
"bg@duskhorizon": {
"display:name": "Dusk Horizon",
"display:order": 3.1,
"bg:*": true,
"bg": "linear-gradient(0deg, rgba(128,0,0,1) 0%, rgba(204,85,0,0.7) 20%, rgba(255,140,0,0.6) 45%, rgba(160,90,160,0.5) 65%, rgba(60,60,120,1) 100%), radial-gradient(circle at 30% 30%, rgba(255,255,255,0.1), transparent 60%), radial-gradient(circle at 70% 70%, rgba(255,255,255,0.05), transparent 70%)",
"bg:opacity": 0.9,
"bg:blendmode": "overlay"
},
"bg@tropical-radiance": {
"display:name": "Tropical Radiance",
"display:order": 3.3,
"bg:*": true,
"bg": "linear-gradient(135deg, rgba(204, 51, 255, 0.9) 0%, rgba(255, 85, 153, 0.75) 30%, rgba(255, 51, 153, 0.65) 60%, rgba(204, 51, 255, 0.6) 80%, rgba(51, 102, 255, 0.5) 100%), radial-gradient(circle at 30% 40%, rgba(255,255,255,0.1), transparent 60%), radial-gradient(circle at 70% 70%, rgba(255,255,255,0.05), transparent 70%)",
"bg:opacity": 0.9,
"bg:blendmode": "overlay"
}
}

View File

@ -19,6 +19,7 @@ const (
ConfigKey_TermFontFamily = "term:fontfamily"
ConfigKey_TermDisableWebGl = "term:disablewebgl"
ConfigKey_TermLocalShellPath = "term:localshellpath"
ConfigKey_TermLocalShellOpts = "term:localshellopts"
ConfigKey_EditorMinimapEnabled = "editor:minimapenabled"
ConfigKey_EditorStickyScrollEnabled = "editor:stickyscrollenabled"

View File

@ -48,11 +48,12 @@ type SettingsType struct {
AiMaxTokens float64 `json:"ai:maxtokens,omitempty"`
AiTimeoutMs float64 `json:"ai:timeoutms,omitempty"`
TermClear bool `json:"term:*,omitempty"`
TermFontSize float64 `json:"term:fontsize,omitempty"`
TermFontFamily string `json:"term:fontfamily,omitempty"`
TermDisableWebGl bool `json:"term:disablewebgl,omitempty"`
TermLocalShellPath string `json:"term:localshellpath,omitempty"`
TermClear bool `json:"term:*,omitempty"`
TermFontSize float64 `json:"term:fontsize,omitempty"`
TermFontFamily string `json:"term:fontfamily,omitempty"`
TermDisableWebGl bool `json:"term:disablewebgl,omitempty"`
TermLocalShellPath string `json:"term:localshellpath,omitempty"`
TermLocalShellOpts []string `json:"term:localshellopts,omitempty"`
EditorMinimapEnabled bool `json:"editor:minimapenabled,omitempty"`
EditorStickyScrollEnabled bool `json:"editor:stickyscrollenabled,omitempty"`

View File

@ -23,6 +23,7 @@ import (
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
"github.com/wavetermdev/waveterm/pkg/authkey"
"github.com/wavetermdev/waveterm/pkg/docsite"
"github.com/wavetermdev/waveterm/pkg/filestore"
"github.com/wavetermdev/waveterm/pkg/service"
"github.com/wavetermdev/waveterm/pkg/telemetry"
@ -441,6 +442,8 @@ func MakeUnixListener() (net.Listener, error) {
return rtn, nil
}
const docsitePrefix = "/docsite/"
// blocking
func RunWebServer(listener net.Listener) {
gr := mux.NewRouter()
@ -448,6 +451,7 @@ func RunWebServer(listener net.Listener) {
gr.HandleFunc("/wave/file", WebFnWrap(WebFnOpts{AllowCaching: false}, handleWaveFile))
gr.HandleFunc("/wave/service", WebFnWrap(WebFnOpts{JsonErrors: true}, handleService))
gr.HandleFunc("/wave/log-active-state", WebFnWrap(WebFnOpts{JsonErrors: true}, handleLogActiveState))
gr.PathPrefix(docsitePrefix).Handler(http.StripPrefix(docsitePrefix, docsite.GetDocsiteHandler()))
handler := http.TimeoutHandler(gr, HttpTimeoutDuration, "Timeout")
if wavebase.IsDevMode() {
handler = handlers.CORS(handlers.AllowedOrigins([]string{"*"}))(handler)

View File

@ -3,7 +3,7 @@ module.exports = {
plugins: ["prettier-plugin-jsdoc", "prettier-plugin-organize-imports"],
printWidth: 120,
trailingComma: "es5",
useTabs: false,
useTabs: false,
jsdocVerticalAlignment: true,
jsdocSeparateReturnsFromParam: true,
jsdocSeparateTagGroups: true,

832
yarn.lock

File diff suppressed because it is too large Load Diff