Change presets/bg.json => backgrounds.json, migrate, change tab background to tab:background key (#3108)

also fixes aipanel's border colors
This commit is contained in:
Mike Sawka 2026-03-24 09:00:45 -07:00 committed by GitHub
parent 2b110433d0
commit 645424a8be
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
43 changed files with 738 additions and 477 deletions

View file

@ -30,7 +30,7 @@ Create a narrowing whenever you are writing a component (or group of components)
```ts
import {
BlockMetaKeyAtomFnType, // only if you use getBlockMetaKeyAtom
MetaKeyAtomFnType, // only if you use getBlockMetaKeyAtom or getTabMetaKeyAtom
ConnConfigKeyAtomFnType, // only if you use getConnConfigKeyAtom
SettingsKeyAtomFnType, // only if you use getSettingsKeyAtom
WaveEnv,
@ -77,12 +77,14 @@ export type MyEnv = WaveEnvSubset<{
// --- key-parameterized atom factories: enumerate the keys you use ---
getSettingsKeyAtom: SettingsKeyAtomFnType<"app:focusfollowscursor" | "window:magnifiedblockopacity">;
getBlockMetaKeyAtom: BlockMetaKeyAtomFnType<"view" | "frame:title" | "connection">;
getBlockMetaKeyAtom: MetaKeyAtomFnType<"view" | "frame:title" | "connection">;
getTabMetaKeyAtom: MetaKeyAtomFnType<"tabid" | "name">;
getConnConfigKeyAtom: ConnConfigKeyAtomFnType<"conn:wshenabled">;
// --- other atom helpers: copy verbatim ---
getConnStatusAtom: WaveEnv["getConnStatusAtom"];
getLocalHostDisplayNameAtom: WaveEnv["getLocalHostDisplayNameAtom"];
getConfigBackgroundAtom: WaveEnv["getConfigBackgroundAtom"];
}>;
```
@ -104,7 +106,8 @@ Every `WaveEnvSubset<T>` automatically includes the mock fields — you never ne
| `wos` | `wos: WaveEnv["wos"]` | Take the whole `wos` object (no sub-typing needed), but **only add it if `wos` is actually used**. |
| `services` | `services: { svc: WaveEnv["services"]["svc"]; }` | List each service used; take the whole service object (no method-level narrowing). |
| `getSettingsKeyAtom` | `SettingsKeyAtomFnType<"key1" \| "key2">` | Union all settings keys accessed. |
| `getBlockMetaKeyAtom` | `BlockMetaKeyAtomFnType<"key1" \| "key2">` | Union all block meta keys accessed. |
| `getBlockMetaKeyAtom` | `MetaKeyAtomFnType<"key1" \| "key2">` | Union all block meta keys accessed. |
| `getTabMetaKeyAtom` | `MetaKeyAtomFnType<"key1" \| "key2">` | Union all tab meta keys accessed. |
| `getConnConfigKeyAtom` | `ConnConfigKeyAtomFnType<"key1">` | Union all conn config keys accessed. |
| All other `WaveEnv` fields | `WaveEnv["fieldName"]` | Copy type verbatim. |

View file

@ -20,7 +20,7 @@ const WaveSchemaSettingsFileName = "schema/settings.json"
const WaveSchemaConnectionsFileName = "schema/connections.json"
const WaveSchemaAiPresetsFileName = "schema/aipresets.json"
const WaveSchemaWidgetsFileName = "schema/widgets.json"
const WaveSchemaBgPresetsFileName = "schema/bgpresets.json"
const WaveSchemaBackgroundsFileName = "schema/backgrounds.json"
const WaveSchemaWaveAIFileName = "schema/waveai.json"
// ViewNameType is a string type whose JSON Schema offers enum suggestions for the most
@ -105,8 +105,26 @@ type WidgetsMetaSchemaHints struct {
TermDurable *bool `json:"term:durable,omitempty"`
}
func generateSchema(template any, dir string) error {
// allowNullValues wraps the top-level additionalProperties of a map schema with
// anyOf: [originalSchema, {type: "null"}] so that setting a key to null is valid
// (e.g. "bg@foo": null to remove a default entry).
func allowNullValues(schema *jsonschema.Schema) {
if schema.AdditionalProperties != nil && schema.AdditionalProperties != jsonschema.TrueSchema && schema.AdditionalProperties != jsonschema.FalseSchema {
original := schema.AdditionalProperties
schema.AdditionalProperties = &jsonschema.Schema{
AnyOf: []*jsonschema.Schema{
original,
{Type: "null"},
},
}
}
}
func generateSchema(template any, dir string, allowNull bool) error {
settingsSchema := jsonschema.Reflect(template)
if allowNull {
allowNullValues(settingsSchema)
}
jsonSettingsSchema, err := json.MarshalIndent(settingsSchema, "", " ")
if err != nil {
@ -147,6 +165,7 @@ func generateWidgetsSchema(dir string) error {
widgetsTemplate := make(map[string]wconfig.WidgetConfigType)
widgetsSchema := r.Reflect(&widgetsTemplate)
allowNullValues(widgetsSchema)
jsonWidgetsSchema, err := json.MarshalIndent(widgetsSchema, "", " ")
if err != nil {
@ -163,19 +182,19 @@ func generateWidgetsSchema(dir string) error {
}
func main() {
err := generateSchema(&wconfig.SettingsType{}, WaveSchemaSettingsFileName)
err := generateSchema(&wconfig.SettingsType{}, WaveSchemaSettingsFileName, false)
if err != nil {
log.Fatalf("settings schema error: %v", err)
}
connectionTemplate := make(map[string]wconfig.ConnKeywords)
err = generateSchema(&connectionTemplate, WaveSchemaConnectionsFileName)
err = generateSchema(&connectionTemplate, WaveSchemaConnectionsFileName, false)
if err != nil {
log.Fatalf("connections schema error: %v", err)
}
aiPresetsTemplate := make(map[string]wconfig.AiSettingsType)
err = generateSchema(&aiPresetsTemplate, WaveSchemaAiPresetsFileName)
err = generateSchema(&aiPresetsTemplate, WaveSchemaAiPresetsFileName, false)
if err != nil {
log.Fatalf("ai presets schema error: %v", err)
}
@ -185,14 +204,14 @@ func main() {
log.Fatalf("widgets schema error: %v", err)
}
bgPresetsTemplate := make(map[string]wconfig.BgPresetsType)
err = generateSchema(&bgPresetsTemplate, WaveSchemaBgPresetsFileName)
backgroundsTemplate := make(map[string]wconfig.BackgroundConfigType)
err = generateSchema(&backgroundsTemplate, WaveSchemaBackgroundsFileName, true)
if err != nil {
log.Fatalf("bg presets schema error: %v", err)
log.Fatalf("backgrounds schema error: %v", err)
}
waveAITemplate := make(map[string]wconfig.AIModeConfigType)
err = generateSchema(&waveAITemplate, WaveSchemaWaveAIFileName)
err = generateSchema(&waveAITemplate, WaveSchemaWaveAIFileName, false)
if err != nil {
log.Fatalf("waveai schema error: %v", err)
}

View file

@ -560,6 +560,7 @@ func main() {
createMainWshClient()
sigutil.InstallShutdownSignalHandlers(doShutdown)
sigutil.InstallSIGUSR1Handler()
wconfig.MigratePresetsBackgrounds()
startConfigWatcher()
aiusechat.InitAIModeConfigWatcher()
maybeStartPprofServer()

View file

@ -19,7 +19,7 @@ import (
)
var setBgCmd = &cobra.Command{
Use: "setbg [--opacity value] [--tile|--center] [--scale value] (image-path|\"#color\"|color-name)",
Use: "setbg [--opacity value] [--tile|--center] [--scale value] [--border-color color] [--active-border-color color] (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)
@ -31,18 +31,22 @@ You can also:
- 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 --border-color to set the block frame border color
- Use --active-border-color to set the block frame focused border color
- 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
setBgOpacity float64
setBgTile bool
setBgCenter bool
setBgSize string
setBgClear bool
setBgPrint bool
setBgBorderColor string
setBgActiveBorderColor string
)
func init() {
@ -53,8 +57,9 @@ func init() {
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")
setBgCmd.Flags().StringVar(&setBgBorderColor, "border-color", "", "block frame border color (#RRGGBB, #RRGGBBAA, or CSS color name)")
setBgCmd.Flags().StringVar(&setBgActiveBorderColor, "active-border-color", "", "block frame focused border color (#RRGGBB, #RRGGBBAA, or CSS color name)")
// Make tile and center mutually exclusive
setBgCmd.MarkFlagsMutuallyExclusive("tile", "center")
}
@ -73,17 +78,41 @@ func validateHexColor(color string) error {
return nil
}
func validateColor(color string) error {
if strings.HasPrefix(color, "#") {
return validateHexColor(color)
}
if !CssColorNames[strings.ToLower(color)] {
return fmt.Errorf("invalid color %q: must be a hex color (#RRGGBB or #RRGGBBAA) or a CSS color name", color)
}
return nil
}
func setBgRun(cmd *cobra.Command, args []string) (rtnErr error) {
defer func() {
sendActivity("setbg", rtnErr == nil)
}()
borderColorChanged := cmd.Flags().Changed("border-color")
activeBorderColorChanged := cmd.Flags().Changed("active-border-color")
if borderColorChanged {
if err := validateColor(setBgBorderColor); err != nil {
return fmt.Errorf("--border-color: %v", err)
}
}
if activeBorderColorChanged {
if err := validateColor(setBgActiveBorderColor); err != nil {
return fmt.Errorf("--active-border-color: %v", err)
}
}
// Create base metadata
meta := map[string]interface{}{}
// Handle opacity-only change or clear
if len(args) == 0 {
if !cmd.Flags().Changed("opacity") && !setBgClear {
if !cmd.Flags().Changed("opacity") && !setBgClear && !borderColorChanged && !activeBorderColorChanged {
OutputHelpMessage(cmd)
return fmt.Errorf("setbg requires an image path or color value")
}
@ -92,7 +121,7 @@ func setBgRun(cmd *cobra.Command, args []string) (rtnErr error) {
}
if setBgClear {
meta["bg:*"] = true
} else {
} else if cmd.Flags().Changed("opacity") {
meta["bg:opacity"] = setBgOpacity
}
} else if len(args) > 1 {
@ -101,6 +130,7 @@ func setBgRun(cmd *cobra.Command, args []string) (rtnErr error) {
} else {
// Handle background setting
meta["bg:*"] = true
meta["tab:background"] = nil
if setBgOpacity < 0 || setBgOpacity > 1 {
return fmt.Errorf("opacity must be between 0.0 and 1.0")
}
@ -159,6 +189,13 @@ func setBgRun(cmd *cobra.Command, args []string) (rtnErr error) {
meta["bg"] = bgStyle
}
if borderColorChanged {
meta["bg:bordercolor"] = setBgBorderColor
}
if activeBorderColorChanged {
meta["bg:activebordercolor"] = setBgActiveBorderColor
}
if setBgPrint {
jsonBytes, err := json.MarshalIndent(meta, "", " ")
if err != nil {

View file

@ -6,7 +6,7 @@ title: "Configuration"
import { Kbd } from "@site/src/components/kbd";
import { PlatformProvider, PlatformSelectorButton } from "@site/src/components/platformcontext";
import { VersionBadge } from "@site/src/components/versionbadge";
import { VersionBadge, DeprecatedBadge } from "@site/src/components/versionbadge";
<PlatformProvider>
@ -92,7 +92,8 @@ wsh editconfig
| autoupdate:intervalms | float64 | time in milliseconds to wait between update checks (requires app restart) |
| autoupdate:installonquit | bool | whether to automatically install updates on quit (requires app restart) |
| autoupdate:channel | string | the auto update channel "latest" (stable builds), or "beta" (updated more frequently) (requires app restart) |
| tab:preset | string | a "bg@" preset to automatically apply to new tabs. e.g. `bg@green`. should match the preset key |
| tab:preset <DeprecatedBadge /> | string | a "bg@" preset to automatically apply to new tabs. e.g. `bg@green`. should match the preset key. deprecated in favor of `tab:background` |
| tab:background <VersionBadge version="v0.14.4" /> | string | a "bg@" preset to automatically apply to new tabs. e.g. `bg@green`. should match the preset key |
| tab:confirmclose | bool | if set to true, a confirmation dialog will be shown before closing a tab (defaults to false) |
| widget:showhelp | bool | whether to show help/tips widgets in right sidebar |
| window:transparent | bool | set to true to enable window transparency (cannot be combined with `window:blur`) (macOS and Windows only, requires app restart, see [note on Windows compatibility](https://www.electronjs.org/docs/latest/tutorial/custom-window-styles#limitations)) |

View file

@ -10,7 +10,9 @@ title: "Customization"
Right click on any tab to bring up a menu which allows you to rename the tab and select different backgrounds.
It is also possible to create your own themes using custom colors, gradients, images and more by editing your presets.json config file. To see how Wave's built in tab themes are defined, you can check out our [default presets file](https://github.com/wavetermdev/waveterm/blob/main/pkg/wconfig/defaultconfig/presets.json).
It is also possible to create your own background themes using custom colors, gradients, images and more by editing your backgrounds.json config file. To see how Wave's built-in tab backgrounds are defined, you can check out the [default backgrounds.json file](https://github.com/wavetermdev/waveterm/blob/main/pkg/wconfig/defaultconfig/backgrounds.json).
To apply a tab background to all new tabs by default, set the key `tab:background` in your [Wave Config File](/config) to one of the background preset keys (e.g. `"bg@ocean-depths"`). The available built-in background keys can be found in the [default backgrounds.json file](https://github.com/wavetermdev/waveterm/blob/main/pkg/wconfig/defaultconfig/backgrounds.json).
## Terminal Customization
@ -26,8 +28,6 @@ in the [default termthemes.json file](https://github.com/wavetermdev/waveterm/bl
If you add your own termthemes.json file in the config directory, you can also add your own custom terminal themes (just follow the same format).
You can set the key `tab:preset` in your [Wave Config File](/config) to apply a theme to all new tabs.
#### Font Size
From the same context menu you can also change the font-size of the terminal. To change the default font size across all of your (non-overridden) terminals, you can set the config key `term:fontsize` to the size you want. e.g. `{ "term:fontsize": 14}`.
@ -79,6 +79,6 @@ To preview the metadata for any background without applying it, use the `--print
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.
For more advanced customization options including gradients, colors, and saving your own custom backgrounds, check out our [Tab Backgrounds](/tab-backgrounds) documentation.

View file

@ -479,7 +479,7 @@ New minor release that introduces Wave's connected computing extensions. We've i
### v0.9.2 &mdash; Nov 11, 2024
New minor release with bug fixes and new features! Fixed the bug around making Wave fullscreen (also affecting certain window managers like Hyprland). We've also put a lot of work into the doc site (https://docs.waveterm.dev), including documenting how [Widgets](./widgets) and [Presets](./presets) work!
New minor release with bug fixes and new features! Fixed the bug around making Wave fullscreen (also affecting certain window managers like Hyprland). We've also put a lot of work into the doc site (https://docs.waveterm.dev), including documenting how [Widgets](./widgets) and Presets work!
- Updated documentation
- Wave AI now supports the Anthropic API! Checkout the [FAQ](./faq) for how to use the Claude models with Wave AI.

View file

@ -1,80 +1,57 @@
---
sidebar_position: 3.5
id: "presets"
title: "Presets"
id: "tab-backgrounds"
title: "Tab Backgrounds"
---
# Presets
# Tab Backgrounds
Wave's preset system allows you to save and apply multiple configuration settings at once. Presets are used for:
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.
- Tab backgrounds: Apply visual styles to your tabs
## Managing Backgrounds
## Managing Presets
Custom backgrounds are stored in `~/.config/waveterm/backgrounds.json`.
You can store presets in two locations:
- `~/.config/waveterm/presets.json`: Main presets file
- `~/.config/waveterm/presets/`: Directory for organizing presets into separate files
All presets are aggregated regardless of which file they're in, so you can use the `presets` directory to organize them (e.g., `presets/bg.json`).
:::info
You can easily edit your presets using the built-in editor:
**To edit using the UI:**
1. Click the settings (gear) icon in the widget bar
2. Select "Settings" from the menu
3. Choose "Tab Backgrounds" from the settings sidebar
**Or launch from the command line:**
```bash
wsh editconfig presets.json # Edit main presets file
wsh editconfig presets/bg.json # Edit background presets
wsh editconfig backgrounds.json
```
:::
## File Format
Presets follow this format:
Backgrounds follow this format:
```json
{
"<preset-type>@<preset-key>": {
"display:name": "<Preset name>",
"display:order": "<number>", // optional
"<overridden-config-key-1>": "<overridden-config-value-1>"
...
"bg@<key>": {
"display:name": "<Background name>",
"display:order": <number>,
"bg": "<CSS background value>",
"bg:opacity": <float>
}
}
```
The `preset-type` determines where the preset appears in Wave's interface:
To see how Wave's built-in backgrounds are defined, check out the [default backgrounds.json file](https://github.com/wavetermdev/waveterm/blob/main/pkg/wconfig/defaultconfig/backgrounds.json).
- `bg`: Appears in the "Backgrounds" submenu when right-clicking a tab
### Common Keys
| Key Name | Type | Function |
| ------------- | ------ | ----------------------------------------- |
| display:name | string | Name shown in the UI menu (required) |
| display:order | float | Controls the order in the menu (optional) |
:::info
When a preset is applied, it overrides the default configuration values for that tab or block. Using `bg:*` will clear any previously overridden values, setting them back to defaults. It's recommended to include this key in your presets to ensure a clean slate.
:::
## Background Presets
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.
### Configuration Keys
## Configuration Keys
| Key Name | Type | Function |
| -------------------- | ------ | ------------------------------------------------------------------------------------------------------- |
| 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.) |
| display:name | string | Name shown in the UI menu (required) |
| display:order | float | Controls the order in the menu (optional) |
| 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 when a block is not active (rarely used) |
| bg:activebordercolor | string | The color of the border when a block is active |
### Examples
## Examples
#### Simple solid color:
@ -82,7 +59,6 @@ Wave's background system harnesses the full power of CSS backgrounds, letting yo
{
"bg@blue": {
"display:name": "Blue",
"bg:*": true,
"bg": "blue",
"bg:opacity": 0.3,
"bg:activebordercolor": "rgba(0, 0, 255, 1.0)"
@ -96,7 +72,6 @@ Wave's background system harnesses the full power of CSS backgrounds, letting yo
{
"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"
@ -110,7 +85,6 @@ Wave's background system harnesses the full power of CSS backgrounds, letting yo
{
"bg@ocean": {
"display:name": "Ocean Scene",
"bg:*": true,
"bg": "url('/path/to/ocean.jpg') center/cover no-repeat",
"bg:opacity": 0.2
}
@ -122,10 +96,10 @@ Background images support both URLs and local file paths. For better reliability
:::
:::tip
The `setbg` command can help generate background preset JSON:
The `setbg` command can help generate background JSON:
```bash
# Preview a solid color preset
# Preview a solid color background
wsh setbg --print "#ff0000"
{
"bg:*": true,
@ -133,7 +107,7 @@ wsh setbg --print "#ff0000"
"bg:opacity": 0.5
}
# Preview a centered image preset
# Preview a centered image background
wsh setbg --print --center --opacity 0.3 ~/logo.png
{
"bg:*": true,
@ -142,5 +116,5 @@ wsh setbg --print --center --opacity 0.3 ~/logo.png
}
```
Just add the required `display:name` field to complete your preset!
Just add the required `display:name` field and a `bg@<key>` wrapper to complete your background entry!
:::

View file

@ -200,7 +200,7 @@ wsh editconfig presets/ai.json
The `setbg` command allows you to set a background image or color for the current tab with various customization options.
```sh
wsh setbg [--opacity value] [--tile|--center] [--size value] (image-path|"#color"|color-name)
wsh setbg [--opacity value] [--tile|--center] [--size value] [--border-color color] [--active-border-color color] (image-path|"#color"|color-name)
```
You can set a background using:
@ -216,6 +216,8 @@ Flags:
- `--center` - center the image without scaling (good for logos)
- `--size` - size for centered images (px, %, or auto)
- `--clear` - remove the background
- `--border-color color` - set the block frame border color (hex or CSS color name)
- `--active-border-color color` - set the block frame focused border color (hex or CSS color name)
- `--print` - show the metadata without applying it
Supported image formats: JPEG, PNG, GIF, WebP, and SVG.
@ -243,6 +245,10 @@ wsh setbg forestgreen # CSS color name
# Change just the opacity of current background
wsh setbg --opacity 0.7
# Set border colors alongside a background
wsh setbg --border-color "#ff0000" --active-border-color "#00ff00" ~/pictures/background.jpg
wsh setbg --border-color steelblue forestgreen
# Remove background
wsh setbg --clear
@ -258,7 +264,7 @@ The command validates that:
- 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)
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 entry](/tab-backgrounds)
:::
---

View file

@ -20,3 +20,22 @@
background-color: var(--ifm-color-primary-dark);
color: var(--ifm-background-color);
}
.deprecated-badge {
display: inline-block;
padding: 0.125rem 0.5rem;
margin-left: 0.25rem;
font-size: 0.75rem;
font-weight: 600;
line-height: 1.5;
border-radius: 0.25rem;
background-color: #9e9e9e;
color: #fff;
vertical-align: middle;
white-space: nowrap;
}
[data-theme="dark"] .deprecated-badge {
background-color: #616161;
color: #e0e0e0;
}

View file

@ -7,4 +7,8 @@ interface VersionBadgeProps {
export function VersionBadge({ version, noLeftMargin }: VersionBadgeProps) {
return <span className={`version-badge${noLeftMargin ? " no-left-margin" : ""}`}>{version}</span>;
}
export function DeprecatedBadge() {
return <span className="deprecated-badge">deprecated</span>;
}

View file

@ -3,11 +3,13 @@
import { handleWaveAIContextMenu } from "@/app/aipanel/aipanel-contextmenu";
import { waveAIHasSelection } from "@/app/aipanel/waveai-focus-utils";
import { useTabBackground } from "@/app/block/blockutil";
import { ErrorBoundary } from "@/app/element/errorboundary";
import { atoms, getSettingsKeyAtom } from "@/app/store/global";
import { globalStore } from "@/app/store/jotaiStore";
import { useTabModelMaybe } from "@/app/store/tab-model";
import { isBuilderWindow } from "@/app/store/windowtype";
import { useWaveEnv } from "@/app/waveenv/waveenv";
import { checkKeyPressed, keydownWrapper } from "@/util/keyutil";
import { isMacOS, isWindows } from "@/util/platformutil";
import { cn } from "@/util/util";
@ -255,6 +257,7 @@ const AIPanelComponentInner = memo(({ roundTopLeft }: AIPanelComponentInnerProps
const [initialLoadDone, setInitialLoadDone] = useState(false);
const model = WaveAIModel.getInstance();
const containerRef = useRef<HTMLDivElement>(null);
const waveEnv = useWaveEnv();
const isLayoutMode = jotai.useAtomValue(atoms.controlShiftDelayAtom);
const showOverlayBlockNums = jotai.useAtomValue(getSettingsKeyAtom("app:showoverlayblocknums")) ?? true;
const isFocused = jotai.useAtomValue(model.isWaveAIFocusedAtom);
@ -262,6 +265,7 @@ const AIPanelComponentInner = memo(({ roundTopLeft }: AIPanelComponentInnerProps
const telemetryEnabled = jotai.useAtomValue(getSettingsKeyAtom("telemetry:enabled")) ?? false;
const isPanelVisible = jotai.useAtomValue(model.getPanelVisibleAtom());
const tabModel = useTabModelMaybe();
const [tabBorderColor, tabActiveBorderColor] = useTabBackground(waveEnv, tabModel?.tabId);
const defaultMode = jotai.useAtomValue(getSettingsKeyAtom("waveai:defaultmode")) ?? "waveai@balanced";
const aiModeConfigs = jotai.useAtomValue(model.aiModeConfigs);
@ -546,6 +550,7 @@ const AIPanelComponentInner = memo(({ roundTopLeft }: AIPanelComponentInnerProps
};
const showBlockMask = isLayoutMode && showOverlayBlockNums;
const borderColor = isFocused ? (tabActiveBorderColor ?? null) : (tabBorderColor ?? null);
return (
<div
@ -555,13 +560,14 @@ const AIPanelComponentInner = memo(({ roundTopLeft }: AIPanelComponentInnerProps
"@container bg-zinc-900/70 flex flex-col relative",
model.inBuilder ? "mt-0 h-full" : "mt-1 h-[calc(100%-4px)]",
(isDragOver || isReactDndDragOver) && "bg-zinc-800 border-accent",
isFocused ? "border-2 border-accent" : "border-2 border-transparent"
isFocused && !borderColor ? "border-2 border-accent" : "border-2 border-transparent"
)}
style={{
borderTopLeftRadius: roundTopLeft ? 10 : 0,
borderTopRightRadius: model.inBuilder ? 0 : 10,
borderBottomRightRadius: model.inBuilder ? 0 : 10,
borderBottomLeftRadius: 10,
borderColor: borderColor ?? undefined,
}}
onFocusCapture={handleFocusCapture}
onPointerEnter={handlePointerEnter}

View file

@ -1,6 +1,7 @@
// Copyright 2025, Command Line Inc.
// Copyright 2026, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { MetaKeyAtomFnType, useWaveEnv, WaveEnv, WaveEnvSubset } from "@/app/waveenv/waveenv";
import { PLATFORM, PlatformMacOS } from "@/util/platformutil";
import { computeBgStyleFromMeta } from "@/util/waveutil";
import useResizeObserver from "@react-hook/resize-observer";
@ -10,11 +11,20 @@ import { debounce } from "throttle-debounce";
import { atoms, getApi, WOS } from "./store/global";
import { useWaveObjectValue } from "./store/wos";
type AppBgEnv = WaveEnvSubset<{
getTabMetaKeyAtom: MetaKeyAtomFnType<"tab:background">;
getConfigBackgroundAtom: WaveEnv["getConfigBackgroundAtom"];
}>;
export function AppBackground() {
const bgRef = useRef<HTMLDivElement>(null);
const tabId = useAtomValue(atoms.staticTabId);
const [tabData] = useWaveObjectValue<Tab>(WOS.makeORef("tab", tabId));
const style: CSSProperties = computeBgStyleFromMeta(tabData?.meta, 0.5) ?? {};
const env = useWaveEnv<AppBgEnv>();
const tabBg = useAtomValue(env.getTabMetaKeyAtom(tabId, "tab:background"));
const configBg = useAtomValue(env.getConfigBackgroundAtom(tabBg));
const resolvedMeta: Omit<BackgroundConfigType, "display:name"> = tabBg && configBg ? configBg : tabData?.meta;
const style: CSSProperties = computeBgStyleFromMeta(resolvedMeta, 0.5) ?? {};
const getAvgColor = useCallback(
debounce(30, () => {
if (
@ -42,5 +52,11 @@ export function AppBackground() {
useLayoutEffect(getAvgColor, [getAvgColor]);
useResizeObserver(bgRef, getAvgColor);
return <div ref={bgRef} className="pointer-events-none absolute top-0 left-0 w-full h-full z-[var(--zindex-app-background)]" style={style} />;
return (
<div
ref={bgRef}
className="pointer-events-none absolute top-0 left-0 w-full h-full z-[var(--zindex-app-background)]"
style={style}
/>
);
}

View file

@ -2,8 +2,8 @@
// SPDX-License-Identifier: Apache-2.0
import {
BlockMetaKeyAtomFnType,
ConnConfigKeyAtomFnType,
MetaKeyAtomFnType,
SettingsKeyAtomFnType,
WaveEnv,
WaveEnvSubset,
@ -36,7 +36,7 @@ export type BlockEnv = WaveEnvSubset<{
getConnStatusAtom: WaveEnv["getConnStatusAtom"];
getLocalHostDisplayNameAtom: WaveEnv["getLocalHostDisplayNameAtom"];
getConnConfigKeyAtom: ConnConfigKeyAtomFnType<"conn:wshenabled">;
getBlockMetaKeyAtom: BlockMetaKeyAtomFnType<
getBlockMetaKeyAtom: MetaKeyAtomFnType<
| "frame:text"
| "frame:activebordercolor"
| "frame:bordercolor"
@ -46,4 +46,6 @@ export type BlockEnv = WaveEnvSubset<{
| "frame:title"
| "frame:icon"
>;
getTabMetaKeyAtom: MetaKeyAtomFnType<"bg:activebordercolor" | "bg:bordercolor" | "tab:background">;
getConfigBackgroundAtom: WaveEnv["getConfigBackgroundAtom"];
}>;

View file

@ -3,7 +3,7 @@
import { BlockModel } from "@/app/block/block-model";
import { BlockFrame_Header } from "@/app/block/blockframe-header";
import { blockViewToIcon, getViewIconElem } from "@/app/block/blockutil";
import { blockViewToIcon, getViewIconElem, useTabBackground } from "@/app/block/blockutil";
import { ConnStatusOverlay } from "@/app/block/connstatusoverlay";
import { ChangeConnectionBlockModal } from "@/app/modals/conntypeahead";
import { getBlockComponentModel, globalStore, useBlockAtom } from "@/app/store/global";
@ -36,8 +36,7 @@ const BlockMask = React.memo(({ nodeModel }: { nodeModel: NodeModel }) => {
waveEnv.getBlockMetaKeyAtom(nodeModel.blockId, "frame:activebordercolor")
);
const frameBorderColor = jotai.useAtomValue(waveEnv.getBlockMetaKeyAtom(nodeModel.blockId, "frame:bordercolor"));
const tabActiveBorderColor = jotai.useAtomValue(tabModel.getTabMetaAtom("bg:activebordercolor"));
const tabBorderColor = jotai.useAtomValue(tabModel.getTabMetaAtom("bg:bordercolor"));
const [tabBorderColor, tabActiveBorderColor] = useTabBackground(waveEnv, tabModel.tabId);
const style: React.CSSProperties = {};
let showBlockMask = false;
@ -107,9 +106,13 @@ const BlockFrame_Default_Component = (props: BlockFrameProps) => {
const connModalOpen = jotai.useAtomValue(changeConnModalAtom);
const isMagnified = jotai.useAtomValue(nodeModel.isMagnified);
const isEphemeral = jotai.useAtomValue(nodeModel.isEphemeral);
const [magnifiedBlockBlurAtom] = React.useState(() => waveEnv.getSettingsKeyAtom("window:magnifiedblockblurprimarypx"));
const [magnifiedBlockBlurAtom] = React.useState(() =>
waveEnv.getSettingsKeyAtom("window:magnifiedblockblurprimarypx")
);
const magnifiedBlockBlur = jotai.useAtomValue(magnifiedBlockBlurAtom);
const [magnifiedBlockOpacityAtom] = React.useState(() => waveEnv.getSettingsKeyAtom("window:magnifiedblockopacity"));
const [magnifiedBlockOpacityAtom] = React.useState(() =>
waveEnv.getSettingsKeyAtom("window:magnifiedblockopacity")
);
const magnifiedBlockOpacity = jotai.useAtomValue(magnifiedBlockOpacityAtom);
const connBtnRef = React.useRef<HTMLDivElement>(null);
const connName = jotai.useAtomValue(waveEnv.getBlockMetaKeyAtom(nodeModel.blockId, "connection"));
@ -141,7 +144,11 @@ const BlockFrame_Default_Component = (props: BlockFrameProps) => {
if (!util.isLocalConnName(connName)) {
console.log("ensure conn", nodeModel.blockId, connName);
waveEnv.rpc
.ConnEnsureCommand(TabRpcClient, { connname: connName, logblockid: nodeModel.blockId }, { timeout: 60000 })
.ConnEnsureCommand(
TabRpcClient,
{ connname: connName, logblockid: nodeModel.blockId },
{ timeout: 60000 }
)
.catch((e) => {
console.log("error ensuring connection", nodeModel.blockId, connName, e);
});

View file

@ -2,13 +2,24 @@
// SPDX-License-Identifier: Apache-2.0
import { Button } from "@/app/element/button";
import {
MetaKeyAtomFnType,
WaveEnv,
WaveEnvSubset,
} from "@/app/waveenv/waveenv";
import { IconButton, ToggleIconButton } from "@/element/iconbutton";
import { MagnifyIcon } from "@/element/magnify";
import { MenuButton } from "@/element/menubutton";
import * as util from "@/util/util";
import clsx from "clsx";
import * as jotai from "jotai";
import * as React from "react";
export type TabBackgroundEnv = WaveEnvSubset<{
getTabMetaKeyAtom: MetaKeyAtomFnType<"bg:activebordercolor" | "bg:bordercolor" | "tab:background">;
getConfigBackgroundAtom: WaveEnv["getConfigBackgroundAtom"];
}>;
export const colorRegex = /^((#[0-9a-f]{6,8})|([a-z]+))$/;
export const NumActiveConnColors = 8;
@ -155,6 +166,19 @@ export function getViewIconElem(
}
}
export function useTabBackground(
waveEnv: TabBackgroundEnv,
tabId: string | null
): [string, string, BackgroundConfigType] {
const tabActiveBorderColorDirect = jotai.useAtomValue(waveEnv.getTabMetaKeyAtom(tabId, "bg:activebordercolor"));
const tabBorderColorDirect = jotai.useAtomValue(waveEnv.getTabMetaKeyAtom(tabId, "bg:bordercolor"));
const tabBg = jotai.useAtomValue(waveEnv.getTabMetaKeyAtom(tabId, "tab:background"));
const configBg = jotai.useAtomValue(waveEnv.getConfigBackgroundAtom(tabBg));
const tabActiveBorderColor = tabActiveBorderColorDirect ?? configBg?.["bg:activebordercolor"];
const tabBorderColor = tabBorderColorDirect ?? configBg?.["bg:bordercolor"];
return [tabBorderColor, tabActiveBorderColor, configBg];
}
export const Input = React.memo(
({ decl, className, preview }: { decl: HeaderInput; className: string; preview: boolean }) => {
const { value, ref, isDisabled, onChange, onKeyDown, onFocus, onBlur } = decl;

View file

@ -1,10 +1,10 @@
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import settingsSchema from "../../../schema/settings.json";
import connectionsSchema from "../../../schema/connections.json";
import aipresetsSchema from "../../../schema/aipresets.json";
import bgpresetsSchema from "../../../schema/bgpresets.json";
import backgroundsSchema from "../../../schema/backgrounds.json";
import connectionsSchema from "../../../schema/connections.json";
import settingsSchema from "../../../schema/settings.json";
import waveaiSchema from "../../../schema/waveai.json";
import widgetsSchema from "../../../schema/widgets.json";
@ -31,9 +31,9 @@ const MonacoSchemas: SchemaInfo[] = [
schema: aipresetsSchema,
},
{
uri: "wave://schema/bgpresets.json",
fileMatch: ["*/WAVECONFIGPATH/presets/bg.json"],
schema: bgpresetsSchema,
uri: "wave://schema/backgrounds.json",
fileMatch: ["*/WAVECONFIGPATH/backgrounds.json"],
schema: backgroundsSchema,
},
{
uri: "wave://schema/waveai.json",

View file

@ -132,6 +132,10 @@ function getBlockMetaKeyAtom<T extends keyof MetaType>(blockId: string, key: T):
return metaAtom;
}
function getTabMetaKeyAtom<T extends keyof MetaType>(tabId: string, key: T): Atom<MetaType[T]> {
return getOrefMetaKeyAtom(WOS.makeORef("tab", tabId), key);
}
function getOrefMetaKeyAtom<T extends keyof MetaType>(oref: string, key: T): Atom<MetaType[T]> {
const orefCache = getSingleOrefAtomCache(oref);
const metaAtomName = "#meta-" + key;
@ -229,6 +233,21 @@ function useSettingsKeyAtom<T extends keyof SettingsType>(key: T): SettingsType[
return useAtomValue(getSettingsKeyAtom(key));
}
const configBackgroundAtomCache = new Map<string, Atom<BackgroundConfigType>>();
function getConfigBackgroundAtom(bgKey: string | null): Atom<BackgroundConfigType> {
if (isPreviewWindow() || bgKey == null) return NullAtom as Atom<BackgroundConfigType>;
let bgAtom = configBackgroundAtomCache.get(bgKey);
if (bgAtom == null) {
bgAtom = atom((get) => {
const fullConfig = get(atoms.fullConfigAtom);
return fullConfig.backgrounds?.[bgKey];
});
configBackgroundAtomCache.set(bgKey, bgAtom);
}
return bgAtom;
}
function getSettingsPrefixAtom(prefix: string): Atom<SettingsType> {
if (isPreviewWindow()) return NullAtom as Atom<SettingsType>;
let settingsPrefixAtom = settingsAtomCache.get(prefix + ":");
@ -666,6 +685,8 @@ export {
getBlockComponentModel,
getBlockMetaKeyAtom,
getBlockTermDurableAtom,
getTabMetaKeyAtom,
getConfigBackgroundAtom,
getConnConfigKeyAtom,
getConnStatusAtom,
getFocusedBlockId,

View file

@ -75,28 +75,38 @@ export function buildTabContextMenu(
];
menu.push({ label: "Flag Tab", type: "submenu", submenu: flagSubmenu }, { type: "separator" });
const fullConfig = globalStore.get(env.atoms.fullConfigAtom);
const bgPresets: string[] = [];
for (const key in fullConfig?.presets ?? {}) {
if (key.startsWith("bg@") && fullConfig.presets[key] != null) {
bgPresets.push(key);
}
}
bgPresets.sort((a, b) => {
const aOrder = fullConfig.presets[a]["display:order"] ?? 0;
const bOrder = fullConfig.presets[b]["display:order"] ?? 0;
const backgrounds = fullConfig?.backgrounds ?? {};
const bgKeys = Object.keys(backgrounds).filter((k) => backgrounds[k] != null);
bgKeys.sort((a, b) => {
const aOrder = backgrounds[a]["display:order"] ?? 0;
const bOrder = backgrounds[b]["display:order"] ?? 0;
return aOrder - bOrder;
});
if (bgPresets.length > 0) {
if (bgKeys.length > 0) {
const submenu: ContextMenuItem[] = [];
const oref = makeORef("tab", id);
for (const presetName of bgPresets) {
// preset cannot be null (filtered above)
const preset = fullConfig.presets[presetName];
submenu.push({
label: "Default",
click: () =>
fireAndForget(async () => {
await env.rpc.SetMetaCommand(TabRpcClient, {
oref,
meta: { "bg:*": true, "tab:background": null },
});
env.rpc.ActivityCommand(TabRpcClient, { settabtheme: 1 }, { noresponse: true });
recordTEvent("action:settabtheme");
}),
});
for (const bgKey of bgKeys) {
const bg = backgrounds[bgKey];
submenu.push({
label: preset["display:name"] ?? presetName,
label: bg["display:name"] ?? bgKey,
click: () =>
fireAndForget(async () => {
await env.rpc.SetMetaCommand(TabRpcClient, { oref, meta: preset });
await env.rpc.SetMetaCommand(TabRpcClient, {
oref,
meta: { "bg:*": true, "tab:background": bgKey },
});
env.rpc.ActivityCommand(TabRpcClient, { settabtheme: 1 }, { noresponse: true });
recordTEvent("action:settabtheme");
}),

View file

@ -93,7 +93,7 @@ export function CodeEditor({ blockId, text, language, fileName, readonly, onChan
}, [minimapEnabled, stickyScrollEnabled, wordWrap, fontSize, readonly]);
return (
<div className="flex flex-col w-full h-full overflow-hidden items-center justify-center">
<div className="flex flex-col w-full h-full items-center justify-center">
<div className="flex flex-col h-full w-full" ref={divRef}>
<MonacoCodeEditor
readonly={readonly}

View file

@ -14,7 +14,7 @@ import * as React from "react";
import { useDimensionsWithExistingRef } from "@/app/hook/useDimensions";
import { waveEventSubscribeSingle } from "@/app/store/wps";
import { TabRpcClient } from "@/app/store/wshrpcutil";
import type { BlockMetaKeyAtomFnType, WaveEnv, WaveEnvSubset } from "@/app/waveenv/waveenv";
import type { MetaKeyAtomFnType, WaveEnv, WaveEnvSubset } from "@/app/waveenv/waveenv";
import { OverlayScrollbarsComponent, OverlayScrollbarsComponentRef } from "overlayscrollbars-react";
export type SysinfoEnv = WaveEnvSubset<{
@ -26,7 +26,7 @@ export type SysinfoEnv = WaveEnvSubset<{
fullConfigAtom: WaveEnv["atoms"]["fullConfigAtom"];
};
getConnStatusAtom: WaveEnv["getConnStatusAtom"];
getBlockMetaKeyAtom: BlockMetaKeyAtomFnType<"graph:numpoints" | "sysinfo:type" | "connection" | "count">;
getBlockMetaKeyAtom: MetaKeyAtomFnType<"graph:numpoints" | "sysinfo:type" | "connection" | "count">;
}>;
const DefaultNumPoints = 120;

View file

@ -8,7 +8,22 @@
import type { FitAddon as IFitApi } from "@xterm/addon-fit";
import type { ITerminalAddon, Terminal } from "@xterm/xterm";
import { IRenderDimensions } from "@xterm/xterm/src/browser/renderer/shared/Types";
interface IDimensions {
width: number;
height: number;
}
interface IRenderDimensions {
css: {
canvas: IDimensions;
cell: IDimensions;
};
device: {
canvas: IDimensions;
cell: IDimensions;
};
}
interface ITerminalDimensions {
/**

View file

@ -32,16 +32,6 @@ export type ConfigFile = {
export const SecretNameRegex = /^[A-Za-z][A-Za-z0-9_]*$/;
function validateBgJson(parsed: any): ValidationResult {
const keys = Object.keys(parsed);
for (const key of keys) {
if (!key.startsWith("bg@")) {
return { error: `Invalid key "${key}": all top-level keys must start with "bg@"` };
}
}
return { success: true };
}
function validateAiJson(parsed: any): ValidationResult {
const keys = Object.keys(parsed);
for (const key of keys) {
@ -101,10 +91,9 @@ function makeConfigFiles(isWindows: boolean): ConfigFile[] {
},
{
name: "Tab Backgrounds",
path: "presets/bg.json",
path: "backgrounds.json",
language: "json",
docsUrl: "https://docs.waveterm.dev/presets#background-configurations",
validator: validateBgJson,
docsUrl: "https://docs.waveterm.dev/tab-backgrounds",
hasJsonView: true,
},
{

View file

@ -10,7 +10,7 @@ import type { WaveConfigEnv } from "@/app/view/waveconfig/waveconfigenv";
import { useWaveEnv } from "@/app/waveenv/waveenv";
import { adaptFromReactOrNativeKeyEvent, checkKeyPressed, keydownWrapper } from "@/util/keyutil";
import { cn } from "@/util/util";
import { useAtom, useAtomValue } from "jotai";
import { useAtom, useAtomValue, useSetAtom } from "jotai";
import type * as MonacoTypes from "monaco-editor";
import { memo, useCallback, useEffect } from "react";
@ -20,7 +20,7 @@ interface ConfigSidebarProps {
const ConfigSidebar = memo(({ model }: ConfigSidebarProps) => {
const selectedFile = useAtomValue(model.selectedFileAtom);
const [isMenuOpen, setIsMenuOpen] = useAtom(model.isMenuOpenAtom);
const setIsMenuOpen = useSetAtom(model.isMenuOpenAtom);
const configFiles = model.getConfigFiles();
const deprecatedConfigFiles = model.getDeprecatedConfigFiles();
const configErrorFiles = useAtomValue(model.configErrorFilesAtom);
@ -164,141 +164,144 @@ const WaveConfigView = memo(({ blockId, model }: ViewComponentProps<WaveConfigVi
return (
<div className="@container flex flex-col w-full h-full">
<div className="flex flex-row flex-1 min-h-0">
{isMenuOpen && (
<div className="absolute inset-0 bg-black/50 z-5 @w600:hidden" onClick={() => setIsMenuOpen(false)} />
)}
<div className={`h-full ${isMenuOpen ? "" : "@max-w600:hidden"}`}>
<ConfigSidebar model={model} />
</div>
<div className="flex flex-col flex-1 min-w-0">
{selectedFile && (
<>
<div className="flex flex-row items-center justify-between px-4 py-2 border-b border-border">
<div className="flex items-baseline gap-2 min-w-0">
<button
onClick={() => setIsMenuOpen(true)}
className="@w600:hidden hover:bg-secondary/50 rounded p-1 cursor-pointer transition-colors mr-2 shrink-0"
>
<i className="fa fa-bars" />
</button>
<div className="text-lg font-semibold whitespace-nowrap shrink-0">
{selectedFile.name}
</div>
{selectedFile.docsUrl && (
<Tooltip content="View documentation">
<a
href={`${selectedFile.docsUrl}?ref=waveconfig`}
target="_blank"
rel="noopener noreferrer"
className="!text-muted-foreground hover:!text-primary transition-colors ml-1 shrink-0 cursor-pointer"
>
<i className="fa fa-book text-sm" />
</a>
</Tooltip>
)}
<div className="text-xs text-muted-foreground font-mono pb-0.5 ml-1 truncate @max-w450:hidden">
{selectedFile.path}
</div>
</div>
<div className="flex gap-2 items-baseline shrink-0">
{selectedFile.hasJsonView && (
<>
{hasChanges && (
<span className="text-xs text-warning pb-0.5 @max-w450:hidden">
Unsaved changes
</span>
)}
<Tooltip content={saveTooltip} placement="bottom" divClassName="shrink-0">
<button
onClick={() => model.saveFile()}
disabled={!hasChanges || isSaving}
className={`px-3 py-1 rounded transition-colors text-sm ${
!hasChanges || isSaving
? "border border-border text-muted-foreground opacity-50"
: "bg-accent/80 text-primary hover:bg-accent cursor-pointer"
}`}
>
{isSaving ? "Saving..." : "Save"}
</button>
</Tooltip>
</>
)}
</div>
</div>
{selectedFile.visualComponent && selectedFile.hasJsonView && (
<div className="flex gap-0 border-b border-border">
<button
onClick={() => setActiveTab("visual")}
className={cn(
"px-4 pt-1 pb-1.5 cursor-pointer transition-colors text-secondary",
activeTab === "visual"
? "bg-highlightbg text-primary"
: "bg-transparent hover:bg-hover"
)}
>
Visual
</button>
<button
onClick={() => setActiveTab("json")}
className={cn(
"px-4 pt-1 pb-1.5 cursor-pointer transition-colors text-secondary",
activeTab === "json"
? "bg-highlightbg text-primary"
: "bg-transparent hover:bg-hover"
)}
>
Raw JSON
</button>
</div>
)}
{errorMessage && (
<div className="bg-error text-primary px-4 py-2 border-b border-error flex items-center justify-between">
<span>{errorMessage}</span>
<button
onClick={() => model.clearError()}
className="ml-2 hover:bg-black/20 rounded p-1 cursor-pointer transition-colors"
>
</button>
</div>
)}
{validationError && (
<div className="bg-error text-primary px-4 py-2 border-b border-error flex items-center justify-between">
<span>{validationError}</span>
<button
onClick={() => model.clearValidationError()}
className="ml-2 hover:bg-black/20 rounded p-1 cursor-pointer transition-colors"
>
</button>
</div>
)}
<div className="flex-1 overflow-hidden">
{isLoading ? (
<div className="flex items-center justify-center h-full text-muted-foreground">
Loading...
</div>
) : selectedFile.visualComponent &&
(!selectedFile.hasJsonView || activeTab === "visual") ? (
(() => {
const VisualComponent = selectedFile.visualComponent;
return <VisualComponent model={model} />;
})()
) : (
<CodeEditor
blockId={blockId}
text={fileContent}
fileName={`WAVECONFIGPATH/${selectedFile.path}`}
language={selectedFile.language}
readonly={false}
onChange={handleContentChange}
onMount={handleEditorMount}
/>
)}
</div>
</>
{isMenuOpen && (
<div
className="absolute inset-0 bg-black/50 z-5 @w600:hidden"
onClick={() => setIsMenuOpen(false)}
/>
)}
</div>
<div className={`h-full ${isMenuOpen ? "" : "@max-w600:hidden"}`}>
<ConfigSidebar model={model} />
</div>
<div className="flex flex-col flex-1 min-w-0">
{selectedFile && (
<>
<div className="flex flex-row items-center justify-between px-4 py-2 border-b border-border">
<div className="flex items-baseline gap-2 min-w-0">
<button
onClick={() => setIsMenuOpen(true)}
className="@w600:hidden hover:bg-secondary/50 rounded p-1 cursor-pointer transition-colors mr-2 shrink-0"
>
<i className="fa fa-bars" />
</button>
<div className="text-lg font-semibold whitespace-nowrap shrink-0">
{selectedFile.name}
</div>
{selectedFile.docsUrl && (
<Tooltip content="View documentation">
<a
href={`${selectedFile.docsUrl}?ref=waveconfig`}
target="_blank"
rel="noopener noreferrer"
className="!text-muted-foreground hover:!text-primary transition-colors ml-1 shrink-0 cursor-pointer"
>
<i className="fa fa-book text-sm" />
</a>
</Tooltip>
)}
<div className="text-xs text-muted-foreground font-mono pb-0.5 ml-1 truncate @max-w450:hidden">
{selectedFile.path}
</div>
</div>
<div className="flex gap-2 items-baseline shrink-0">
{selectedFile.hasJsonView && (
<>
{hasChanges && (
<span className="text-xs text-warning pb-0.5 @max-w450:hidden">
Unsaved changes
</span>
)}
<Tooltip content={saveTooltip} placement="bottom" divClassName="shrink-0">
<button
onClick={() => model.saveFile()}
disabled={!hasChanges || isSaving}
className={`px-3 py-1 rounded transition-colors text-sm ${
!hasChanges || isSaving
? "border border-border text-muted-foreground opacity-50"
: "bg-accent/80 text-primary hover:bg-accent cursor-pointer"
}`}
>
{isSaving ? "Saving..." : "Save"}
</button>
</Tooltip>
</>
)}
</div>
</div>
{selectedFile.visualComponent && selectedFile.hasJsonView && (
<div className="flex gap-0 border-b border-border">
<button
onClick={() => setActiveTab("visual")}
className={cn(
"px-4 pt-1 pb-1.5 cursor-pointer transition-colors text-secondary",
activeTab === "visual"
? "bg-highlightbg text-primary"
: "bg-transparent hover:bg-hover"
)}
>
Visual
</button>
<button
onClick={() => setActiveTab("json")}
className={cn(
"px-4 pt-1 pb-1.5 cursor-pointer transition-colors text-secondary",
activeTab === "json"
? "bg-highlightbg text-primary"
: "bg-transparent hover:bg-hover"
)}
>
Raw JSON
</button>
</div>
)}
{errorMessage && (
<div className="bg-error text-primary px-4 py-2 border-b border-error flex items-center justify-between">
<span>{errorMessage}</span>
<button
onClick={() => model.clearError()}
className="ml-2 hover:bg-black/20 rounded p-1 cursor-pointer transition-colors"
>
</button>
</div>
)}
{validationError && (
<div className="bg-error text-primary px-4 py-2 border-b border-error flex items-center justify-between">
<span>{validationError}</span>
<button
onClick={() => model.clearValidationError()}
className="ml-2 hover:bg-black/20 rounded p-1 cursor-pointer transition-colors"
>
</button>
</div>
)}
<div className="flex-1 min-h-0">
{isLoading ? (
<div className="flex items-center justify-center h-full text-muted-foreground">
Loading...
</div>
) : selectedFile.visualComponent &&
(!selectedFile.hasJsonView || activeTab === "visual") ? (
(() => {
const VisualComponent = selectedFile.visualComponent;
return <VisualComponent model={model} />;
})()
) : (
<CodeEditor
blockId={blockId}
text={fileContent}
fileName={`WAVECONFIGPATH/${selectedFile.path}`}
language={selectedFile.language}
readonly={false}
onChange={handleContentChange}
onMount={handleEditorMount}
/>
)}
</div>
</>
)}
</div>
</div>
{configErrors?.length > 0 && (
<div className="bg-error text-primary px-4 py-1 max-h-12 overflow-y-auto border-t border-error/50 shrink-0">

View file

@ -1,7 +1,7 @@
// Copyright 2026, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import type { BlockMetaKeyAtomFnType, WaveEnv, WaveEnvSubset } from "@/app/waveenv/waveenv";
import type { MetaKeyAtomFnType, WaveEnv, WaveEnvSubset } from "@/app/waveenv/waveenv";
export type WaveConfigEnv = WaveEnvSubset<{
electron: {
@ -22,6 +22,6 @@ export type WaveConfigEnv = WaveEnvSubset<{
atoms: {
fullConfigAtom: WaveEnv["atoms"]["fullConfigAtom"];
};
getBlockMetaKeyAtom: BlockMetaKeyAtomFnType<"file">;
getBlockMetaKeyAtom: MetaKeyAtomFnType<"file">;
isWindows: WaveEnv["isWindows"];
}>;

View file

@ -1,7 +1,7 @@
// Copyright 2026, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import type { BlockMetaKeyAtomFnType, SettingsKeyAtomFnType, WaveEnv, WaveEnvSubset } from "@/app/waveenv/waveenv";
import type { MetaKeyAtomFnType, SettingsKeyAtomFnType, WaveEnv, WaveEnvSubset } from "@/app/waveenv/waveenv";
export type WebViewEnv = WaveEnvSubset<{
electron: {
@ -19,7 +19,7 @@ export type WebViewEnv = WaveEnvSubset<{
wos: WaveEnv["wos"];
createBlock: WaveEnv["createBlock"];
getSettingsKeyAtom: SettingsKeyAtomFnType<"web:defaulturl" | "web:defaultsearch">;
getBlockMetaKeyAtom: BlockMetaKeyAtomFnType<
getBlockMetaKeyAtom: MetaKeyAtomFnType<
"web:hidenav" | "web:useragenttype" | "web:zoom" | "web:partition"
>;
}>;

View file

@ -6,8 +6,8 @@ import { RpcApiType } from "@/app/store/wshclientapi";
import { Atom, PrimitiveAtom } from "jotai";
import React from "react";
export type BlockMetaKeyAtomFnType<Keys extends keyof MetaType = keyof MetaType> = <T extends Keys>(
blockId: string,
export type MetaKeyAtomFnType<Keys extends keyof MetaType = keyof MetaType> = <T extends Keys>(
id: string,
key: T
) => Atom<MetaType[T]>;
@ -74,8 +74,10 @@ export type WaveEnv = {
useWaveObjectValue: <T extends WaveObj>(oref: string) => [T, boolean];
};
getSettingsKeyAtom: SettingsKeyAtomFnType;
getBlockMetaKeyAtom: BlockMetaKeyAtomFnType;
getBlockMetaKeyAtom: MetaKeyAtomFnType;
getTabMetaKeyAtom: MetaKeyAtomFnType;
getConnConfigKeyAtom: ConnConfigKeyAtomFnType;
getConfigBackgroundAtom: (bgKey: string | null) => Atom<BackgroundConfigType>;
// the mock fields are only usable in the preview server (may be be null or throw errors in production)
mockSetWaveObj: <T extends WaveObj>(oref: string, obj: T) => void;

View file

@ -7,10 +7,12 @@ import {
atoms,
createBlock,
getBlockMetaKeyAtom,
getConfigBackgroundAtom,
getConnConfigKeyAtom,
getConnStatusAtom,
getLocalHostDisplayNameAtom,
getSettingsKeyAtom,
getTabMetaKeyAtom,
isDev,
WOS,
} from "@/app/store/global";
@ -44,6 +46,8 @@ export function makeWaveEnvImpl(): WaveEnv {
useWaveObjectValue: WOS.useWaveObjectValue,
},
getBlockMetaKeyAtom,
getTabMetaKeyAtom,
getConfigBackgroundAtom,
getConnConfigKeyAtom,
mockSetWaveObj: <T extends WaveObj>(_oref: string, _obj: T) => {

View file

@ -1,6 +1,7 @@
// Copyright 2026, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import backgroundsJson from "../../../pkg/wconfig/defaultconfig/backgrounds.json";
import mimetypesJson from "../../../pkg/wconfig/defaultconfig/mimetypes.json";
import presetsJson from "../../../pkg/wconfig/defaultconfig/presets.json";
import settingsJson from "../../../pkg/wconfig/defaultconfig/settings.json";
@ -18,5 +19,6 @@ export const DefaultFullConfig: FullConfigType = {
connections: {},
bookmarks: {},
waveai: waveaiJson as unknown as { [key: string]: AIModeConfigType },
backgrounds: backgroundsJson as { [key: string]: BackgroundConfigType },
configerrors: [],
};

View file

@ -8,6 +8,7 @@ import { handleWaveEvent } from "@/app/store/wps";
import { RpcApiType } from "@/app/store/wshclientapi";
import { WaveEnv } from "@/app/waveenv/waveenv";
import { PlatformLinux, PlatformMacOS, PlatformWindows } from "@/util/platformutil";
import { NullAtom } from "@/util/util";
import { Atom, atom, PrimitiveAtom, useAtomValue } from "jotai";
import { showPreviewContextMenu } from "../preview-contextmenu";
import { MockSysinfoConnection } from "../previews/sysinfo.preview-util";
@ -428,8 +429,9 @@ export function makeMockWaveEnv(mockEnv?: MockEnv): MockWaveEnv {
const connStatusAtomCache = new Map<string, PrimitiveAtom<ConnStatus>>();
const waveObjectValueAtomCache = new Map<string, PrimitiveAtom<any>>();
const waveObjectDerivedAtomCache = new Map<string, Atom<any>>();
const blockMetaKeyAtomCache = new Map<string, Atom<any>>();
const orefMetaKeyAtomCache = new Map<string, Atom<any>>();
const connConfigKeyAtomCache = new Map<string, Atom<any>>();
const configBackgroundAtomCache = new Map<string, Atom<BackgroundConfigType>>();
const getWaveObjectAtom = <T extends WaveObj>(oref: string): PrimitiveAtom<T> => {
if (!waveObjectValueAtomCache.has(oref)) {
const obj = (mergedOverrides.mockWaveObjs?.[oref] ?? null) as T;
@ -461,7 +463,11 @@ export function makeMockWaveEnv(mockEnv?: MockEnv): MockWaveEnv {
globalStore.set(waveObjectValueAtomCache.get(oref), obj);
},
};
const { rpc, setRpcHandler, setRpcStreamHandler } = makeMockRpc(mergedOverrides.rpc, mergedOverrides.rpcStreaming, mockWosFns);
const { rpc, setRpcHandler, setRpcStreamHandler } = makeMockRpc(
mergedOverrides.rpc,
mergedOverrides.rpcStreaming,
mockWosFns
);
const env = {
isMock: true,
mockEnv: mergedOverrides,
@ -539,17 +545,36 @@ export function makeMockWaveEnv(mockEnv?: MockEnv): MockWaveEnv {
},
},
getBlockMetaKeyAtom: <T extends keyof MetaType>(blockId: string, key: T) => {
const cacheKey = blockId + "#meta-" + key;
if (!blockMetaKeyAtomCache.has(cacheKey)) {
if (blockId == null) {
return NullAtom as Atom<MetaType[T]>;
}
const oref = "block:" + blockId;
const cacheKey = oref + "#meta-" + key;
if (!orefMetaKeyAtomCache.has(cacheKey)) {
const metaAtom = atom<MetaType[T]>((get) => {
const blockORef = "block:" + blockId;
const blockAtom = env.wos.getWaveObjectAtom<Block>(blockORef);
const blockAtom = env.wos.getWaveObjectAtom<Block>(oref);
const blockData = get(blockAtom);
return blockData?.meta?.[key] as MetaType[T];
});
blockMetaKeyAtomCache.set(cacheKey, metaAtom);
orefMetaKeyAtomCache.set(cacheKey, metaAtom);
}
return blockMetaKeyAtomCache.get(cacheKey) as Atom<MetaType[T]>;
return orefMetaKeyAtomCache.get(cacheKey) as Atom<MetaType[T]>;
},
getTabMetaKeyAtom: <T extends keyof MetaType>(tabId: string, key: T) => {
if (tabId == null) {
return NullAtom as Atom<MetaType[T]>;
}
const oref = "tab:" + tabId;
const cacheKey = oref + "#meta-" + key;
if (!orefMetaKeyAtomCache.has(cacheKey)) {
const metaAtom = atom<MetaType[T]>((get) => {
const tabAtom = env.wos.getWaveObjectAtom<Tab>(oref);
const tabData = get(tabAtom);
return tabData?.meta?.[key] as MetaType[T];
});
orefMetaKeyAtomCache.set(cacheKey, metaAtom);
}
return orefMetaKeyAtomCache.get(cacheKey) as Atom<MetaType[T]>;
},
getConnConfigKeyAtom: <T extends keyof ConnKeywords>(connName: string, key: T) => {
const cacheKey = connName + "#conn-" + key;
@ -562,6 +587,19 @@ export function makeMockWaveEnv(mockEnv?: MockEnv): MockWaveEnv {
}
return connConfigKeyAtomCache.get(cacheKey) as Atom<ConnKeywords[T]>;
},
getConfigBackgroundAtom: (bgKey: string | null) => {
if (bgKey == null) return NullAtom as Atom<BackgroundConfigType>;
if (!configBackgroundAtomCache.has(bgKey)) {
configBackgroundAtomCache.set(
bgKey,
atom((get) => {
const fullConfig = get(atoms.fullConfigAtom);
return fullConfig.backgrounds?.[bgKey];
})
);
}
return configBackgroundAtomCache.get(bgKey);
},
services: null as any,
callBackendService: (service: string, method: string, args: any[], noUIContext?: boolean) => {
const fn = mergedOverrides.services?.[service]?.[method];

View file

@ -108,6 +108,17 @@ declare global {
iconcolor: string;
};
// wconfig.BackgroundConfigType
type BackgroundConfigType = {
bg?: string;
"bg:opacity"?: number;
"bg:blendmode"?: string;
"bg:bordercolor"?: string;
"bg:activebordercolor"?: string;
"display:name": string;
"display:order"?: number;
};
// baseds.Badge
type Badge = {
badgeid: string;
@ -991,6 +1002,7 @@ declare global {
defaultwidgets: {[key: string]: WidgetConfigType};
widgets: {[key: string]: WidgetConfigType};
presets: {[key: string]: MetaType};
backgrounds: {[key: string]: BackgroundConfigType};
termthemes: {[key: string]: TermThemeType};
connections: {[key: string]: ConnKeywords};
bookmarks: {[key: string]: WebBookmark};
@ -1129,6 +1141,7 @@ declare global {
"graph:metrics"?: string[];
"sysinfo:type"?: string;
"tab:flagcolor"?: string;
"tab:background"?: string;
"bg:*"?: boolean;
bg?: string;
"bg:opacity"?: number;
@ -1378,6 +1391,7 @@ declare global {
"preview:defaultsort"?: string;
"tab:preset"?: string;
"tab:confirmclose"?: boolean;
"tab:background"?: string;
"widget:*"?: boolean;
"widget:showhelp"?: boolean;
"window:*"?: boolean;

View file

@ -69,7 +69,7 @@ export function processBackgroundUrls(cssText: string): string {
return rtnStyle.replace(/^background:\s*/, "");
}
export function computeBgStyleFromMeta(meta: MetaType, defaultOpacity: number = null): React.CSSProperties {
export function computeBgStyleFromMeta(meta: Omit<BackgroundConfigType, "display:name">, defaultOpacity: number = null): React.CSSProperties {
const bgAttr = meta?.["bg"];
if (isBlank(bgAttr)) {
return null;

View file

@ -90,6 +90,7 @@ const (
MetaKey_SysinfoType = "sysinfo:type"
MetaKey_TabFlagColor = "tab:flagcolor"
MetaKey_TabBackground = "tab:background"
MetaKey_BgClear = "bg:*"
MetaKey_Bg = "bg"

View file

@ -93,6 +93,7 @@ type MetaTSType struct {
// for tabs
TabFlagColor string `json:"tab:flagcolor,omitempty"`
TabBackground string `json:"tab:background,omitempty"`
BgClear bool `json:"bg:*,omitempty"`
Bg string `json:"bg,omitempty"`
BgOpacity float64 `json:"bg:opacity,omitempty"`

View file

@ -0,0 +1,90 @@
{
"bg@rainbow": {
"display:name": "Rainbow",
"display:order": 2.1,
"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": "green",
"bg:opacity": 0.3
},
"bg@blue": {
"display:name": "Blue",
"display:order": 1.1,
"bg": "blue",
"bg:opacity": 0.3,
"bg:activebordercolor": "rgba(0, 0, 255, 1.0)"
},
"bg@red": {
"display:name": "Red",
"display:order": 1.3,
"bg": "red",
"bg:opacity": 0.3,
"bg:activebordercolor": "rgba(255, 0, 0, 1.0)"
},
"bg@ocean-depths": {
"display:name": "Ocean Depths",
"display:order": 2.2,
"bg": "linear-gradient(135deg, purple, blue, teal)",
"bg:opacity": 0.7
},
"bg@aqua-horizon": {
"display:name": "Aqua Horizon",
"display:order": 2.3,
"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": "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": "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": "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": "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": "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"
},
"bg@twilight-ember": {
"display:name": "Twilight Ember",
"display:order": 3.5,
"bg": "linear-gradient(120deg,hsla(350, 65%, 57%, 1),hsla(30,60%,60%, .75), hsla(208,69%,50%,.15), hsl(230,60%,40%)),radial-gradient(at top right,hsla(300,60%,70%,0.3),transparent),radial-gradient(at top left,hsla(330,100%,70%,.20),transparent),radial-gradient(at top right,hsla(190,100%,40%,.20),transparent),radial-gradient(at bottom left,hsla(323,54%,50%,.5),transparent),radial-gradient(at bottom left,hsla(144,54%,50%,.25),transparent)",
"bg:blendmode": "overlay",
"bg:text": "rgb(200, 200, 200)"
},
"bg@cosmic-tide": {
"display:name": "Cosmic Tide",
"display:order": 3.6,
"bg:activebordercolor": "#ff55aa",
"bg": "linear-gradient(135deg, #00d9d9, #ff55aa, #1e1e2f, #2f3b57, #ff99ff)",
"bg:opacity": 0.6
}
}

View file

@ -1,108 +1 @@
{
"bg@default": {
"display:name": "Default",
"display:order": -1,
"bg:*": true
},
"bg@rainbow": {
"display:name": "Rainbow",
"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:activebordercolor": "rgba(0, 0, 255, 1.0)"
},
"bg@red": {
"display:name": "Red",
"display:order": 1.3,
"bg:*": true,
"bg": "red",
"bg:opacity": 0.3,
"bg:activebordercolor": "rgba(255, 0, 0, 1.0)"
},
"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"
},
"bg@twilight-ember": {
"display:name": "Twilight Ember",
"display:order": 3.5,
"bg:*": true,
"bg": "linear-gradient(120deg,hsla(350, 65%, 57%, 1),hsla(30,60%,60%, .75), hsla(208,69%,50%,.15), hsl(230,60%,40%)),radial-gradient(at top right,hsla(300,60%,70%,0.3),transparent),radial-gradient(at top left,hsla(330,100%,70%,.20),transparent),radial-gradient(at top right,hsla(190,100%,40%,.20),transparent),radial-gradient(at bottom left,hsla(323,54%,50%,.5),transparent),radial-gradient(at bottom left,hsla(144,54%,50%,.25),transparent)",
"bg:blendmode": "overlay",
"bg:text": "rgb(200, 200, 200)"
},
"bg@cosmic-tide": {
"display:name": "Cosmic Tide",
"display:order": 3.6,
"bg:activebordercolor": "#ff55aa",
"bg:*": true,
"bg": "linear-gradient(135deg, #00d9d9, #ff55aa, #1e1e2f, #2f3b57, #ff99ff)",
"bg:opacity": 0.6
}
}
{}

View file

@ -85,6 +85,7 @@ const (
ConfigKey_TabPreset = "tab:preset"
ConfigKey_TabConfirmClose = "tab:confirmclose"
ConfigKey_TabBackground = "tab:background"
ConfigKey_WidgetClear = "widget:*"
ConfigKey_WidgetShowHelp = "widget:showhelp"

View file

@ -136,6 +136,7 @@ type SettingsType struct {
TabPreset string `json:"tab:preset,omitempty"`
TabConfirmClose bool `json:"tab:confirmclose,omitempty"`
TabBackground string `json:"tab:background,omitempty"`
WidgetClear bool `json:"widget:*,omitempty"`
WidgetShowHelp *bool `json:"widget:showhelp,omitempty"`
@ -308,17 +309,72 @@ type AIModeConfigUpdate struct {
Configs map[string]AIModeConfigType `json:"configs"`
}
type WidgetConfigType struct {
DisplayOrder float64 `json:"display:order,omitempty"`
DisplayHidden bool `json:"display:hidden,omitempty"`
Icon string `json:"icon,omitempty"`
Color string `json:"color,omitempty"`
Label string `json:"label,omitempty"`
Description string `json:"description,omitempty"`
Workspaces []string `json:"workspaces,omitempty"`
Magnified bool `json:"magnified,omitempty"`
BlockDef waveobj.BlockDef `json:"blockdef"`
}
type BackgroundConfigType struct {
Bg string `json:"bg,omitempty" jsonschema_description:"CSS background property value"`
BgOpacity float64 `json:"bg:opacity,omitempty" jsonschema_description:"Background opacity (0.0-1.0)"`
BgBlendMode string `json:"bg:blendmode,omitempty" jsonschema_description:"CSS background-blend-mode property value"`
BgBorderColor string `json:"bg:bordercolor,omitempty" jsonschema_description:"Block frame border color"`
BgActiveBorderColor string `json:"bg:activebordercolor,omitempty" jsonschema_description:"Block frame focused border color"`
DisplayName string `json:"display:name" jsonschema_description:"The name shown in the context menu"`
DisplayOrder float64 `json:"display:order,omitempty" jsonschema_description:"Determines the order of the background in the context menu"`
}
type MimeTypeConfigType struct {
Icon string `json:"icon"`
Color string `json:"color"`
}
type TermThemeType struct {
DisplayName string `json:"display:name"`
DisplayOrder float64 `json:"display:order"`
Black string `json:"black"`
Red string `json:"red"`
Green string `json:"green"`
Yellow string `json:"yellow"`
Blue string `json:"blue"`
Magenta string `json:"magenta"`
Cyan string `json:"cyan"`
White string `json:"white"`
BrightBlack string `json:"brightBlack"`
BrightRed string `json:"brightRed"`
BrightGreen string `json:"brightGreen"`
BrightYellow string `json:"brightYellow"`
BrightBlue string `json:"brightBlue"`
BrightMagenta string `json:"brightMagenta"`
BrightCyan string `json:"brightCyan"`
BrightWhite string `json:"brightWhite"`
Gray string `json:"gray"`
CmdText string `json:"cmdtext"`
Foreground string `json:"foreground"`
SelectionBackground string `json:"selectionBackground"`
Background string `json:"background"`
Cursor string `json:"cursor"`
}
type FullConfigType struct {
Settings SettingsType `json:"settings" merge:"meta"`
MimeTypes map[string]MimeTypeConfigType `json:"mimetypes"`
DefaultWidgets map[string]WidgetConfigType `json:"defaultwidgets"`
Widgets map[string]WidgetConfigType `json:"widgets"`
Presets map[string]waveobj.MetaMapType `json:"presets"`
TermThemes map[string]TermThemeType `json:"termthemes"`
Connections map[string]ConnKeywords `json:"connections"`
Bookmarks map[string]WebBookmark `json:"bookmarks"`
WaveAIModes map[string]AIModeConfigType `json:"waveai"`
ConfigErrors []ConfigError `json:"configerrors" configfile:"-"`
Settings SettingsType `json:"settings" merge:"meta"`
MimeTypes map[string]MimeTypeConfigType `json:"mimetypes"`
DefaultWidgets map[string]WidgetConfigType `json:"defaultwidgets"`
Widgets map[string]WidgetConfigType `json:"widgets"`
Presets map[string]waveobj.MetaMapType `json:"presets"`
Backgrounds map[string]BackgroundConfigType `json:"backgrounds"`
TermThemes map[string]TermThemeType `json:"termthemes"`
Connections map[string]ConnKeywords `json:"connections"`
Bookmarks map[string]WebBookmark `json:"bookmarks"`
WaveAIModes map[string]AIModeConfigType `json:"waveai"`
ConfigErrors []ConfigError `json:"configerrors" configfile:"-"`
}
type ConnKeywords struct {
@ -837,59 +893,47 @@ func SetConnectionsConfigValue(connName string, toMerge waveobj.MetaMapType) err
return WriteWaveHomeConfigFile(ConnectionsFile, m)
}
type WidgetConfigType struct {
DisplayOrder float64 `json:"display:order,omitempty"`
DisplayHidden bool `json:"display:hidden,omitempty"`
Icon string `json:"icon,omitempty"`
Color string `json:"color,omitempty"`
Label string `json:"label,omitempty"`
Description string `json:"description,omitempty"`
Workspaces []string `json:"workspaces,omitempty"`
Magnified bool `json:"magnified,omitempty"`
BlockDef waveobj.BlockDef `json:"blockdef"`
}
type BgPresetsType struct {
BgClear bool `json:"bg:*,omitempty"`
Bg string `json:"bg,omitempty" jsonschema_description:"CSS background property value"`
BgOpacity float64 `json:"bg:opacity,omitempty" jsonschema_description:"Background opacity (0.0-1.0)"`
BgBlendMode string `json:"bg:blendmode,omitempty" jsonschema_description:"CSS background-blend-mode property value"`
BgBorderColor string `json:"bg:bordercolor,omitempty" jsonschema_description:"Block frame border color"`
BgActiveBorderColor string `json:"bg:activebordercolor,omitempty" jsonschema_description:"Block frame focused border color"`
DisplayName string `json:"display:name,omitempty" jsonschema_description:"The name shown in the context menu"`
DisplayOrder float64 `json:"display:order,omitempty" jsonschema_description:"Determines the order of the background in the context menu"`
}
type MimeTypeConfigType struct {
Icon string `json:"icon"`
Color string `json:"color"`
}
type TermThemeType struct {
DisplayName string `json:"display:name"`
DisplayOrder float64 `json:"display:order"`
Black string `json:"black"`
Red string `json:"red"`
Green string `json:"green"`
Yellow string `json:"yellow"`
Blue string `json:"blue"`
Magenta string `json:"magenta"`
Cyan string `json:"cyan"`
White string `json:"white"`
BrightBlack string `json:"brightBlack"`
BrightRed string `json:"brightRed"`
BrightGreen string `json:"brightGreen"`
BrightYellow string `json:"brightYellow"`
BrightBlue string `json:"brightBlue"`
BrightMagenta string `json:"brightMagenta"`
BrightCyan string `json:"brightCyan"`
BrightWhite string `json:"brightWhite"`
Gray string `json:"gray"`
CmdText string `json:"cmdtext"`
Foreground string `json:"foreground"`
SelectionBackground string `json:"selectionBackground"`
Background string `json:"background"`
Cursor string `json:"cursor"`
func MigratePresetsBackgrounds() {
configDirAbsPath := wavebase.GetWaveConfigDir()
backgroundsFile := filepath.Join(configDirAbsPath, "backgrounds.json")
if _, err := os.Stat(backgroundsFile); err == nil {
return
} else if !os.IsNotExist(err) {
log.Printf("error checking backgrounds.json during migration: %v\n", err)
return
}
bgFile := filepath.Join(configDirAbsPath, "presets", "bg.json")
bgData, err := os.ReadFile(bgFile)
if err != nil {
if !os.IsNotExist(err) {
log.Printf("error reading presets/bg.json for migration: %v\n", err)
}
return
}
var rawMap map[string]json.RawMessage
if err := json.Unmarshal(bgData, &rawMap); err != nil {
log.Printf("error parsing presets/bg.json for migration: %v\n", err)
return
}
filtered := make(map[string]json.RawMessage)
for k, v := range rawMap {
if strings.HasPrefix(k, "bg@") {
filtered[k] = v
}
}
if len(filtered) == 0 {
return
}
outBarr, err := json.MarshalIndent(filtered, "", " ")
if err != nil {
log.Printf("error marshaling backgrounds.json during migration: %v\n", err)
return
}
if err := fileutil.AtomicWriteFile(backgroundsFile, outBarr, 0644); err != nil {
log.Printf("error writing backgrounds.json during migration: %v\n", err)
return
}
log.Printf("migrated %d background presets from presets/bg.json to backgrounds.json\n", len(filtered))
}
// CountCustomWidgets returns the number of custom widgets the user has defined.

View file

@ -187,14 +187,12 @@ func GetWorkspace(ctx context.Context, wsID string) (*waveobj.Workspace, error)
return wstore.DBMustGet[*waveobj.Workspace](ctx, wsID)
}
func getTabPresetMeta() (waveobj.MetaMapType, error) {
settings := wconfig.GetWatcher().GetFullConfig()
tabPreset := settings.Settings.TabPreset
if tabPreset == "" {
return nil, nil
func getTabBackground() string {
config := wconfig.GetWatcher().GetFullConfig()
if config.Settings.TabBackground != "" {
return config.Settings.TabBackground
}
presetMeta := settings.Presets[tabPreset]
return presetMeta, nil
return config.Settings.TabPreset
}
var tabNameRe = regexp.MustCompile(`^T(\d+)$`)
@ -256,12 +254,10 @@ func CreateTab(ctx context.Context, workspaceId string, tabName string, activate
if err != nil {
return tab.OID, fmt.Errorf("error applying new tab layout: %w", err)
}
presetMeta, presetErr := getTabPresetMeta()
if presetErr != nil {
log.Printf("error getting tab preset meta: %v\n", presetErr)
} else if len(presetMeta) > 0 {
tabBg := getTabBackground()
if tabBg != "" {
tabORef := waveobj.ORefFromWaveObj(tab)
wstore.UpdateObjectMeta(ctx, *tabORef, presetMeta, true)
wstore.UpdateObjectMeta(ctx, *tabORef, waveobj.MetaMapType{waveobj.MetaKey_TabBackground: tabBg}, false)
}
}
telemetry.GoUpdateActivityWrap(wshrpc.ActivityUpdate{NewTab: 1}, "createtab")

View file

@ -1,11 +1,8 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$defs": {
"BgPresetsType": {
"BackgroundConfigType": {
"properties": {
"bg:*": {
"type": "boolean"
},
"bg": {
"type": "string",
"description": "CSS background property value"
@ -36,11 +33,21 @@
}
},
"additionalProperties": false,
"type": "object"
"type": "object",
"required": [
"display:name"
]
}
},
"additionalProperties": {
"$ref": "#/$defs/BgPresetsType"
"anyOf": [
{
"$ref": "#/$defs/BackgroundConfigType"
},
{
"type": "null"
}
]
},
"type": "object"
}

View file

@ -232,6 +232,9 @@
"tab:confirmclose": {
"type": "boolean"
},
"tab:background": {
"type": "string"
},
"widget:*": {
"type": "boolean"
},

View file

@ -224,7 +224,14 @@
}
},
"additionalProperties": {
"$ref": "#/$defs/WidgetConfigType"
"anyOf": [
{
"$ref": "#/$defs/WidgetConfigType"
},
{
"type": "null"
}
]
},
"type": "object"
}

View file

@ -1,5 +1,6 @@
{
"include": ["frontend/**/*", "emain/**/*"],
"exclude": ["node_modules"],
"compilerOptions": {
"target": "es6",
"module": "es2020",