mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-01-02 18:39:05 +01:00
Merge remote-tracking branch 'origin/main' into red/tabs-new-design-2
This commit is contained in:
commit
a6f6640ae1
2
.github/workflows/build-helper.yml
vendored
2
.github/workflows/build-helper.yml
vendored
@ -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:
|
||||
|
2
.github/workflows/testdriver.yml
vendored
2
.github/workflows/testdriver.yml
vendored
@ -23,7 +23,7 @@ on:
|
||||
workflow_dispatch: null
|
||||
|
||||
env:
|
||||
GO_VERSION: "1.22"
|
||||
GO_VERSION: "1.23"
|
||||
NODE_VERSION: 22
|
||||
|
||||
permissions:
|
||||
|
@ -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
147
cmd/wsh/cmd/csscolormap.go
Normal 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
186
cmd/wsh/cmd/wshcmd-setbg.go
Normal 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
|
||||
}
|
@ -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).
|
||||
|
@ -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 `""`.
|
||||
|
@ -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. |
|
||||
|
@ -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.
|
||||
|
@ -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();
|
||||
}
|
||||
});
|
||||
|
@ -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> = [];
|
||||
|
@ -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);
|
||||
|
@ -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 });
|
||||
});
|
||||
}, []);
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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") {
|
||||
|
1
frontend/types/custom.d.ts
vendored
1
frontend/types/custom.d.ts
vendored
@ -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;
|
||||
|
19
frontend/types/gotypes.d.ts
vendored
19
frontend/types/gotypes.d.ts
vendored
@ -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
2
go.mod
@ -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
|
||||
|
@ -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,7 +454,39 @@ 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()) {
|
||||
@ -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")
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 == "" {
|
||||
|
@ -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"`
|
||||
|
@ -1,3 +0,0 @@
|
||||
{
|
||||
"askbeforewshinstall": true
|
||||
}
|
@ -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"`
|
||||
|
@ -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) {
|
||||
|
Loading…
Reference in New Issue
Block a user