mirror of
https://github.com/wavetermdev/waveterm.git
synced 2024-12-21 16:38:23 +01:00
51bd45bd2b
Sets up a configurable global hotkey to focus the last window used in the application. Note that this is established at startup and configuration changes will not be applied until rebooting the app.
563 lines
18 KiB
Go
563 lines
18 KiB
Go
// Copyright 2024, Command Line Inc.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package wconfig
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/fs"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/wavetermdev/waveterm/pkg/util/utilfn"
|
|
"github.com/wavetermdev/waveterm/pkg/wavebase"
|
|
"github.com/wavetermdev/waveterm/pkg/waveobj"
|
|
"github.com/wavetermdev/waveterm/pkg/wconfig/defaultconfig"
|
|
"github.com/wavetermdev/waveterm/pkg/wshrpc"
|
|
)
|
|
|
|
const SettingsFile = "settings.json"
|
|
const ConnectionsFile = "connections.json"
|
|
|
|
const AnySchema = `
|
|
{
|
|
"type": "object",
|
|
"additionalProperties": true
|
|
}
|
|
`
|
|
|
|
type SettingsType struct {
|
|
AiClear bool `json:"ai:*,omitempty"`
|
|
AiPreset string `json:"ai:preset,omitempty"`
|
|
AiApiType string `json:"ai:apitype,omitempty"`
|
|
AiBaseURL string `json:"ai:baseurl,omitempty"`
|
|
AiApiToken string `json:"ai:apitoken,omitempty"`
|
|
AiName string `json:"ai:name,omitempty"`
|
|
AiModel string `json:"ai:model,omitempty"`
|
|
AiOrgID string `json:"ai:orgid,omitempty"`
|
|
AIApiVersion string `json:"ai:apiversion,omitempty"`
|
|
AiMaxTokens float64 `json:"ai:maxtokens,omitempty"`
|
|
AiTimeoutMs float64 `json:"ai:timeoutms,omitempty"`
|
|
|
|
TermClear bool `json:"term:*,omitempty"`
|
|
TermFontSize float64 `json:"term:fontsize,omitempty"`
|
|
TermFontFamily string `json:"term:fontfamily,omitempty"`
|
|
TermTheme string `json:"term:theme,omitempty"`
|
|
TermDisableWebGl bool `json:"term:disablewebgl,omitempty"`
|
|
TermLocalShellPath string `json:"term:localshellpath,omitempty"`
|
|
TermLocalShellOpts []string `json:"term:localshellopts,omitempty"`
|
|
TermScrollback *int64 `json:"term:scrollback,omitempty"`
|
|
TermCopyOnSelect *bool `json:"term:copyonselect,omitempty"`
|
|
|
|
EditorMinimapEnabled bool `json:"editor:minimapenabled,omitempty"`
|
|
EditorStickyScrollEnabled bool `json:"editor:stickyscrollenabled,omitempty"`
|
|
EditorWordWrap bool `json:"editor:wordwrap,omitempty"`
|
|
|
|
WebClear bool `json:"web:*,omitempty"`
|
|
WebOpenLinksInternally bool `json:"web:openlinksinternally,omitempty"`
|
|
WebDefaultUrl string `json:"web:defaulturl,omitempty"`
|
|
WebDefaultSearch string `json:"web:defaultsearch,omitempty"`
|
|
|
|
BlockHeaderClear bool `json:"blockheader:*,omitempty"`
|
|
BlockHeaderShowBlockIds bool `json:"blockheader:showblockids,omitempty"`
|
|
|
|
AutoUpdateClear bool `json:"autoupdate:*,omitempty"`
|
|
AutoUpdateEnabled bool `json:"autoupdate:enabled,omitempty"`
|
|
AutoUpdateIntervalMs float64 `json:"autoupdate:intervalms,omitempty"`
|
|
AutoUpdateInstallOnQuit bool `json:"autoupdate:installonquit,omitempty"`
|
|
AutoUpdateChannel string `json:"autoupdate:channel,omitempty"`
|
|
|
|
PreviewShowHiddenFiles *bool `json:"preview:showhiddenfiles,omitempty"`
|
|
|
|
TabPreset string `json:"tab:preset,omitempty"`
|
|
|
|
WidgetClear bool `json:"widget:*,omitempty"`
|
|
WidgetShowHelp *bool `json:"widget:showhelp,omitempty"`
|
|
|
|
WindowClear bool `json:"window:*,omitempty"`
|
|
WindowTransparent bool `json:"window:transparent,omitempty"`
|
|
WindowBlur bool `json:"window:blur,omitempty"`
|
|
WindowOpacity *float64 `json:"window:opacity,omitempty"`
|
|
WindowBgColor string `json:"window:bgcolor,omitempty"`
|
|
WindowReducedMotion bool `json:"window:reducedmotion,omitempty"`
|
|
WindowTileGapSize *int64 `json:"window:tilegapsize,omitempty"`
|
|
WindowShowMenuBar bool `json:"window:showmenubar,omitempty"`
|
|
WindowNativeTitleBar bool `json:"window:nativetitlebar,omitempty"`
|
|
WindowDisableHardwareAcceleration bool `json:"window:disablehardwareacceleration,omitempty"`
|
|
WindowMaxTabCacheSize int `json:"window:maxtabcachesize,omitempty"`
|
|
WindowMagnifiedBlockOpacity *float64 `json:"window:magnifiedblockopacity,omitempty"`
|
|
WindowMagnifiedBlockSize *float64 `json:"window:magnifiedblocksize,omitempty"`
|
|
WindowMagnifiedBlockBlurPrimaryPx *int64 `json:"window:magnifiedblockblurprimarypx,omitempty"`
|
|
WindowMagnifiedBlockBlurSecondaryPx *int64 `json:"window:magnifiedblockblursecondarypx,omitempty"`
|
|
|
|
KeyClear bool `json:"key:*,omitempty"`
|
|
KeyGlobalHotkey string `json:"key:globalhotkey,omitempty"`
|
|
|
|
TelemetryClear bool `json:"telemetry:*,omitempty"`
|
|
TelemetryEnabled bool `json:"telemetry:enabled,omitempty"`
|
|
|
|
ConnClear bool `json:"conn:*,omitempty"`
|
|
ConnAskBeforeWshInstall bool `json:"conn:askbeforewshinstall,omitempty"`
|
|
ConnWshEnabled bool `json:"conn:wshenabled,omitempty"`
|
|
}
|
|
|
|
type ConfigError struct {
|
|
File string `json:"file"`
|
|
Err string `json:"err"`
|
|
}
|
|
|
|
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]wshrpc.ConnKeywords `json:"connections"`
|
|
ConfigErrors []ConfigError `json:"configerrors" configfile:"-"`
|
|
}
|
|
|
|
func goBackWS(barr []byte, offset int) int {
|
|
if offset >= len(barr) {
|
|
offset = offset - 1
|
|
}
|
|
for i := offset - 1; i >= 0; i-- {
|
|
if barr[i] == ' ' || barr[i] == '\t' || barr[i] == '\n' || barr[i] == '\r' {
|
|
continue
|
|
}
|
|
return i
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func isTrailingCommaError(barr []byte, offset int) bool {
|
|
if offset >= len(barr) {
|
|
offset = offset - 1
|
|
}
|
|
offset = goBackWS(barr, offset)
|
|
if barr[offset] == '}' {
|
|
offset = goBackWS(barr, offset)
|
|
if barr[offset] == ',' {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func readConfigHelper(fileName string, barr []byte, readErr error) (waveobj.MetaMapType, []ConfigError) {
|
|
var cerrs []ConfigError
|
|
if readErr != nil && !os.IsNotExist(readErr) {
|
|
cerrs = append(cerrs, ConfigError{File: fileName, Err: readErr.Error()})
|
|
}
|
|
if len(barr) == 0 {
|
|
return nil, cerrs
|
|
}
|
|
var rtn waveobj.MetaMapType
|
|
err := json.Unmarshal(barr, &rtn)
|
|
if err != nil {
|
|
if syntaxErr, ok := err.(*json.SyntaxError); ok {
|
|
offset := syntaxErr.Offset
|
|
if offset > 0 {
|
|
offset = offset - 1
|
|
}
|
|
lineNum, colNum := utilfn.GetLineColFromOffset(barr, int(offset))
|
|
isTrailingComma := isTrailingCommaError(barr, int(offset))
|
|
if isTrailingComma {
|
|
err = fmt.Errorf("json syntax error at line %d, col %d: probably an extra trailing comma: %v", lineNum, colNum, syntaxErr)
|
|
} else {
|
|
err = fmt.Errorf("json syntax error at line %d, col %d: %v", lineNum, colNum, syntaxErr)
|
|
}
|
|
}
|
|
cerrs = append(cerrs, ConfigError{File: fileName, Err: err.Error()})
|
|
}
|
|
return rtn, cerrs
|
|
}
|
|
|
|
func readConfigFileFS(fsys fs.FS, logPrefix string, fileName string) (waveobj.MetaMapType, []ConfigError) {
|
|
barr, readErr := fs.ReadFile(fsys, fileName)
|
|
if readErr != nil {
|
|
// If we get an error, we may be using the wrong path separator for the given FS interface. Try switching the separator.
|
|
barr, readErr = fs.ReadFile(fsys, filepath.ToSlash(fileName))
|
|
}
|
|
return readConfigHelper(logPrefix+fileName, barr, readErr)
|
|
}
|
|
|
|
func ReadDefaultsConfigFile(fileName string) (waveobj.MetaMapType, []ConfigError) {
|
|
return readConfigFileFS(defaultconfig.ConfigFS, "defaults:", fileName)
|
|
}
|
|
|
|
func ReadWaveHomeConfigFile(fileName string) (waveobj.MetaMapType, []ConfigError) {
|
|
configDirAbsPath := wavebase.GetWaveConfigDir()
|
|
configDirFsys := os.DirFS(configDirAbsPath)
|
|
return readConfigFileFS(configDirFsys, "", fileName)
|
|
}
|
|
|
|
func WriteWaveHomeConfigFile(fileName string, m waveobj.MetaMapType) error {
|
|
configDirAbsPath := wavebase.GetWaveConfigDir()
|
|
fullFileName := filepath.Join(configDirAbsPath, fileName)
|
|
barr, err := jsonMarshalConfigInOrder(m)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return os.WriteFile(fullFileName, barr, 0644)
|
|
}
|
|
|
|
// simple merge that overwrites
|
|
func mergeMetaMapSimple(m waveobj.MetaMapType, toMerge waveobj.MetaMapType) waveobj.MetaMapType {
|
|
if m == nil {
|
|
return toMerge
|
|
}
|
|
if toMerge == nil {
|
|
return m
|
|
}
|
|
for k, v := range toMerge {
|
|
if v == nil {
|
|
delete(m, k)
|
|
continue
|
|
}
|
|
m[k] = v
|
|
}
|
|
if len(m) == 0 {
|
|
return nil
|
|
}
|
|
return m
|
|
}
|
|
|
|
func mergeMetaMap(m waveobj.MetaMapType, toMerge waveobj.MetaMapType, simpleMerge bool) waveobj.MetaMapType {
|
|
if simpleMerge {
|
|
return mergeMetaMapSimple(m, toMerge)
|
|
} else {
|
|
return waveobj.MergeMeta(m, toMerge, true)
|
|
}
|
|
}
|
|
|
|
func selectDirEntsBySuffix(dirEnts []fs.DirEntry, fileNameSuffix string) []fs.DirEntry {
|
|
var rtn []fs.DirEntry
|
|
for _, ent := range dirEnts {
|
|
if ent.IsDir() {
|
|
continue
|
|
}
|
|
if !strings.HasSuffix(ent.Name(), fileNameSuffix) {
|
|
continue
|
|
}
|
|
rtn = append(rtn, ent)
|
|
}
|
|
return rtn
|
|
}
|
|
|
|
func SortFileNameDescend(files []fs.DirEntry) {
|
|
sort.Slice(files, func(i, j int) bool {
|
|
return files[i].Name() > files[j].Name()
|
|
})
|
|
}
|
|
|
|
// Read and merge all files in the specified directory matching the supplied suffix
|
|
func readConfigFilesForDir(fsys fs.FS, logPrefix string, dirName string, fileName string, simpleMerge bool) (waveobj.MetaMapType, []ConfigError) {
|
|
dirEnts, _ := fs.ReadDir(fsys, dirName)
|
|
suffixEnts := selectDirEntsBySuffix(dirEnts, fileName+".json")
|
|
SortFileNameDescend(suffixEnts)
|
|
var rtn waveobj.MetaMapType
|
|
var errs []ConfigError
|
|
for _, ent := range suffixEnts {
|
|
fileVal, cerrs := readConfigFileFS(fsys, logPrefix, filepath.Join(dirName, ent.Name()))
|
|
rtn = mergeMetaMap(rtn, fileVal, simpleMerge)
|
|
errs = append(errs, cerrs...)
|
|
}
|
|
return rtn, errs
|
|
}
|
|
|
|
// Read and merge all files in the specified config filesystem matching the patterns `<partName>.json` and `<partName>/*.json`
|
|
func readConfigPartForFS(fsys fs.FS, logPrefix string, partName string, simpleMerge bool) (waveobj.MetaMapType, []ConfigError) {
|
|
config, errs := readConfigFilesForDir(fsys, logPrefix, partName, "", simpleMerge)
|
|
allErrs := errs
|
|
rtn := config
|
|
config, errs = readConfigFileFS(fsys, logPrefix, partName+".json")
|
|
allErrs = append(allErrs, errs...)
|
|
return mergeMetaMap(rtn, config, simpleMerge), allErrs
|
|
}
|
|
|
|
// Combine files from the defaults and home directory for the specified config part name
|
|
func readConfigPart(partName string, simpleMerge bool) (waveobj.MetaMapType, []ConfigError) {
|
|
configDirAbsPath := wavebase.GetWaveConfigDir()
|
|
configDirFsys := os.DirFS(configDirAbsPath)
|
|
defaultConfigs, cerrs := readConfigPartForFS(defaultconfig.ConfigFS, "defaults:", partName, simpleMerge)
|
|
homeConfigs, cerrs1 := readConfigPartForFS(configDirFsys, "", partName, simpleMerge)
|
|
|
|
rtn := defaultConfigs
|
|
allErrs := append(cerrs, cerrs1...)
|
|
return mergeMetaMap(rtn, homeConfigs, simpleMerge), allErrs
|
|
}
|
|
|
|
func ReadFullConfig() FullConfigType {
|
|
var fullConfig FullConfigType
|
|
configRType := reflect.TypeOf(fullConfig)
|
|
configRVal := reflect.ValueOf(&fullConfig).Elem()
|
|
for fieldIdx := 0; fieldIdx < configRType.NumField(); fieldIdx++ {
|
|
field := configRType.Field(fieldIdx)
|
|
if field.PkgPath != "" {
|
|
continue
|
|
}
|
|
configFile := field.Tag.Get("configfile")
|
|
if configFile == "-" {
|
|
continue
|
|
}
|
|
jsonTag := utilfn.GetJsonTag(field)
|
|
simpleMerge := field.Tag.Get("merge") == ""
|
|
var configPart waveobj.MetaMapType
|
|
var errs []ConfigError
|
|
if jsonTag == "-" || jsonTag == "" {
|
|
continue
|
|
} else {
|
|
configPart, errs = readConfigPart(jsonTag, simpleMerge)
|
|
}
|
|
fullConfig.ConfigErrors = append(fullConfig.ConfigErrors, errs...)
|
|
if configPart != nil {
|
|
fieldPtr := configRVal.Field(fieldIdx).Addr().Interface()
|
|
utilfn.ReUnmarshal(fieldPtr, configPart)
|
|
}
|
|
}
|
|
return fullConfig
|
|
}
|
|
|
|
func GetConfigSubdirs() []string {
|
|
var fullConfig FullConfigType
|
|
configRType := reflect.TypeOf(fullConfig)
|
|
var retVal []string
|
|
configDirAbsPath := wavebase.GetWaveConfigDir()
|
|
for fieldIdx := 0; fieldIdx < configRType.NumField(); fieldIdx++ {
|
|
field := configRType.Field(fieldIdx)
|
|
if field.PkgPath != "" {
|
|
continue
|
|
}
|
|
configFile := field.Tag.Get("configfile")
|
|
if configFile == "-" {
|
|
continue
|
|
}
|
|
jsonTag := utilfn.GetJsonTag(field)
|
|
if jsonTag != "-" && jsonTag != "" && jsonTag != "settings" {
|
|
retVal = append(retVal, filepath.Join(configDirAbsPath, jsonTag))
|
|
}
|
|
}
|
|
log.Printf("subdirs: %v\n", retVal)
|
|
return retVal
|
|
}
|
|
|
|
func getConfigKeyType(configKey string) reflect.Type {
|
|
ctype := reflect.TypeOf(SettingsType{})
|
|
for i := 0; i < ctype.NumField(); i++ {
|
|
field := ctype.Field(i)
|
|
jsonTag := utilfn.GetJsonTag(field)
|
|
if jsonTag == configKey {
|
|
return field.Type
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func getConfigKeyNamespace(key string) string {
|
|
colonIdx := strings.Index(key, ":")
|
|
if colonIdx == -1 {
|
|
return ""
|
|
}
|
|
return key[:colonIdx]
|
|
}
|
|
|
|
func orderConfigKeys(m waveobj.MetaMapType) []string {
|
|
keys := make([]string, 0, len(m))
|
|
for k := range m {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Slice(keys, func(i, j int) bool {
|
|
k1 := keys[i]
|
|
k2 := keys[j]
|
|
k1ns := getConfigKeyNamespace(k1)
|
|
k2ns := getConfigKeyNamespace(k2)
|
|
if k1ns != k2ns {
|
|
return k1ns < k2ns
|
|
}
|
|
return k1 < k2
|
|
})
|
|
return keys
|
|
}
|
|
|
|
func reindentJson(barr []byte, indentStr string) []byte {
|
|
if len(barr) < 2 {
|
|
return barr
|
|
}
|
|
if barr[0] != '{' && barr[0] != '[' {
|
|
return barr
|
|
}
|
|
if !bytes.Contains(barr, []byte("\n")) {
|
|
return barr
|
|
}
|
|
outputLines := bytes.Split(barr, []byte("\n"))
|
|
for i, line := range outputLines {
|
|
if i == 0 {
|
|
continue
|
|
}
|
|
outputLines[i] = append([]byte(indentStr), line...)
|
|
}
|
|
return bytes.Join(outputLines, []byte("\n"))
|
|
}
|
|
|
|
func jsonMarshalConfigInOrder(m waveobj.MetaMapType) ([]byte, error) {
|
|
if len(m) == 0 {
|
|
return []byte("{}"), nil
|
|
}
|
|
var buf bytes.Buffer
|
|
orderedKeys := orderConfigKeys(m)
|
|
buf.WriteString("{\n")
|
|
for idx, key := range orderedKeys {
|
|
val := m[key]
|
|
keyBarr, err := json.Marshal(key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
valBarr, err := json.MarshalIndent(val, "", " ")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
valBarr = reindentJson(valBarr, " ")
|
|
buf.WriteString(" ")
|
|
buf.Write(keyBarr)
|
|
buf.WriteString(": ")
|
|
buf.Write(valBarr)
|
|
if idx < len(orderedKeys)-1 {
|
|
buf.WriteString(",")
|
|
}
|
|
buf.WriteString("\n")
|
|
}
|
|
buf.WriteString("}")
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
var dummyNumber json.Number
|
|
|
|
func convertJsonNumber(num json.Number, ctype reflect.Type) (interface{}, error) {
|
|
// ctype might be int64, float64, string, *int64, *float64, *string
|
|
// switch on ctype first
|
|
if ctype.Kind() == reflect.Pointer {
|
|
ctype = ctype.Elem()
|
|
}
|
|
if reflect.Int64 == ctype.Kind() {
|
|
if ival, err := num.Int64(); err == nil {
|
|
return ival, nil
|
|
}
|
|
return nil, fmt.Errorf("invalid number for int64: %s", num)
|
|
}
|
|
if reflect.Float64 == ctype.Kind() {
|
|
if fval, err := num.Float64(); err == nil {
|
|
return fval, nil
|
|
}
|
|
return nil, fmt.Errorf("invalid number for float64: %s", num)
|
|
}
|
|
if reflect.String == ctype.Kind() {
|
|
return num.String(), nil
|
|
}
|
|
return nil, fmt.Errorf("cannot convert number to %s", ctype)
|
|
}
|
|
|
|
func SetBaseConfigValue(toMerge waveobj.MetaMapType) error {
|
|
m, cerrs := ReadWaveHomeConfigFile(SettingsFile)
|
|
if len(cerrs) > 0 {
|
|
return fmt.Errorf("error reading config file: %v", cerrs[0])
|
|
}
|
|
if m == nil {
|
|
m = make(waveobj.MetaMapType)
|
|
}
|
|
for configKey, val := range toMerge {
|
|
ctype := getConfigKeyType(configKey)
|
|
if ctype == nil {
|
|
return fmt.Errorf("invalid config key: %s", configKey)
|
|
}
|
|
if val == nil {
|
|
delete(m, configKey)
|
|
} else {
|
|
rtype := reflect.TypeOf(val)
|
|
if rtype == reflect.TypeOf(dummyNumber) {
|
|
convertedVal, err := convertJsonNumber(val.(json.Number), ctype)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot convert %s: %v", configKey, err)
|
|
}
|
|
val = convertedVal
|
|
rtype = reflect.TypeOf(val)
|
|
}
|
|
if rtype != ctype {
|
|
if ctype == reflect.PointerTo(rtype) {
|
|
m[configKey] = &val
|
|
} else {
|
|
return fmt.Errorf("invalid value type for %s: %T", configKey, val)
|
|
}
|
|
}
|
|
m[configKey] = val
|
|
}
|
|
}
|
|
return WriteWaveHomeConfigFile(SettingsFile, m)
|
|
}
|
|
|
|
func SetConnectionsConfigValue(connName string, toMerge waveobj.MetaMapType) error {
|
|
m, cerrs := ReadWaveHomeConfigFile(ConnectionsFile)
|
|
if len(cerrs) > 0 {
|
|
return fmt.Errorf("error reading config file: %v", cerrs[0])
|
|
}
|
|
if m == nil {
|
|
m = make(waveobj.MetaMapType)
|
|
}
|
|
connData := m.GetMap(connName)
|
|
if connData == nil {
|
|
connData = make(waveobj.MetaMapType)
|
|
}
|
|
for configKey, val := range toMerge {
|
|
connData[configKey] = val
|
|
}
|
|
m[connName] = connData
|
|
return WriteWaveHomeConfigFile(ConnectionsFile, m)
|
|
}
|
|
|
|
type WidgetConfigType struct {
|
|
DisplayOrder float64 `json:"display:order,omitempty"`
|
|
Icon string `json:"icon,omitempty"`
|
|
Color string `json:"color,omitempty"`
|
|
Label string `json:"label,omitempty"`
|
|
Description string `json:"description,omitempty"`
|
|
BlockDef waveobj.BlockDef `json:"blockdef"`
|
|
}
|
|
|
|
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"`
|
|
}
|