mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-02-23 02:51:26 +01:00
Merge branch 'main' of github.com:wavetermdev/waveterm into red/chatview
This commit is contained in:
commit
f04774391c
2
.gitattributes
vendored
2
.gitattributes
vendored
@ -1 +1 @@
|
||||
* text=lf
|
||||
* text=auto
|
31
.github/ISSUE_TEMPLATE/bug-report.md
vendored
31
.github/ISSUE_TEMPLATE/bug-report.md
vendored
@ -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
87
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
Normal 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
|
14
.github/ISSUE_TEMPLATE/feature-request.md
vendored
14
.github/ISSUE_TEMPLATE/feature-request.md
vendored
@ -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.
|
26
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
Normal file
26
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
Normal 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
|
14
.github/workflows/build-helper.yml
vendored
14
.github/workflows/build-helper.yml
vendored
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
54
cmd/wsh/cmd/wshcmd-createblock.go
Normal file
54
cmd/wsh/cmd/wshcmd-createblock.go
Normal 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
|
||||
}
|
@ -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) {
|
||||
|
@ -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
27
emain/docsite.ts
Normal 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;
|
||||
}
|
||||
}
|
164
emain/emain.ts
164
emain/emain.ts
@ -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();
|
||||
|
@ -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);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -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
28
emain/preload-webview.ts
Normal 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");
|
@ -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)),
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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 ✕ 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 + ↑</code>, <code>Ctrl + Shift + ↓</code>, <code>Ctrl + Shift + ←</code>, or <code>Ctrl + Shift + →</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 ↔ 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 ↑ and ↓ 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 ✕ 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 👁️ 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 ✕ 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 };
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -79,5 +79,5 @@
|
||||
|
||||
.full-preview-content {
|
||||
flex-grow: 1;
|
||||
overflow-y: hidden;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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 != "") {
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
2
frontend/types/custom.d.ts
vendored
2
frontend/types/custom.d.ts
vendored
@ -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;
|
||||
|
2
frontend/types/gotypes.d.ts
vendored
2
frontend/types/gotypes.d.ts
vendored
@ -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;
|
||||
|
@ -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 = "";
|
||||
|
@ -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
12
go.mod
@ -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
26
go.sum
@ -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=
|
||||
|
34
package.json
34
package.json
@ -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",
|
||||
|
@ -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
29
pkg/docsite/docsite.go
Normal 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
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
30
pkg/wavebase/wavebase-posix.go
Normal file
30
pkg/wavebase/wavebase-posix.go
Normal 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
|
||||
}
|
29
pkg/wavebase/wavebase-win.go
Normal file
29
pkg/wavebase/wavebase-win.go
Normal 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
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -60,6 +60,7 @@ const (
|
||||
MetaKey_TermMode = "term:mode"
|
||||
MetaKey_TermTheme = "term:theme"
|
||||
MetaKey_TermLocalShellPath = "term:localshellpath"
|
||||
MetaKey_TermLocalShellOpts = "term:localshellopts"
|
||||
|
||||
MetaKey_Count = "count"
|
||||
)
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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"`
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user