Merge remote-tracking branch 'origin/main' into red/tabs-new-design-2

This commit is contained in:
sawka 2024-12-05 13:46:38 -08:00
commit a6f6640ae1
27 changed files with 785 additions and 49 deletions

View File

@ -10,7 +10,7 @@ on:
- "v[0-9]+.[0-9]+.[0-9]+*"
workflow_dispatch:
env:
GO_VERSION: "1.22"
GO_VERSION: "1.23"
NODE_VERSION: 22
jobs:
build-app:

View File

@ -23,7 +23,7 @@ on:
workflow_dispatch: null
env:
GO_VERSION: "1.22"
GO_VERSION: "1.23"
NODE_VERSION: 22
permissions:

View File

@ -143,6 +143,7 @@ func beforeSendActivityUpdate(ctx context.Context) {
activity.NumWindows, _ = wstore.DBGetCount[*waveobj.Window](ctx)
activity.NumSSHConn = conncontroller.GetNumSSHHasConnected()
activity.NumWSLConn = wsl.GetNumWSLHasConnected()
activity.NumWSNamed, activity.NumWS, _ = wstore.DBGetWSCounts(ctx)
err := telemetry.UpdateActivity(ctx, activity)
if err != nil {
log.Printf("error updating before activity: %v\n", err)

147
cmd/wsh/cmd/csscolormap.go Normal file
View File

@ -0,0 +1,147 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package cmd
var CssColorNames = map[string]bool{
"aliceblue": true,
"antiquewhite": true,
"aqua": true,
"aquamarine": true,
"azure": true,
"beige": true,
"bisque": true,
"black": true,
"blanchedalmond": true,
"blue": true,
"blueviolet": true,
"brown": true,
"burlywood": true,
"cadetblue": true,
"chartreuse": true,
"chocolate": true,
"coral": true,
"cornflowerblue": true,
"cornsilk": true,
"crimson": true,
"cyan": true,
"darkblue": true,
"darkcyan": true,
"darkgoldenrod": true,
"darkgray": true,
"darkgreen": true,
"darkkhaki": true,
"darkmagenta": true,
"darkolivegreen": true,
"darkorange": true,
"darkorchid": true,
"darkred": true,
"darksalmon": true,
"darkseagreen": true,
"darkslateblue": true,
"darkslategray": true,
"darkturquoise": true,
"darkviolet": true,
"deeppink": true,
"deepskyblue": true,
"dimgray": true,
"dodgerblue": true,
"firebrick": true,
"floralwhite": true,
"forestgreen": true,
"fuchsia": true,
"gainsboro": true,
"ghostwhite": true,
"gold": true,
"goldenrod": true,
"gray": true,
"green": true,
"greenyellow": true,
"honeydew": true,
"hotpink": true,
"indianred": true,
"indigo": true,
"ivory": true,
"khaki": true,
"lavender": true,
"lavenderblush": true,
"lawngreen": true,
"lemonchiffon": true,
"lightblue": true,
"lightcoral": true,
"lightcyan": true,
"lightgoldenrodyellow": true,
"lightgray": true,
"lightgreen": true,
"lightpink": true,
"lightsalmon": true,
"lightseagreen": true,
"lightskyblue": true,
"lightslategray": true,
"lightsteelblue": true,
"lightyellow": true,
"lime": true,
"limegreen": true,
"linen": true,
"magenta": true,
"maroon": true,
"mediumaquamarine": true,
"mediumblue": true,
"mediumorchid": true,
"mediumpurple": true,
"mediumseagreen": true,
"mediumslateblue": true,
"mediumspringgreen": true,
"mediumturquoise": true,
"mediumvioletred": true,
"midnightblue": true,
"mintcream": true,
"mistyrose": true,
"moccasin": true,
"navajowhite": true,
"navy": true,
"oldlace": true,
"olive": true,
"olivedrab": true,
"orange": true,
"orangered": true,
"orchid": true,
"palegoldenrod": true,
"palegreen": true,
"paleturquoise": true,
"palevioletred": true,
"papayawhip": true,
"peachpuff": true,
"peru": true,
"pink": true,
"plum": true,
"powderblue": true,
"purple": true,
"red": true,
"rosybrown": true,
"royalblue": true,
"saddlebrown": true,
"salmon": true,
"sandybrown": true,
"seagreen": true,
"seashell": true,
"sienna": true,
"silver": true,
"skyblue": true,
"slateblue": true,
"slategray": true,
"snow": true,
"springgreen": true,
"steelblue": true,
"tan": true,
"teal": true,
"thistle": true,
"tomato": true,
"turquoise": true,
"violet": true,
"wheat": true,
"white": true,
"whitesmoke": true,
"yellow": true,
"yellowgreen": true,
}

186
cmd/wsh/cmd/wshcmd-setbg.go Normal file
View File

@ -0,0 +1,186 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package cmd
import (
"encoding/hex"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/spf13/cobra"
"github.com/wavetermdev/waveterm/pkg/util/utilfn"
"github.com/wavetermdev/waveterm/pkg/wavebase"
"github.com/wavetermdev/waveterm/pkg/wshrpc"
"github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient"
)
var setBgCmd = &cobra.Command{
Use: "setbg [--opacity value] [--tile|--center] [--scale value] (image-path|\"#color\"|color-name)",
Short: "set background image or color for a tab",
Long: `Set a background image or color for a tab. Colors can be specified as:
- A quoted hex value like "#ff0000" (quotes required to prevent # being interpreted as a shell comment)
- A CSS color name like "blue" or "forestgreen"
Or provide a path to a supported image file (jpg, png, gif, webp, or svg).
You can also:
- Use --clear to remove the background
- Use --opacity without other arguments to change just the opacity
- Use --center for centered images without scaling (good for logos)
- Use --scale with --center to control image size
- Use --print to see the metadata without applying it`,
RunE: setBgRun,
PreRunE: preRunSetupRpcClient,
}
var (
setBgOpacity float64
setBgTile bool
setBgCenter bool
setBgSize string
setBgClear bool
setBgPrint bool
)
func init() {
rootCmd.AddCommand(setBgCmd)
setBgCmd.Flags().Float64Var(&setBgOpacity, "opacity", 0.5, "background opacity (0.0-1.0)")
setBgCmd.Flags().BoolVar(&setBgTile, "tile", false, "tile the background image")
setBgCmd.Flags().BoolVar(&setBgCenter, "center", false, "center the image without scaling")
setBgCmd.Flags().StringVar(&setBgSize, "size", "auto", "size for centered images (px, %, or auto)")
setBgCmd.Flags().BoolVar(&setBgClear, "clear", false, "clear the background")
setBgCmd.Flags().BoolVar(&setBgPrint, "print", false, "print the metadata without applying it")
// Make tile and center mutually exclusive
setBgCmd.MarkFlagsMutuallyExclusive("tile", "center")
}
func validateHexColor(color string) error {
if !strings.HasPrefix(color, "#") {
return fmt.Errorf("color must start with #")
}
colorHex := color[1:]
if len(colorHex) != 6 && len(colorHex) != 8 {
return fmt.Errorf("color must be in #RRGGBB or #RRGGBBAA format")
}
_, err := hex.DecodeString(colorHex)
if err != nil {
return fmt.Errorf("invalid hex color: %v", err)
}
return nil
}
func setBgRun(cmd *cobra.Command, args []string) (rtnErr error) {
defer func() {
sendActivity("setbg", rtnErr == nil)
}()
// Create base metadata
meta := map[string]interface{}{}
// Handle opacity-only change or clear
if len(args) == 0 {
if !cmd.Flags().Changed("opacity") && !setBgClear {
OutputHelpMessage(cmd)
return fmt.Errorf("setbg requires an image path or color value")
}
if setBgOpacity < 0 || setBgOpacity > 1 {
return fmt.Errorf("opacity must be between 0.0 and 1.0")
}
if cmd.Flags().Changed("opacity") {
meta["bg:opacity"] = setBgOpacity
}
} else if len(args) > 1 {
OutputHelpMessage(cmd)
return fmt.Errorf("too many arguments")
} else {
// Handle background setting
meta["bg:*"] = true
if setBgOpacity < 0 || setBgOpacity > 1 {
return fmt.Errorf("opacity must be between 0.0 and 1.0")
}
meta["bg:opacity"] = setBgOpacity
input := args[0]
var bgStyle string
// Check for hex color
if strings.HasPrefix(input, "#") {
if err := validateHexColor(input); err != nil {
return err
}
bgStyle = input
} else if CssColorNames[strings.ToLower(input)] {
// Handle CSS color name
bgStyle = strings.ToLower(input)
} else {
// Handle image input
absPath, err := filepath.Abs(wavebase.ExpandHomeDirSafe(input))
if err != nil {
return fmt.Errorf("resolving image path: %v", err)
}
fileInfo, err := os.Stat(absPath)
if err != nil {
return fmt.Errorf("cannot access image file: %v", err)
}
if fileInfo.IsDir() {
return fmt.Errorf("path is a directory, not an image file")
}
mimeType := utilfn.DetectMimeType(absPath, fileInfo, true)
switch mimeType {
case "image/jpeg", "image/png", "image/gif", "image/webp", "image/svg+xml":
// Valid image type
default:
return fmt.Errorf("file does not appear to be a valid image (detected type: %s)", mimeType)
}
// Create URL-safe path
escapedPath := strings.ReplaceAll(absPath, "'", "\\'")
bgStyle = fmt.Sprintf("url('%s')", escapedPath)
switch {
case setBgTile:
bgStyle += " repeat"
case setBgCenter:
bgStyle += fmt.Sprintf(" no-repeat center/%s", setBgSize)
default:
bgStyle += " center/cover no-repeat"
}
}
meta["bg"] = bgStyle
}
if setBgPrint {
jsonBytes, err := json.MarshalIndent(meta, "", " ")
if err != nil {
return fmt.Errorf("error formatting metadata: %v", err)
}
WriteStdout(string(jsonBytes) + "\n")
return nil
}
// Resolve tab reference
oRef, err := resolveSimpleId("tab")
if err != nil {
return err
}
// Send RPC request
setMetaWshCmd := wshrpc.CommandSetMetaData{
ORef: *oRef,
Meta: meta,
}
err = wshclient.SetMetaCommand(RpcClient, setMetaWshCmd, &wshrpc.RpcOpts{Timeout: 2000})
if err != nil {
return fmt.Errorf("setting background: %v", err)
}
WriteStdout("background set\n")
return nil
}

View File

@ -65,6 +65,41 @@ You can also suppress the help widgets in the bottom right by setting the config
<div style={{ clear: "both" }} />
## Tab Backgrounds
Wave supports powerful custom backgrounds for your tabs using images, patterns, gradients, and colors. The quickest way to set an image background is using the `wsh setbg` command:
```bash
# Set an image background with 50% opacity (default)
wsh setbg ~/pictures/background.jpg
# Set a color background (use quotes to prevent # being interpreted as a shell comment)
wsh setbg "#ff0000" # hex color
wsh setbg forestgreen # CSS color name
# Adjust opacity
wsh setbg --opacity 0.3 ~/pictures/light-pattern.png
wsh setbg --opacity 0.7 # change only opacity of current background
# Image positioning options
wsh setbg --tile ~/pictures/texture.png # create tiled pattern
wsh setbg --center ~/pictures/logo.png # center without scaling
wsh setbg --center --size 200px ~/pictures/logo.png # center with specific size (px, %, auto)
# Remove background
wsh setbg --clear
```
You can use any JPEG, PNG, GIF, WebP, or SVG image as your background. The `--center` option is particularly useful for logos or icons where you want to maintain the original size.
To preview the metadata for any background without applying it, use the `--print` flag:
```bash
wsh setbg --print "#ff0000"
```
For more advanced customization options including gradients, colors, and saving your own background presets, check out our [Background Configuration](/presets#background-configurations) documentation.
## Presets
For more advanced customization, to set up multiple AI models, and your own tab backgrounds, check out our [Presets Documentation](./presets).

View File

@ -18,7 +18,7 @@ wsh editconfig presets.json
:::
### File format
## File format
Presets follow the following format:
@ -61,7 +61,7 @@ A complete example of a preset for an AI model is the following:
}
```
### Preset type
## Preset Types
The type of the preset determines where it can be discovered in the app. Currently, the two types that will be discovered in the app are `bg` and `ai`.
@ -90,7 +90,7 @@ Configs in a preset are applied in order to override the default config values,
:::
#### AI configurations
## AI Configurations
| Key Name | Type | Function |
| ------------- | ------ | -------------------------------------------------------------------------------------------------- |
@ -106,16 +106,105 @@ Configs in a preset are applied in order to override the default config values,
| ai:maxtokens | int | max tokens to pass to API |
| ai:timeoutms | int | timeout (in milliseconds) for AI calls |
#### Background configurations
<a name="background-configurations" />
## Background Configurations
Wave's background system harnesses the full power of CSS backgrounds, letting you create rich visual effects through the "background" attribute. You can apply solid colors, gradients (both linear and radial), images, and even blend multiple elements together. The system offers preset configurations while maintaining the flexibility of pure CSS, making it both powerful and easy to use.
| Key Name | Type | Function |
| -------------------- | ------ | ----------------------------------------------------------------------------------------------- |
| bg:\* | bool | reset all existing bg keys |
| bg:opacity | float | the opacity of the background |
| -------------------- | ------ | ------------------------------------------------------------------------------------------------------- |
| bg:\* | bool | reset all existing bg keys (recommended to prevent any existing background settings from carrying over) |
| bg | string | CSS `background` attribute for the tab (supports colors, gradients images, etc.) |
| bg:opacity | float | the opacity of the background (defaults to 0.5) |
| bg:blendmode | string | the [blend mode](https://developer.mozilla.org/en-US/docs/Web/CSS/blend-mode) of the background |
| bg:bordercolor | string | the color of the border |
| bg:bordercolor | string | the color of the border when a block is not active (rarely used) |
| bg:activebordercolor | string | the color of the border when a block is active |
### Simple solid color with opacity:
```json
{
"bg@blue": {
"display:name": "Blue",
"bg:*": true,
"bg": "blue",
"bg:opacity": 0.3,
"bg:activebordercolor": "rgba(0, 0, 255, 1.0)"
}
}
```
### Complex gradient combining multiple effects:
```json
{
"bg@duskhorizon": {
"display:name": "Dusk Horizon",
"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"
}
}
```
### Repeating pattern background:
```json
{
"bg@pattern": {
"display:name": "Diamond Pattern",
"bg:*": true,
"bg": "url('/path/to/pattern.png') repeat",
"bg:opacity": 0.15
}
}
```
### Full-cover background image:
```json
{
"bg@cover": {
"display:name": "Ocean Scene",
"bg:*": true,
"bg": "url('/path/to/ocean.jpg') center/cover no-repeat",
"bg:opacity": 0.2
}
}
```
Replace image URLs with your own assets. All examples use reduced opacity to work well with dark themes.
:::info
Background images can be specified using URLs or local file paths. While URLs are supported, it's recommended to download and serve images locally for better reliability. For local files, you must use absolute paths or paths starting with `~` (e.g. `~/Downloads/background.png`). The system will automatically rewrite local paths to ensure proper access. We support all common web image formats: PNG, JPEG/JPG, WebP, GIF, and SVG.
:::
:::tip
You can use `wsh setbg --print` to help generate the JSON for your background presets. For example:
```bash
# Preview the metadata for a gradient background
wsh setbg --print "#ff0000"
{
"bg:*": true,
"bg": "#ff0000",
"bg:opacity": 0.5
}
# Preview a centered logo configuration
wsh setbg --print --center --opacity 0.3 ~/logo.png
{
"bg:*": true,
"bg": "url('/absolute/path/to/logo.png') no-repeat center/auto",
"bg:opacity": 0.3
}
```
Copy the output and use it as a starting point for your preset configuration, just add the required `display:name` field!
:::
#### Unset a default value
To unset a default value in a preset, add an override that sets it to an empty string, like `""`.

View File

@ -51,12 +51,15 @@ When telemetry is active, we collect the following data. It is stored in the `te
| NumTabs | The number of existing tabs open on a given day. |
| NewTab | The number of new tabs created on a given day |
| NumWindows | The number of existing windows open on a given day. |
| NumWS | The number of existing workspaces on a given day. |
| NumWSNamed | The number of named workspaces on a give day. |
| NewTab | The number of new tabs opened on a given day. |
| NumStartup | The number of times waveterm has been started on a given day. |
| NumShutdown | The number of times waveterm has been shut down on a given day. |
| SetTabTheme | The number of times the tab theme is changed from the context menu |
| NumMagnify | The number of times any block is magnified |
| NumPanics | The number of backend (golang) panics caught in the current day |
| NumAIReqs | The number of AI requests made in the current day |
| NumSSHConn | The number of distinct SSH connections that have been made to distinct hosts |
| NumWSLConns | The number of distinct WSL connections that have been made to distinct distros |
| Renderers | The number of new block views of each type are open on a given day. |

View File

@ -145,6 +145,74 @@ wsh editconfig widgets.json
---
## setbg
The `setbg` command allows you to set a background image or color for the current tab with various customization options.
```bash
wsh setbg [--opacity value] [--tile|--center] [--size value] (image-path|"#color"|color-name)
```
You can set a background using:
- An image file (displayed as cover, tiled, or centered)
- A hex color (must be quoted like "#ff0000")
- A CSS color name (like "blue" or "forestgreen")
Flags:
- `--opacity value` - set the background opacity (0.0-1.0, default 0.5)
- `--tile` - tile the background image instead of using cover mode
- `--center` - center the image without scaling (good for logos)
- `--size` - size for centered images (px, %, or auto)
- `--clear` - remove the background
- `--print` - show the metadata without applying it
Supported image formats: JPEG, PNG, GIF, WebP, and SVG.
Examples:
```bash
# Set an image background with default settings
wsh setbg ~/pictures/background.jpg
# Set a background with custom opacity
wsh setbg --opacity 0.3 ~/pictures/light-pattern.png
# Set a tiled background
wsh setbg --tile --opacity 0.2 ~/pictures/texture.png
# Center an image (good for logos)
wsh setbg --center ~/pictures/logo.png
wsh setbg --center --size 200px ~/pictures/logo.png
# Set color backgrounds
wsh setbg "#ff0000" # hex color (requires quotes)
wsh setbg forestgreen # CSS color name
# Change just the opacity of current background
wsh setbg --opacity 0.7
# Remove background
wsh setbg --clear
# Preview the metadata
wsh setbg --print "#ff0000"
```
The command validates that:
- Color values are valid hex codes or CSS color names
- Image paths point to accessible, supported image files
- The opacity value is between 0.0 and 1.0
- The center and tile options are not used together
:::tip
Use `--print` to preview the metadata for any background configuration without applying it. You can then copy this JSON representation to use as a [Background Preset](/presets#background-configurations)
:::
---
## run
The `run` command creates a new terminal command block and executes a specified command within it. The command can be provided either as arguments after `--` or using the `-c` flag. Unless the `-x` or `-X` flags are passed, commands can be re-executed by pressing `Enter` once the command has finished running.

View File

@ -410,7 +410,6 @@ electron.ipcMain.on("set-window-init-status", (event, status: "ready" | "wave-re
return;
}
if (status === "ready") {
console.log("initResolve");
tabView.initResolve();
if (tabView.savedInitOpts) {
console.log("savedInitOpts");
@ -419,7 +418,6 @@ electron.ipcMain.on("set-window-init-status", (event, status: "ready" | "wave-re
console.log("no-savedInitOpts");
}
} else if (status === "wave-ready") {
console.log("waveReadyResolve");
tabView.waveReadyResolve();
}
});

View File

@ -15,6 +15,8 @@ import { TypeAheadModal } from "@/app/modals/typeaheadmodal";
import { ContextMenuModel } from "@/app/store/contextmenu";
import {
atoms,
createBlock,
getApi,
getBlockComponentModel,
getConnStatusAtom,
getHostName,
@ -182,6 +184,9 @@ const BlockFrame_Header = ({
const prevMagifiedState = React.useRef(magnified);
const manageConnection = util.useAtomValueSafe(viewModel?.manageConnection);
const dragHandleRef = preview ? null : nodeModel.dragHandleRef;
const connName = blockData?.meta?.connection;
const allSettings = jotai.useAtomValue(atoms.fullConfigAtom);
const wshEnabled = allSettings?.connections?.[connName]?.["conn:wshenabled"] ?? true;
React.useEffect(() => {
if (!magnified || preview || prevMagifiedState.current) {
@ -239,6 +244,11 @@ const BlockFrame_Header = ({
</div>
);
}
const wshInstallButton: IconButtonDecl = {
elemtype: "iconbutton",
icon: "link-slash",
title: "wsh is not installed for this connection",
};
return (
<div className="block-frame-default-header" ref={dragHandleRef} onContextMenu={onContextMenu}>
@ -256,6 +266,9 @@ const BlockFrame_Header = ({
changeConnModalAtom={changeConnModalAtom}
/>
)}
{manageConnection && !wshEnabled && (
<IconButton decl={wshInstallButton} className="block-frame-header-iconbutton" />
)}
<div className="block-frame-textelems-wrapper">{headerTextElems}</div>
<div className="block-frame-end-icons">{endIconsElem}</div>
</div>
@ -568,6 +581,10 @@ const ChangeConnectionBlockModal = React.memo(
const allConnStatus = jotai.useAtomValue(atoms.allConnStatus);
const [rowIndex, setRowIndex] = React.useState(0);
const connStatusMap = new Map<string, ConnStatus>();
const fullConfig = jotai.useAtomValue(atoms.fullConfigAtom);
const connectionsConfig = fullConfig.connections;
let filterOutNowsh = util.useAtomValueSafe(viewModel.filterOutNowsh) || true;
let maxActiveConnNum = 1;
for (const conn of allConnStatus) {
if (conn.activeconnnum > maxActiveConnNum) {
@ -638,7 +655,12 @@ const ChangeConnectionBlockModal = React.memo(
if (conn === connSelected) {
createNew = false;
}
if (conn.includes(connSelected)) {
if (
conn.includes(connSelected) &&
connectionsConfig[conn]?.["display:hidden"] != true &&
(connectionsConfig[conn]?.["conn:wshenabled"] != false || !filterOutNowsh)
// != false is necessary because of defaults
) {
filteredList.push(conn);
}
}
@ -647,7 +669,12 @@ const ChangeConnectionBlockModal = React.memo(
if (conn === connSelected) {
createNew = false;
}
if (conn.includes(connSelected)) {
if (
conn.includes(connSelected) &&
connectionsConfig[conn]?.["display:hidden"] != true &&
(connectionsConfig[conn]?.["conn:wshenabled"] != false || !filterOutNowsh)
// != false is necessary because of defaults
) {
filteredWslList.push(conn);
}
}
@ -734,9 +761,38 @@ const ChangeConnectionBlockModal = React.memo(
};
return item;
});
const connectionsEditItem: SuggestionConnectionItem = {
status: "disconnected",
icon: "gear",
iconColor: "var(--grey-text-color",
value: "Edit Connections",
label: "Edit Connections",
onSelect: () => {
util.fireAndForget(async () => {
globalStore.set(changeConnModalAtom, false);
const path = `${getApi().getConfigDir()}/connections.json`;
const blockDef: BlockDef = {
meta: {
view: "preview",
file: path,
},
};
await createBlock(blockDef, false, true);
});
},
};
const sortedRemoteItems = remoteItems.sort(
(itemA: SuggestionConnectionItem, itemB: SuggestionConnectionItem) => {
const connNameA = itemA.value;
const connNameB = itemB.value;
const valueA = connectionsConfig[connNameA]?.["display:order"] ?? 0;
const valueB = connectionsConfig[connNameB]?.["display:order"] ?? 0;
return valueA - valueB;
}
);
const remoteSuggestions: SuggestionConnectionScope = {
headerText: "Remote",
items: remoteItems,
items: [...sortedRemoteItems, connectionsEditItem],
};
let suggestions: Array<SuggestionsType> = [];

View File

@ -248,6 +248,21 @@ function useBlockMetaKeyAtom<T extends keyof MetaType>(blockId: string, key: T):
return useAtomValue(getBlockMetaKeyAtom(blockId, key));
}
function getConnConfigKeyAtom<T extends keyof ConnKeywords>(connName: string, key: T): Atom<ConnKeywords[T]> {
let connCache = getSingleConnAtomCache(connName);
const keyAtomName = "#conn-" + key;
let keyAtom = connCache.get(keyAtomName);
if (keyAtom != null) {
return keyAtom;
}
keyAtom = atom((get) => {
let fullConfig = get(atoms.fullConfigAtom);
return fullConfig.connections?.[connName]?.[key];
});
connCache.set(keyAtomName, keyAtom);
return keyAtom;
}
const settingsAtomCache = new Map<string, Atom<any>>();
function getOverrideConfigAtom<T extends keyof SettingsType>(blockId: string, key: T): Atom<SettingsType[T]> {
@ -263,6 +278,13 @@ function getOverrideConfigAtom<T extends keyof SettingsType>(blockId: string, ke
if (metaKeyVal != null) {
return metaKeyVal;
}
const connNameAtom = getBlockMetaKeyAtom(blockId, "connection");
const connName = get(connNameAtom);
const connConfigKeyAtom = getConnConfigKeyAtom(connName, key as any);
const connConfigKeyVal = get(connConfigKeyAtom);
if (connConfigKeyVal != null) {
return connConfigKeyVal;
}
const settingsKeyAtom = getSettingsKeyAtom(key);
const settingsVal = get(settingsKeyAtom);
if (settingsVal != null) {
@ -324,6 +346,15 @@ function getSingleBlockAtomCache(blockId: string): Map<string, Atom<any>> {
return blockCache;
}
function getSingleConnAtomCache(connName: string): Map<string, Atom<any>> {
let blockCache = blockAtomCache.get(connName);
if (blockCache == null) {
blockCache = new Map<string, Atom<any>>();
blockAtomCache.set(connName, blockCache);
}
return blockCache;
}
function useBlockAtom<T>(blockId: string, name: string, makeFn: () => Atom<T>): Atom<T> {
const blockCache = getSingleBlockAtomCache(blockId);
let atom = blockCache.get(name);

View File

@ -110,7 +110,13 @@ const ColorAndIconSelector = memo(
return (
<div className="color-icon-selector">
<Input ref={inputRef} className="vertical-padding-3" onChange={onTitleChange} value={title} autoFocus />
<Input
ref={inputRef}
className={clsx("vertical-padding-3", { error: title === "" })}
onChange={onTitleChange}
value={title}
autoFocus
/>
<ColorSelector
selectedColor={color}
colors={["#e91e63", "#8bc34a", "#ff9800", "#ffc107", "#03a9f4", "#3f51b5", "#f44336"]}
@ -258,7 +264,9 @@ const WorkspaceSwitcherItem = ({
const setWorkspace = useCallback((newWorkspace: Workspace) => {
fireAndForget(async () => {
if (newWorkspace.name != "") {
setObjectValue({ ...newWorkspace, otype: "workspace" }, undefined, true);
}
setWorkspaceEntry({ ...workspaceEntry, workspace: newWorkspace });
});
}, []);

View File

@ -121,6 +121,7 @@ export class PreviewModel implements ViewModel {
loadableSpecializedView: Atom<Loadable<{ specializedView?: string; errorStr?: string }>>;
manageConnection: Atom<boolean>;
connStatus: Atom<ConnStatus>;
filterOutNowsh?: Atom<boolean>;
metaFilePath: Atom<string>;
statFilePath: Atom<Promise<string>>;
@ -164,6 +165,7 @@ export class PreviewModel implements ViewModel {
this.manageConnection = atom(true);
this.blockAtom = WOS.getWaveObjectAtom<Block>(`block:${blockId}`);
this.markdownShowToc = atom(false);
this.filterOutNowsh = atom(true);
this.monacoRef = createRef();
this.viewIcon = atom((get) => {
const blockData = get(this.blockAtom);

View File

@ -91,7 +91,7 @@ function convertWaveEventToDataItem(event: WaveEvent): DataItem {
return dataItem;
}
class SysinfoViewModel {
class SysinfoViewModel implements ViewModel {
viewType: string;
blockAtom: jotai.Atom<Block>;
termMode: jotai.Atom<string>;
@ -109,6 +109,7 @@ class SysinfoViewModel {
metrics: jotai.Atom<string[]>;
connection: jotai.Atom<string>;
manageConnection: jotai.Atom<boolean>;
filterOutNowsh: jotai.Atom<boolean>;
connStatus: jotai.Atom<ConnStatus>;
plotMetaAtom: jotai.PrimitiveAtom<Map<string, TimeSeriesMeta>>;
endIconButtons: jotai.Atom<IconButtonDecl[]>;
@ -176,6 +177,7 @@ class SysinfoViewModel {
});
this.plotMetaAtom = jotai.atom(new Map(Object.entries(DefaultPlotMeta)));
this.manageConnection = jotai.atom(true);
this.filterOutNowsh = jotai.atom(true);
this.loadingAtom = jotai.atom(true);
this.numPoints = jotai.atom((get) => {
const blockData = get(this.blockAtom);

View File

@ -41,7 +41,7 @@ type InitialLoadDataType = {
heldData: Uint8Array[];
};
class TermViewModel {
class TermViewModel implements ViewModel {
viewType: string;
nodeModel: BlockNodeModel;
connected: boolean;
@ -54,6 +54,7 @@ class TermViewModel {
viewText: jotai.Atom<HeaderElem[]>;
blockBg: jotai.Atom<MetaType>;
manageConnection: jotai.Atom<boolean>;
filterOutNowsh?: jotai.Atom<boolean>;
connStatus: jotai.Atom<ConnStatus>;
termWshClient: TermWshClient;
vdomBlockId: jotai.Atom<string>;
@ -196,6 +197,7 @@ class TermViewModel {
}
return true;
});
this.filterOutNowsh = jotai.atom(false);
this.termThemeNameAtom = useBlockAtom(blockId, "termthemeatom", () => {
return jotai.atom<string>((get) => {
return get(getOverrideConfigAtom(this.blockId, "term:theme")) ?? DefaultTermTheme;
@ -221,7 +223,10 @@ class TermViewModel {
const blockData = get(this.blockAtom);
const fsSettingsAtom = getSettingsKeyAtom("term:fontsize");
const settingsFontSize = get(fsSettingsAtom);
const rtnFontSize = blockData?.meta?.["term:fontsize"] ?? settingsFontSize ?? 12;
const connName = blockData?.meta?.connection;
const fullConfig = get(atoms.fullConfigAtom);
const connFontSize = fullConfig?.connections?.[connName]?.["term:fontsize"];
const rtnFontSize = blockData?.meta?.["term:fontsize"] ?? connFontSize ?? settingsFontSize ?? 12;
if (typeof rtnFontSize != "number" || isNaN(rtnFontSize) || rtnFontSize < 4 || rtnFontSize > 64) {
return 12;
}
@ -725,6 +730,8 @@ const TerminalView = ({ blockId, model }: TerminalViewProps) => {
const termModeRef = React.useRef(termMode);
const termFontSize = jotai.useAtomValue(model.fontSizeAtom);
const fullConfig = globalStore.get(atoms.fullConfigAtom);
const connFontFamily = fullConfig.connections?.[blockData?.meta?.connection]?.["term:fontfamily"];
React.useEffect(() => {
const fullConfig = globalStore.get(atoms.fullConfigAtom);
@ -750,7 +757,7 @@ const TerminalView = ({ blockId, model }: TerminalViewProps) => {
{
theme: termTheme,
fontSize: termFontSize,
fontFamily: termSettings?.["term:fontfamily"] ?? "Hack",
fontFamily: termSettings?.["term:fontfamily"] ?? connFontFamily ?? "Hack",
drawBoldTextInBrightColors: false,
fontWeight: "normal",
fontWeightBold: "bold",
@ -784,7 +791,7 @@ const TerminalView = ({ blockId, model }: TerminalViewProps) => {
termWrap.dispose();
rszObs.disconnect();
};
}, [blockId, termSettings, termFontSize]);
}, [blockId, termSettings, termFontSize, connFontFamily]);
React.useEffect(() => {
if (termModeRef.current == "vdom" && termMode == "term") {

View File

@ -238,6 +238,7 @@ declare global {
blockBg?: jotai.Atom<MetaType>;
manageConnection?: jotai.Atom<boolean>;
noPadding?: jotai.Atom<boolean>;
filterOutNowsh?: jotai.Atom<boolean>;
onBack?: () => void;
onForward?: () => void;

View File

@ -22,10 +22,13 @@ declare global {
newtab?: number;
numblocks?: number;
numwindows?: number;
numws?: number;
numwsnamed?: number;
numsshconn?: number;
numwslconn?: number;
nummagnify?: number;
numpanics?: number;
numaireqs?: number;
startup?: number;
shutdown?: number;
settabtheme?: number;
@ -277,8 +280,14 @@ declare global {
// wshrpc.ConnKeywords
type ConnKeywords = {
wshenabled?: boolean;
askbeforewshinstall?: boolean;
"conn:wshenabled"?: boolean;
"conn:askbeforewshinstall"?: boolean;
"display:hidden"?: boolean;
"display:order"?: number;
"term:*"?: boolean;
"term:fontsize"?: number;
"term:fontfamily"?: string;
"term:theme"?: string;
"ssh:user"?: string;
"ssh:hostname"?: string;
"ssh:port"?: string;
@ -1121,9 +1130,9 @@ declare global {
// waveobj.Workspace
type Workspace = WaveObj & {
name: string;
icon: string;
color: string;
name?: string;
icon?: string;
color?: string;
tabids: string[];
pinnedtabids: string[];
activetabid: string;

2
go.mod
View File

@ -1,6 +1,6 @@
module github.com/wavetermdev/waveterm
go 1.22.4
go 1.23.4
require (
github.com/alexflint/go-filemutex v1.3.0

View File

@ -342,7 +342,7 @@ func (conn *SSHConn) CheckAndInstallWsh(ctx context.Context, clientDisplayName s
}
if !response.Confirm {
meta := make(map[string]any)
meta["wshenabled"] = false
meta["conn:wshenabled"] = false
err = wconfig.SetConnectionsConfigValue(conn.GetName(), meta)
if err != nil {
log.Printf("warning: error writing to connections file: %v", err)
@ -454,9 +454,41 @@ func (conn *SSHConn) Connect(ctx context.Context, connFlags *wshrpc.ConnKeywords
}
})
conn.FireConnChangeEvent()
if err != nil {
return err
}
// logic for saving connection and potential flags (we only save once a connection has been made successfully)
// at the moment, identity files is the only saved flag
var identityFiles []string
existingConfig := wconfig.ReadFullConfig()
existingConnection, ok := existingConfig.Connections[conn.GetName()]
if ok {
identityFiles = existingConnection.SshIdentityFile
}
if err != nil {
// i do not consider this a critical failure
log.Printf("config read error: unable to save connection %s: %v", conn.GetName(), err)
}
meta := make(map[string]any)
if connFlags.SshIdentityFile != nil {
for _, identityFile := range connFlags.SshIdentityFile {
if utilfn.ContainsStr(identityFiles, identityFile) {
continue
}
identityFiles = append(identityFiles, connFlags.SshIdentityFile...)
}
meta["ssh:identityfile"] = identityFiles
}
err = wconfig.SetConnectionsConfigValue(conn.GetName(), meta)
if err != nil {
// i do not consider this a critical failure
log.Printf("config write error: unable to save connection %s: %v", conn.GetName(), err)
}
return nil
}
func (conn *SSHConn) WithLock(fn func()) {
conn.Lock.Lock()
defer conn.Lock.Unlock()
@ -484,11 +516,11 @@ func (conn *SSHConn) connectInternal(ctx context.Context, connFlags *wshrpc.Conn
askBeforeInstall := config.Settings.ConnAskBeforeWshInstall
connSettings, ok := config.Connections[conn.GetName()]
if ok {
if connSettings.WshEnabled != nil {
enableWsh = *connSettings.WshEnabled
if connSettings.ConnWshEnabled != nil {
enableWsh = *connSettings.ConnWshEnabled
}
if connSettings.AskBeforeWshInstall != nil {
askBeforeInstall = *connSettings.AskBeforeWshInstall
if connSettings.ConnAskBeforeWshInstall != nil {
askBeforeInstall = *connSettings.ConnAskBeforeWshInstall
}
}
if enableWsh {
@ -661,6 +693,9 @@ func GetConnectionsList() ([]string, error) {
hasConnected = append(hasConnected, stat.Connection)
}
}
fromInternal := GetConnectionsFromInternalConfig()
fromConfig, err := GetConnectionsFromConfig()
if err != nil {
return nil, err
@ -670,7 +705,7 @@ func GetConnectionsList() ([]string, error) {
alreadyUsed := make(map[string]struct{})
var connList []string
for _, subList := range [][]string{currentlyRunning, hasConnected, fromConfig} {
for _, subList := range [][]string{currentlyRunning, hasConnected, fromInternal, fromConfig} {
for _, pattern := range subList {
if _, used := alreadyUsed[pattern]; !used {
connList = append(connList, pattern)
@ -682,6 +717,15 @@ func GetConnectionsList() ([]string, error) {
return connList, nil
}
func GetConnectionsFromInternalConfig() []string {
var internalNames []string
config := wconfig.ReadFullConfig()
for internalName := range config.Connections {
internalNames = append(internalNames, internalName)
}
return internalNames
}
func GetConnectionsFromConfig() ([]string, error) {
home := wavebase.GetHomeDir()
localConfig := filepath.Join(home, ".ssh", "config")

View File

@ -27,6 +27,7 @@ import (
"github.com/wavetermdev/waveterm/pkg/userinput"
"github.com/wavetermdev/waveterm/pkg/util/shellutil"
"github.com/wavetermdev/waveterm/pkg/wavebase"
"github.com/wavetermdev/waveterm/pkg/wconfig"
"github.com/wavetermdev/waveterm/pkg/wshrpc"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
@ -649,7 +650,13 @@ func ConnectToClient(connCtx context.Context, opts *SSHOpts, currentClient *ssh.
connFlags.SshHostName = opts.SSHHost
connFlags.SshPort = fmt.Sprintf("%d", opts.SSHPort)
sshKeywords, err := combineSshKeywords(connFlags, sshConfigKeywords)
rawName := opts.String()
savedKeywords, ok := wconfig.ReadFullConfig().Connections[rawName]
if !ok {
savedKeywords = wshrpc.ConnKeywords{}
}
sshKeywords, err := combineSshKeywords(connFlags, sshConfigKeywords, &savedKeywords)
if err != nil {
return nil, debugInfo.JumpNum, ConnectionError{ConnectionDebugInfo: debugInfo, Err: err}
}
@ -685,7 +692,7 @@ func ConnectToClient(connCtx context.Context, opts *SSHOpts, currentClient *ssh.
return client, debugInfo.JumpNum, nil
}
func combineSshKeywords(userProvidedOpts *wshrpc.ConnKeywords, configKeywords *wshrpc.ConnKeywords) (*wshrpc.ConnKeywords, error) {
func combineSshKeywords(userProvidedOpts *wshrpc.ConnKeywords, configKeywords *wshrpc.ConnKeywords, savedKeywords *wshrpc.ConnKeywords) (*wshrpc.ConnKeywords, error) {
sshKeywords := &wshrpc.ConnKeywords{}
if userProvidedOpts.SshUser != "" {
@ -716,7 +723,13 @@ func combineSshKeywords(userProvidedOpts *wshrpc.ConnKeywords, configKeywords *w
sshKeywords.SshPort = "22"
}
sshKeywords.SshIdentityFile = append(userProvidedOpts.SshIdentityFile, configKeywords.SshIdentityFile...)
// use internal config ones
if savedKeywords != nil {
sshKeywords.SshIdentityFile = append(sshKeywords.SshIdentityFile, savedKeywords.SshIdentityFile...)
}
sshKeywords.SshIdentityFile = append(sshKeywords.SshIdentityFile, userProvidedOpts.SshIdentityFile...)
sshKeywords.SshIdentityFile = append(sshKeywords.SshIdentityFile, configKeywords.SshIdentityFile...)
// these are not officially supported in the waveterm frontend but can be configured
// in ssh config files

View File

@ -39,6 +39,8 @@ type TelemetryData struct {
NumTabs int `json:"numtabs"`
NumBlocks int `json:"numblocks,omitempty"`
NumWindows int `json:"numwindows,omitempty"`
NumWS int `json:"numws,omitempty"`
NumWSNamed int `json:"numwsnamed,omitempty"`
NumSSHConn int `json:"numsshconn,omitempty"`
NumWSLConn int `json:"numwslconn,omitempty"`
NumMagnify int `json:"nummagnify,omitempty"`
@ -46,6 +48,7 @@ type TelemetryData struct {
NumStartup int `json:"numstartup,omitempty"`
NumShutdown int `json:"numshutdown,omitempty"`
NumPanics int `json:"numpanics,omitempty"`
NumAIReqs int `json:"numaireqs,omitempty"`
SetTabTheme int `json:"settabtheme,omitempty"`
Displays []wshrpc.ActivityDisplayType `json:"displays,omitempty"`
Renderers map[string]int `json:"renderers,omitempty"`
@ -116,6 +119,7 @@ func UpdateActivity(ctx context.Context, update wshrpc.ActivityUpdate) error {
tdata.SetTabTheme += update.SetTabTheme
tdata.NumMagnify += update.NumMagnify
tdata.NumPanics += update.NumPanics
tdata.NumAIReqs += update.NumAIReqs
if update.NumTabs > 0 {
tdata.NumTabs = update.NumTabs
}
@ -125,6 +129,12 @@ func UpdateActivity(ctx context.Context, update wshrpc.ActivityUpdate) error {
if update.NumWindows > 0 {
tdata.NumWindows = update.NumWindows
}
if update.NumWS > 0 {
tdata.NumWS = update.NumWS
}
if update.NumWSNamed > 0 {
tdata.NumWSNamed = update.NumWSNamed
}
if update.NumSSHConn > 0 && update.NumSSHConn > tdata.NumSSHConn {
tdata.NumSSHConn = update.NumSSHConn
}

View File

@ -8,6 +8,7 @@ import (
"log"
"time"
"github.com/wavetermdev/waveterm/pkg/telemetry"
"github.com/wavetermdev/waveterm/pkg/wshrpc"
)
@ -63,6 +64,7 @@ func makeAIError(err error) wshrpc.RespOrErrorUnion[wshrpc.OpenAIPacketType] {
}
func RunAICommand(ctx context.Context, request wshrpc.OpenAiStreamRequest) chan wshrpc.RespOrErrorUnion[wshrpc.OpenAIPacketType] {
telemetry.GoUpdateActivityWrap(wshrpc.ActivityUpdate{NumAIReqs: 1}, "RunAICommand")
if request.Opts.APIType == ApiType_Anthropic {
endpoint := request.Opts.BaseURL
if endpoint == "" {

View File

@ -167,9 +167,9 @@ type ActiveTabUpdate struct {
type Workspace struct {
OID string `json:"oid"`
Version int `json:"version"`
Name string `json:"name"`
Icon string `json:"icon"`
Color string `json:"color"`
Name string `json:"name,omitempty"`
Icon string `json:"icon,omitempty"`
Color string `json:"color,omitempty"`
TabIds []string `json:"tabids"`
PinnedTabIds []string `json:"pinnedtabids"`
ActiveTabId string `json:"activetabid"`

View File

@ -1,3 +0,0 @@
{
"askbeforewshinstall": true
}

View File

@ -452,8 +452,16 @@ type CommandRemoteWriteFileData struct {
}
type ConnKeywords struct {
WshEnabled *bool `json:"wshenabled,omitempty"`
AskBeforeWshInstall *bool `json:"askbeforewshinstall,omitempty"`
ConnWshEnabled *bool `json:"conn:wshenabled,omitempty"`
ConnAskBeforeWshInstall *bool `json:"conn:askbeforewshinstall,omitempty"`
DisplayHidden *bool `json:"display:hidden,omitempty"`
DisplayOrder float32 `json:"display:order,omitempty"`
TermClear bool `json:"term:*,omitempty"`
TermFontSize float64 `json:"term:fontsize,omitempty"`
TermFontFamily string `json:"term:fontfamily,omitempty"`
TermTheme string `json:"term:theme,omitempty"`
SshUser string `json:"ssh:user,omitempty"`
SshHostName string `json:"ssh:hostname,omitempty"`
@ -600,10 +608,13 @@ type ActivityUpdate struct {
NewTab int `json:"newtab,omitempty"`
NumBlocks int `json:"numblocks,omitempty"`
NumWindows int `json:"numwindows,omitempty"`
NumWS int `json:"numws,omitempty"`
NumWSNamed int `json:"numwsnamed,omitempty"`
NumSSHConn int `json:"numsshconn,omitempty"`
NumWSLConn int `json:"numwslconn,omitempty"`
NumMagnify int `json:"nummagnify,omitempty"`
NumPanics int `json:"numpanics,omitempty"`
NumAIReqs int `json:"numaireqs,omitempty"`
Startup int `json:"startup,omitempty"`
Shutdown int `json:"shutdown,omitempty"`
SetTabTheme int `json:"settabtheme,omitempty"`

View File

@ -45,6 +45,22 @@ func DBGetCount[T waveobj.WaveObj](ctx context.Context) (int, error) {
})
}
// returns (num named workespaces, num total workspaces, error)
func DBGetWSCounts(ctx context.Context) (int, int, error) {
var named, total int
err := WithTx(ctx, func(tx *TxWrap) error {
query := `SELECT count(*) FROM db_workspace WHERE COALESCE(json_extract(data, '$.name'), '') <> ''`
named = tx.GetInt(query)
query = `SELECT count(*) FROM db_workspace`
total = tx.GetInt(query)
return nil
})
if err != nil {
return 0, 0, err
}
return named, total, nil
}
var viewRe = regexp.MustCompile(`^[a-z0-9]{1,20}$`)
func DBGetBlockViewCounts(ctx context.Context) (map[string]int, error) {