diff --git a/docs/docs/config.mdx b/docs/docs/config.mdx index 1b27b5cc96..2622ddc52a 100644 --- a/docs/docs/config.mdx +++ b/docs/docs/config.mdx @@ -95,34 +95,34 @@ wsh editconfig | window:dimensions | string | set the default dimensions for new windows using the format "WIDTHxHEIGHT" (e.g. "1920x1080"). when a new window is created, these dimensions will be automatically applied. The width and height values should be specified in pixels. | | telemetry:enabled | bool | set to enable/disable telemetry | -For reference, this is the current default configuration (v0.10.4): +For reference, this is the current default configuration (v0.11.5): ```json { - "ai:preset": "ai@global", - "ai:model": "gpt-4o-mini", - "ai:maxtokens": 2048, - "ai:timeoutms": 60000, - "app:defaultnewblock": "term", - "autoupdate:enabled": true, - "autoupdate:installonquit": true, - "autoupdate:intervalms": 3600000, - "conn:askbeforewshinstall": true, - "conn:wshenabled": true, - "editor:minimapenabled": true, - "web:defaulturl": "https://github.com/wavetermdev/waveterm", - "web:defaultsearch": "https://www.google.com/search?q={query}", - "window:tilegapsize": 3, - "window:maxtabcachesize": 10, - "window:nativetitlebar": true, - "window:magnifiedblockopacity": 0.6, - "window:magnifiedblocksize": 0.9, - "window:magnifiedblockblurprimarypx": 10, - "window:magnifiedblockblursecondarypx": 2, - "window:confirmclose": true, - "window:savelastwindow": true, - "telemetry:enabled": true, - "term:copyonselect": true + "ai:preset": "ai@global", + "ai:model": "gpt-4o-mini", + "ai:maxtokens": 2048, + "ai:timeoutms": 60000, + "app:defaultnewblock": "term", + "autoupdate:enabled": true, + "autoupdate:installonquit": true, + "autoupdate:intervalms": 3600000, + "conn:askbeforewshinstall": true, + "conn:wshenabled": true, + "editor:minimapenabled": true, + "web:defaulturl": "https://github.com/wavetermdev/waveterm", + "web:defaultsearch": "https://www.google.com/search?q={query}", + "window:tilegapsize": 3, + "window:maxtabcachesize": 10, + "window:nativetitlebar": true, + "window:magnifiedblockopacity": 0.6, + "window:magnifiedblocksize": 0.9, + "window:magnifiedblockblurprimarypx": 10, + "window:magnifiedblockblursecondarypx": 2, + "window:confirmclose": true, + "window:savelastwindow": true, + "telemetry:enabled": true, + "term:copyonselect": true } ``` @@ -134,6 +134,17 @@ files as well: `termthemes.json`, `presets.json`, and `widgets.json`. ::: +## Environment Variable Resolution + +To avoid putting secrets directly in config files, Wave supports environment variable resolution using `$ENV:VARIABLE_NAME` or `$ENV:VARIABLE_NAME:fallback` syntax. This works for any string value in any config file (settings.json, presets.json, ai.json, etc.). + +```json +{ + "ai:apitoken": "$ENV:OPENAI_APIKEY", + "ai:baseurl": "$ENV:AI_BASEURL:https://api.openai.com/v1" +} +``` + ## WebBookmarks Configuration WebBookmarks allows you to store and manage web links with customizable display preferences. The bookmarks are stored in a JSON file (`bookmarks.json`) as a key-value map where the key (`id`) is an arbitrary identifier for the bookmark. By convention, you should start your ids with "bookmark@". In the web widget, you can pull up your bookmarks using diff --git a/pkg/wconfig/settingsconfig.go b/pkg/wconfig/settingsconfig.go index a6822a61da..5c33e69fe9 100644 --- a/pkg/wconfig/settingsconfig.go +++ b/pkg/wconfig/settingsconfig.go @@ -327,6 +327,65 @@ func isTrailingCommaError(barr []byte, offset int) bool { return false } +func resolveEnvReplacements(m waveobj.MetaMapType) { + if m == nil { + return + } + + for key, value := range m { + switch v := value.(type) { + case string: + if resolved, ok := resolveEnvValue(v); ok { + m[key] = resolved + } + case map[string]interface{}: + resolveEnvReplacements(waveobj.MetaMapType(v)) + case []interface{}: + resolveEnvArray(v) + } + } +} + +func resolveEnvArray(arr []interface{}) { + for i, value := range arr { + switch v := value.(type) { + case string: + if resolved, ok := resolveEnvValue(v); ok { + arr[i] = resolved + } + case map[string]interface{}: + resolveEnvReplacements(waveobj.MetaMapType(v)) + case []interface{}: + resolveEnvArray(v) + } + } +} + +func resolveEnvValue(value string) (string, bool) { + if !strings.HasPrefix(value, "$ENV:") { + return "", false + } + + envSpec := value[5:] // Remove "$ENV:" prefix + parts := strings.SplitN(envSpec, ":", 2) + envVar := parts[0] + var fallback string + if len(parts) > 1 { + fallback = parts[1] + } + + // Get the environment variable value + if envValue, exists := os.LookupEnv(envVar); exists { + return envValue, true + } + + // Return fallback if provided, otherwise return empty string + if fallback != "" { + return fallback, true + } + return "", true +} + func readConfigHelper(fileName string, barr []byte, readErr error) (waveobj.MetaMapType, []ConfigError) { var cerrs []ConfigError if readErr != nil && !os.IsNotExist(readErr) { @@ -353,6 +412,12 @@ func readConfigHelper(fileName string, barr []byte, readErr error) (waveobj.Meta } cerrs = append(cerrs, ConfigError{File: fileName, Err: err.Error()}) } + + // Resolve environment variable replacements + if rtn != nil { + resolveEnvReplacements(rtn) + } + return rtn, cerrs }