diff --git a/.kilocode/skills/waveenv/SKILL.md b/.kilocode/skills/waveenv/SKILL.md index aabda6846d..c5d56af4f1 100644 --- a/.kilocode/skills/waveenv/SKILL.md +++ b/.kilocode/skills/waveenv/SKILL.md @@ -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` 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. | diff --git a/cmd/generateschema/main-generateschema.go b/cmd/generateschema/main-generateschema.go index 2b14a95781..dd24a4df0d 100644 --- a/cmd/generateschema/main-generateschema.go +++ b/cmd/generateschema/main-generateschema.go @@ -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) } diff --git a/cmd/server/main-server.go b/cmd/server/main-server.go index 70c8b3a005..a15b05b1e2 100644 --- a/cmd/server/main-server.go +++ b/cmd/server/main-server.go @@ -560,6 +560,7 @@ func main() { createMainWshClient() sigutil.InstallShutdownSignalHandlers(doShutdown) sigutil.InstallSIGUSR1Handler() + wconfig.MigratePresetsBackgrounds() startConfigWatcher() aiusechat.InitAIModeConfigWatcher() maybeStartPprofServer() diff --git a/cmd/wsh/cmd/wshcmd-setbg.go b/cmd/wsh/cmd/wshcmd-setbg.go index fb5cf0fec0..4385409187 100644 --- a/cmd/wsh/cmd/wshcmd-setbg.go +++ b/cmd/wsh/cmd/wshcmd-setbg.go @@ -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 { diff --git a/docs/docs/config.mdx b/docs/docs/config.mdx index ae83638ea5..abeca3429f 100644 --- a/docs/docs/config.mdx +++ b/docs/docs/config.mdx @@ -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"; @@ -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 | 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 | 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)) | diff --git a/docs/docs/customization.mdx b/docs/docs/customization.mdx index 02fedca70a..e393c8fdb9 100644 --- a/docs/docs/customization.mdx +++ b/docs/docs/customization.mdx @@ -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. diff --git a/docs/docs/releasenotes.mdx b/docs/docs/releasenotes.mdx index 4d985607d0..adc2ce6fe7 100644 --- a/docs/docs/releasenotes.mdx +++ b/docs/docs/releasenotes.mdx @@ -479,7 +479,7 @@ New minor release that introduces Wave's connected computing extensions. We've i ### v0.9.2 — 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. diff --git a/docs/docs/presets.mdx b/docs/docs/tab-backgrounds.mdx similarity index 54% rename from docs/docs/presets.mdx rename to docs/docs/tab-backgrounds.mdx index 31fc5f57d7..77c02a2bb4 100644 --- a/docs/docs/presets.mdx +++ b/docs/docs/tab-backgrounds.mdx @@ -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: - -- Tab backgrounds: Apply visual styles to your tabs - -## Managing Presets - -You can store presets in two locations: +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. -- `~/.config/waveterm/presets.json`: Main presets file -- `~/.config/waveterm/presets/`: Directory for organizing presets into separate files +## Managing Backgrounds -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`). +Custom backgrounds are stored in `~/.config/waveterm/backgrounds.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 { - "@": { - "display:name": "", - "display:order": "", // optional - "": "" - ... + "bg@": { + "display:name": "", + "display:order": , + "bg": "", + "bg:opacity": } } ``` -The `preset-type` determines where the preset appears in Wave's interface: - -- `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. +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). -### 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@` wrapper to complete your background entry! ::: diff --git a/docs/docs/wsh-reference.mdx b/docs/docs/wsh-reference.mdx index 6ff8c2e8f5..6ed1bcaa3f 100644 --- a/docs/docs/wsh-reference.mdx +++ b/docs/docs/wsh-reference.mdx @@ -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) ::: --- diff --git a/docs/src/components/versionbadge.css b/docs/src/components/versionbadge.css index 63ac0b3771..ea09d08480 100644 --- a/docs/src/components/versionbadge.css +++ b/docs/src/components/versionbadge.css @@ -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; +} diff --git a/docs/src/components/versionbadge.tsx b/docs/src/components/versionbadge.tsx index 58c616440c..c4af6d479f 100644 --- a/docs/src/components/versionbadge.tsx +++ b/docs/src/components/versionbadge.tsx @@ -7,4 +7,8 @@ interface VersionBadgeProps { export function VersionBadge({ version, noLeftMargin }: VersionBadgeProps) { return {version}; +} + +export function DeprecatedBadge() { + return deprecated; } \ No newline at end of file diff --git a/frontend/app/aipanel/aipanel.tsx b/frontend/app/aipanel/aipanel.tsx index b903380544..32b8582141 100644 --- a/frontend/app/aipanel/aipanel.tsx +++ b/frontend/app/aipanel/aipanel.tsx @@ -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(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 (
; + getConfigBackgroundAtom: WaveEnv["getConfigBackgroundAtom"]; +}>; + export function AppBackground() { const bgRef = useRef(null); const tabId = useAtomValue(atoms.staticTabId); const [tabData] = useWaveObjectValue(WOS.makeORef("tab", tabId)); - const style: CSSProperties = computeBgStyleFromMeta(tabData?.meta, 0.5) ?? {}; + const env = useWaveEnv(); + const tabBg = useAtomValue(env.getTabMetaKeyAtom(tabId, "tab:background")); + const configBg = useAtomValue(env.getConfigBackgroundAtom(tabBg)); + const resolvedMeta: Omit = 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
; + return ( +
+ ); } diff --git a/frontend/app/block/blockenv.ts b/frontend/app/block/blockenv.ts index 000228c014..f4eebb192d 100644 --- a/frontend/app/block/blockenv.ts +++ b/frontend/app/block/blockenv.ts @@ -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"]; }>; diff --git a/frontend/app/block/blockframe.tsx b/frontend/app/block/blockframe.tsx index 0b4abb755b..8ff2e2d0a7 100644 --- a/frontend/app/block/blockframe.tsx +++ b/frontend/app/block/blockframe.tsx @@ -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(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); }); diff --git a/frontend/app/block/blockutil.tsx b/frontend/app/block/blockutil.tsx index 01346183a0..92d976400f 100644 --- a/frontend/app/block/blockutil.tsx +++ b/frontend/app/block/blockutil.tsx @@ -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; diff --git a/frontend/app/monaco/schemaendpoints.ts b/frontend/app/monaco/schemaendpoints.ts index 2b3134e215..5365d1c739 100644 --- a/frontend/app/monaco/schemaendpoints.ts +++ b/frontend/app/monaco/schemaendpoints.ts @@ -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", diff --git a/frontend/app/store/global.ts b/frontend/app/store/global.ts index 92ffe7a59b..acc0f4d518 100644 --- a/frontend/app/store/global.ts +++ b/frontend/app/store/global.ts @@ -132,6 +132,10 @@ function getBlockMetaKeyAtom(blockId: string, key: T): return metaAtom; } +function getTabMetaKeyAtom(tabId: string, key: T): Atom { + return getOrefMetaKeyAtom(WOS.makeORef("tab", tabId), key); +} + function getOrefMetaKeyAtom(oref: string, key: T): Atom { const orefCache = getSingleOrefAtomCache(oref); const metaAtomName = "#meta-" + key; @@ -229,6 +233,21 @@ function useSettingsKeyAtom(key: T): SettingsType[ return useAtomValue(getSettingsKeyAtom(key)); } +const configBackgroundAtomCache = new Map>(); + +function getConfigBackgroundAtom(bgKey: string | null): Atom { + if (isPreviewWindow() || bgKey == null) return NullAtom as Atom; + 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 { if (isPreviewWindow()) return NullAtom as Atom; let settingsPrefixAtom = settingsAtomCache.get(prefix + ":"); @@ -666,6 +685,8 @@ export { getBlockComponentModel, getBlockMetaKeyAtom, getBlockTermDurableAtom, + getTabMetaKeyAtom, + getConfigBackgroundAtom, getConnConfigKeyAtom, getConnStatusAtom, getFocusedBlockId, diff --git a/frontend/app/tab/tabcontextmenu.ts b/frontend/app/tab/tabcontextmenu.ts index 5f70bc9b9b..bc87302d4c 100644 --- a/frontend/app/tab/tabcontextmenu.ts +++ b/frontend/app/tab/tabcontextmenu.ts @@ -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"); }), diff --git a/frontend/app/view/codeeditor/codeeditor.tsx b/frontend/app/view/codeeditor/codeeditor.tsx index 48461887b5..ba1e28666e 100644 --- a/frontend/app/view/codeeditor/codeeditor.tsx +++ b/frontend/app/view/codeeditor/codeeditor.tsx @@ -93,7 +93,7 @@ export function CodeEditor({ blockId, text, language, fileName, readonly, onChan }, [minimapEnabled, stickyScrollEnabled, wordWrap, fontSize, readonly]); return ( -
+
; + getBlockMetaKeyAtom: MetaKeyAtomFnType<"graph:numpoints" | "sysinfo:type" | "connection" | "count">; }>; const DefaultNumPoints = 120; diff --git a/frontend/app/view/term/fitaddon.ts b/frontend/app/view/term/fitaddon.ts index d22b5577a7..3540a792f9 100644 --- a/frontend/app/view/term/fitaddon.ts +++ b/frontend/app/view/term/fitaddon.ts @@ -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 { /** diff --git a/frontend/app/view/waveconfig/waveconfig-model.ts b/frontend/app/view/waveconfig/waveconfig-model.ts index 35c2d2ea11..aa3688b684 100644 --- a/frontend/app/view/waveconfig/waveconfig-model.ts +++ b/frontend/app/view/waveconfig/waveconfig-model.ts @@ -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, }, { diff --git a/frontend/app/view/waveconfig/waveconfig.tsx b/frontend/app/view/waveconfig/waveconfig.tsx index 8dfa6ad25d..058d5be85d 100644 --- a/frontend/app/view/waveconfig/waveconfig.tsx +++ b/frontend/app/view/waveconfig/waveconfig.tsx @@ -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
- {isMenuOpen && ( -
setIsMenuOpen(false)} /> - )} -
- -
-
- {selectedFile && ( - <> -
-
- -
- {selectedFile.name} -
- {selectedFile.docsUrl && ( - - - - - - )} -
- {selectedFile.path} -
-
-
- {selectedFile.hasJsonView && ( - <> - {hasChanges && ( - - Unsaved changes - - )} - - +
+ {selectedFile.name} +
+ {selectedFile.docsUrl && ( + + - {isSaving ? "Saving..." : "Save"} - + + - - )} -
-
- {selectedFile.visualComponent && selectedFile.hasJsonView && ( -
- -
+
+ {selectedFile.hasJsonView && ( + <> + {hasChanges && ( + + Unsaved changes + + )} + + + + )} - > - Raw JSON - -
- )} - {errorMessage && ( -
- {errorMessage} - -
- )} - {validationError && ( -
- {validationError} - +
- )} -
- {isLoading ? ( -
- Loading... + {selectedFile.visualComponent && selectedFile.hasJsonView && ( +
+ +
- ) : selectedFile.visualComponent && - (!selectedFile.hasJsonView || activeTab === "visual") ? ( - (() => { - const VisualComponent = selectedFile.visualComponent; - return ; - })() - ) : ( - )} -
- - )} -
+ {errorMessage && ( +
+ {errorMessage} + +
+ )} + {validationError && ( +
+ {validationError} + +
+ )} +
+ {isLoading ? ( +
+ Loading... +
+ ) : selectedFile.visualComponent && + (!selectedFile.hasJsonView || activeTab === "visual") ? ( + (() => { + const VisualComponent = selectedFile.visualComponent; + return ; + })() + ) : ( + + )} +
+ + )} +
{configErrors?.length > 0 && (
diff --git a/frontend/app/view/waveconfig/waveconfigenv.ts b/frontend/app/view/waveconfig/waveconfigenv.ts index 5e2937c19b..c76352cbde 100644 --- a/frontend/app/view/waveconfig/waveconfigenv.ts +++ b/frontend/app/view/waveconfig/waveconfigenv.ts @@ -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"]; }>; diff --git a/frontend/app/view/webview/webviewenv.ts b/frontend/app/view/webview/webviewenv.ts index 268e4c9e5d..419b04c4eb 100644 --- a/frontend/app/view/webview/webviewenv.ts +++ b/frontend/app/view/webview/webviewenv.ts @@ -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" >; }>; diff --git a/frontend/app/waveenv/waveenv.ts b/frontend/app/waveenv/waveenv.ts index df1cb01c4a..3b024d2f95 100644 --- a/frontend/app/waveenv/waveenv.ts +++ b/frontend/app/waveenv/waveenv.ts @@ -6,8 +6,8 @@ import { RpcApiType } from "@/app/store/wshclientapi"; import { Atom, PrimitiveAtom } from "jotai"; import React from "react"; -export type BlockMetaKeyAtomFnType = ( - blockId: string, +export type MetaKeyAtomFnType = ( + id: string, key: T ) => Atom; @@ -74,8 +74,10 @@ export type WaveEnv = { useWaveObjectValue: (oref: string) => [T, boolean]; }; getSettingsKeyAtom: SettingsKeyAtomFnType; - getBlockMetaKeyAtom: BlockMetaKeyAtomFnType; + getBlockMetaKeyAtom: MetaKeyAtomFnType; + getTabMetaKeyAtom: MetaKeyAtomFnType; getConnConfigKeyAtom: ConnConfigKeyAtomFnType; + getConfigBackgroundAtom: (bgKey: string | null) => Atom; // the mock fields are only usable in the preview server (may be be null or throw errors in production) mockSetWaveObj: (oref: string, obj: T) => void; diff --git a/frontend/app/waveenv/waveenvimpl.ts b/frontend/app/waveenv/waveenvimpl.ts index 4f9e234eca..6abe00e574 100644 --- a/frontend/app/waveenv/waveenvimpl.ts +++ b/frontend/app/waveenv/waveenvimpl.ts @@ -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: (_oref: string, _obj: T) => { diff --git a/frontend/preview/mock/defaultconfig.ts b/frontend/preview/mock/defaultconfig.ts index 0c2ac11b3a..415630b2b6 100644 --- a/frontend/preview/mock/defaultconfig.ts +++ b/frontend/preview/mock/defaultconfig.ts @@ -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: [], }; diff --git a/frontend/preview/mock/mockwaveenv.ts b/frontend/preview/mock/mockwaveenv.ts index faaf6cdde7..69827a09b4 100644 --- a/frontend/preview/mock/mockwaveenv.ts +++ b/frontend/preview/mock/mockwaveenv.ts @@ -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>(); const waveObjectValueAtomCache = new Map>(); const waveObjectDerivedAtomCache = new Map>(); - const blockMetaKeyAtomCache = new Map>(); + const orefMetaKeyAtomCache = new Map>(); const connConfigKeyAtomCache = new Map>(); + const configBackgroundAtomCache = new Map>(); const getWaveObjectAtom = (oref: string): PrimitiveAtom => { 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: (blockId: string, key: T) => { - const cacheKey = blockId + "#meta-" + key; - if (!blockMetaKeyAtomCache.has(cacheKey)) { + if (blockId == null) { + return NullAtom as Atom; + } + const oref = "block:" + blockId; + const cacheKey = oref + "#meta-" + key; + if (!orefMetaKeyAtomCache.has(cacheKey)) { const metaAtom = atom((get) => { - const blockORef = "block:" + blockId; - const blockAtom = env.wos.getWaveObjectAtom(blockORef); + const blockAtom = env.wos.getWaveObjectAtom(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; + return orefMetaKeyAtomCache.get(cacheKey) as Atom; + }, + getTabMetaKeyAtom: (tabId: string, key: T) => { + if (tabId == null) { + return NullAtom as Atom; + } + const oref = "tab:" + tabId; + const cacheKey = oref + "#meta-" + key; + if (!orefMetaKeyAtomCache.has(cacheKey)) { + const metaAtom = atom((get) => { + const tabAtom = env.wos.getWaveObjectAtom(oref); + const tabData = get(tabAtom); + return tabData?.meta?.[key] as MetaType[T]; + }); + orefMetaKeyAtomCache.set(cacheKey, metaAtom); + } + return orefMetaKeyAtomCache.get(cacheKey) as Atom; }, getConnConfigKeyAtom: (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; }, + getConfigBackgroundAtom: (bgKey: string | null) => { + if (bgKey == null) return NullAtom as Atom; + 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]; diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts index 193929ef4c..9032d3c4fa 100644 --- a/frontend/types/gotypes.d.ts +++ b/frontend/types/gotypes.d.ts @@ -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; diff --git a/frontend/util/waveutil.ts b/frontend/util/waveutil.ts index d22f5a4896..4d0f5952fc 100644 --- a/frontend/util/waveutil.ts +++ b/frontend/util/waveutil.ts @@ -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, defaultOpacity: number = null): React.CSSProperties { const bgAttr = meta?.["bg"]; if (isBlank(bgAttr)) { return null; diff --git a/pkg/waveobj/metaconsts.go b/pkg/waveobj/metaconsts.go index 7028d050be..f41435954c 100644 --- a/pkg/waveobj/metaconsts.go +++ b/pkg/waveobj/metaconsts.go @@ -90,6 +90,7 @@ const ( MetaKey_SysinfoType = "sysinfo:type" MetaKey_TabFlagColor = "tab:flagcolor" + MetaKey_TabBackground = "tab:background" MetaKey_BgClear = "bg:*" MetaKey_Bg = "bg" diff --git a/pkg/waveobj/wtypemeta.go b/pkg/waveobj/wtypemeta.go index 027ff3eff2..4a36fdd46f 100644 --- a/pkg/waveobj/wtypemeta.go +++ b/pkg/waveobj/wtypemeta.go @@ -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"` diff --git a/pkg/wconfig/defaultconfig/backgrounds.json b/pkg/wconfig/defaultconfig/backgrounds.json new file mode 100644 index 0000000000..ab044b9246 --- /dev/null +++ b/pkg/wconfig/defaultconfig/backgrounds.json @@ -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 + } +} diff --git a/pkg/wconfig/defaultconfig/presets.json b/pkg/wconfig/defaultconfig/presets.json index 3d7cc1ad3b..0967ef424b 100644 --- a/pkg/wconfig/defaultconfig/presets.json +++ b/pkg/wconfig/defaultconfig/presets.json @@ -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 - } -} +{} diff --git a/pkg/wconfig/metaconsts.go b/pkg/wconfig/metaconsts.go index 28f481ac16..df048b304c 100644 --- a/pkg/wconfig/metaconsts.go +++ b/pkg/wconfig/metaconsts.go @@ -85,6 +85,7 @@ const ( ConfigKey_TabPreset = "tab:preset" ConfigKey_TabConfirmClose = "tab:confirmclose" + ConfigKey_TabBackground = "tab:background" ConfigKey_WidgetClear = "widget:*" ConfigKey_WidgetShowHelp = "widget:showhelp" diff --git a/pkg/wconfig/settingsconfig.go b/pkg/wconfig/settingsconfig.go index c52012803d..b55cab8cbf 100644 --- a/pkg/wconfig/settingsconfig.go +++ b/pkg/wconfig/settingsconfig.go @@ -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. diff --git a/pkg/wcore/workspace.go b/pkg/wcore/workspace.go index b070d31107..c01e509a13 100644 --- a/pkg/wcore/workspace.go +++ b/pkg/wcore/workspace.go @@ -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 - } - presetMeta := settings.Presets[tabPreset] - return presetMeta, nil +func getTabBackground() string { + config := wconfig.GetWatcher().GetFullConfig() + if config.Settings.TabBackground != "" { + return config.Settings.TabBackground + } + 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") diff --git a/schema/bgpresets.json b/schema/backgrounds.json similarity index 82% rename from schema/bgpresets.json rename to schema/backgrounds.json index 3ab0a8a433..298bdbf2a3 100644 --- a/schema/bgpresets.json +++ b/schema/backgrounds.json @@ -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" } \ No newline at end of file diff --git a/schema/settings.json b/schema/settings.json index eda7dc3326..67d8f5b9d4 100644 --- a/schema/settings.json +++ b/schema/settings.json @@ -232,6 +232,9 @@ "tab:confirmclose": { "type": "boolean" }, + "tab:background": { + "type": "string" + }, "widget:*": { "type": "boolean" }, diff --git a/schema/widgets.json b/schema/widgets.json index a4e6adb16a..1c55fd8e09 100644 --- a/schema/widgets.json +++ b/schema/widgets.json @@ -224,7 +224,14 @@ } }, "additionalProperties": { - "$ref": "#/$defs/WidgetConfigType" + "anyOf": [ + { + "$ref": "#/$defs/WidgetConfigType" + }, + { + "type": "null" + } + ] }, "type": "object" } \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 3ef02e0671..8fd50d2f96 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,6 @@ { "include": ["frontend/**/*", "emain/**/*"], + "exclude": ["node_modules"], "compilerOptions": { "target": "es6", "module": "es2020",