From dcb4d5f2bf4f9c6e32f30e4b96e557cf47f54ade Mon Sep 17 00:00:00 2001 From: Red J Adaya Date: Thu, 25 Jul 2024 11:34:22 +0800 Subject: [PATCH] terminal themes feature (#135) Co-authored-by: sawka --- emain/emain.ts | 2 +- frontend/app/app.tsx | 1 - frontend/app/block/block.tsx | 3 +- frontend/app/store/global.ts | 5 +- frontend/app/term.less | 27 --- frontend/app/view/directorypreview.tsx | 4 +- frontend/app/view/term/term.tsx | 62 +++---- frontend/app/view/term/termtheme.ts | 39 +++++ frontend/types/gotypes.d.ts | 30 +++- pkg/tsgen/tsgen.go | 1 + pkg/wconfig/filewatcher.go | 227 ++++++++++++++++--------- pkg/wconfig/settingsconfig.go | 186 ++++++++++++++------ 12 files changed, 374 insertions(+), 213 deletions(-) delete mode 100644 frontend/app/term.less create mode 100644 frontend/app/view/term/termtheme.ts diff --git a/emain/emain.ts b/emain/emain.ts index 3dca4cf7..2ca78a04 100644 --- a/emain/emain.ts +++ b/emain/emain.ts @@ -15,7 +15,7 @@ import { debounce } from "throttle-debounce"; import * as util from "util"; import winston from "winston"; import * as services from "../frontend/app/store/services"; -import { getWebServerEndpoint, WebServerEndpointVarName, WSServerEndpointVarName } from "../frontend/util/endpoints"; +import { WSServerEndpointVarName, WebServerEndpointVarName, getWebServerEndpoint } from "../frontend/util/endpoints"; import * as keyutil from "../frontend/util/keyutil"; import { fireAndForget } from "../frontend/util/util"; diff --git a/frontend/app/app.tsx b/frontend/app/app.tsx index 07cd24f6..55809b92 100644 --- a/frontend/app/app.tsx +++ b/frontend/app/app.tsx @@ -18,7 +18,6 @@ import { CenteredDiv } from "./element/quickelems"; import clsx from "clsx"; import "overlayscrollbars/overlayscrollbars.css"; import "./app.less"; -import "./term.less"; const App = () => { let Provider = jotai.Provider; diff --git a/frontend/app/block/block.tsx b/frontend/app/block/block.tsx index ac8125b8..fca7bab2 100644 --- a/frontend/app/block/block.tsx +++ b/frontend/app/block/block.tsx @@ -385,7 +385,8 @@ const BlockFrame_Default_Component = ({ "block-frame-default", isFocused ? "block-focused" : null, preview ? "block-preview" : null, - numBlocksInTab == 1 ? "block-no-highlight" : null + numBlocksInTab == 1 ? "block-no-highlight" : null, + "block-" + blockId )} onClick={blockModel?.onClick} onFocusCapture={blockModel?.onFocusCapture} diff --git a/frontend/app/store/global.ts b/frontend/app/store/global.ts index 723b423d..379316aa 100644 --- a/frontend/app/store/global.ts +++ b/frontend/app/store/global.ts @@ -225,10 +225,7 @@ function handleWSEventMessage(msg: WSEventType) { return; } if (msg.eventtype == "config") { - const data: WatcherUpdate = msg.data; - globalStore.set(atoms.settingsConfigAtom, data.update); - - console.log("config", data); + globalStore.set(atoms.settingsConfigAtom, msg.data.settings); return; } if (msg.eventtype == "userinput") { diff --git a/frontend/app/term.less b/frontend/app/term.less deleted file mode 100644 index e425dba2..00000000 --- a/frontend/app/term.less +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2024, Command Line Inc. -// SPDX-License-Identifier: Apache-2.0 - -:root { - --term-black: #757575; - --term-red: #cc685c; - --term-green: #76c266; - --term-yellow: #cbca9b; - --term-blue: #85aacb; - --term-magenta: #cc72ca; - --term-cyan: #74a7cb; - --term-white: #c1c1c1; - - --term-bright-black: #727272; - --term-bright-red: #cc9d97; - --term-bright-green: #a3dd97; - --term-bright-yellow: #cbcaaa; - --term-bright-blue: #9ab6cb; - --term-bright-magenta: #cc8ecb; - --term-bright-cyan: #b7b8cb; - --term-bright-white: #f0f0f0; - - --term-gray: #8b918a; - --term-cmdtext: #f0f0f0; - --term-foreground: #c1c1c1; - --term-background: #00000000; -} diff --git a/frontend/app/view/directorypreview.tsx b/frontend/app/view/directorypreview.tsx index 9a4f39a5..78d4385b 100644 --- a/frontend/app/view/directorypreview.tsx +++ b/frontend/app/view/directorypreview.tsx @@ -157,7 +157,7 @@ function DirectoryTable({ const getIconFromMimeType = React.useCallback( (mimeType: string): string => { while (mimeType.length > 0) { - let icon = settings.mimetypes[mimeType]?.icon ?? null; + let icon = settings.mimetypes?.[mimeType]?.icon ?? null; if (isIconValid(icon)) { return `fa fa-solid fa-${icon} fa-fw`; } @@ -169,7 +169,7 @@ function DirectoryTable({ ); const getIconColor = React.useCallback( (mimeType: string): string => { - let iconColor = settings.mimetypes[mimeType]?.color ?? "inherit"; + let iconColor = settings.mimetypes?.[mimeType]?.color ?? "inherit"; return iconColor; }, [settings.mimetypes] diff --git a/frontend/app/view/term/term.tsx b/frontend/app/view/term/term.tsx index d3f7ccdc..464a4616 100644 --- a/frontend/app/view/term/term.tsx +++ b/frontend/app/view/term/term.tsx @@ -1,48 +1,34 @@ // Copyright 2024, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 +import { WshServer } from "@/app/store/wshserver"; +import { VDomView } from "@/app/view/term/vdom"; import { WOS, atoms, getEventORefSubject, globalStore, useBlockAtom, useSettingsAtom } from "@/store/global"; import * as services from "@/store/services"; import * as keyutil from "@/util/keyutil"; -import type { ITheme } from "@xterm/xterm"; +import * as util from "@/util/util"; import clsx from "clsx"; import { produce } from "immer"; import * as jotai from "jotai"; import * as React from "react"; import { TermStickers } from "./termsticker"; +import { TermThemeUpdater } from "./termtheme"; import { TermWrap } from "./termwrap"; -import { WshServer } from "@/app/store/wshserver"; -import { VDomView } from "@/app/view/term/vdom"; import "public/xterm.css"; -import "./term.less"; -function getThemeFromCSSVars(el: Element): ITheme { - const theme: ITheme = {}; - const elemStyle = getComputedStyle(el); - theme.foreground = elemStyle.getPropertyValue("--term-foreground"); - theme.background = elemStyle.getPropertyValue("--term-background"); - theme.black = elemStyle.getPropertyValue("--term-black"); - theme.red = elemStyle.getPropertyValue("--term-red"); - theme.green = elemStyle.getPropertyValue("--term-green"); - theme.yellow = elemStyle.getPropertyValue("--term-yellow"); - theme.blue = elemStyle.getPropertyValue("--term-blue"); - theme.magenta = elemStyle.getPropertyValue("--term-magenta"); - theme.cyan = elemStyle.getPropertyValue("--term-cyan"); - theme.white = elemStyle.getPropertyValue("--term-white"); - theme.brightBlack = elemStyle.getPropertyValue("--term-bright-black"); - theme.brightRed = elemStyle.getPropertyValue("--term-bright-red"); - theme.brightGreen = elemStyle.getPropertyValue("--term-bright-green"); - theme.brightYellow = elemStyle.getPropertyValue("--term-bright-yellow"); - theme.brightBlue = elemStyle.getPropertyValue("--term-bright-blue"); - theme.brightMagenta = elemStyle.getPropertyValue("--term-bright-magenta"); - theme.brightCyan = elemStyle.getPropertyValue("--term-bright-cyan"); - theme.brightWhite = elemStyle.getPropertyValue("--term-bright-white"); - theme.selectionBackground = elemStyle.getPropertyValue("--term-selection-background"); - theme.selectionInactiveBackground = elemStyle.getPropertyValue("--term-selection-background"); - theme.cursor = elemStyle.getPropertyValue("--term-selection-background"); - theme.cursorAccent = elemStyle.getPropertyValue("--term-cursor-accent"); - return theme; +function computeTheme(settings: SettingsConfigType, themeName: string) { + let defaultThemeName = "default-dark"; + themeName = themeName ?? "default-dark"; + const defaultTheme: TermThemeType = settings?.termthemes?.[defaultThemeName] || ({} as any); + const theme: TermThemeType = settings?.termthemes?.[themeName] || ({} as any); + const combinedTheme = { ...defaultTheme }; + for (const key in theme) { + if (!util.isBlank(theme[key])) { + combinedTheme[key] = theme[key]; + } + } + return combinedTheme; } const keyMap = { @@ -172,7 +158,12 @@ function makeTerminalModel(blockId: string): TermViewModel { return new TermViewModel(blockId); } -const TerminalView = ({ blockId, model }: { blockId: string; model: TermViewModel }) => { +interface TerminalViewProps { + blockId: string; + model: TermViewModel; +} + +const TerminalView = ({ blockId, model }: TerminalViewProps) => { const connectElemRef = React.useRef(null); const termRef = React.useRef(null); model.termRef = termRef; @@ -194,6 +185,7 @@ const TerminalView = ({ blockId, model }: { blockId: string; model: TermViewMode }); const termSettings = jotai.useAtomValue(termSettingsAtom); const isFocused = jotai.useAtomValue(isFocusedAtom); + React.useEffect(() => { function handleTerminalKeydown(event: KeyboardEvent) { const waveEvent = keyutil.adaptFromReactOrNativeKeyEvent(event); @@ -209,12 +201,13 @@ const TerminalView = ({ blockId, model }: { blockId: string; model: TermViewMode return false; } } - + const settings = globalStore.get(atoms.settingsConfigAtom); + const termTheme = computeTheme(settings, blockData?.meta?.termtheme); const termWrap = new TermWrap( blockId, connectElemRef.current, { - theme: getThemeFromCSSVars(connectElemRef.current), + theme: termTheme, fontSize: termSettings?.fontsize ?? 12, fontFamily: termSettings?.fontfamily ?? "Hack", drawBoldTextInBrightColors: false, @@ -239,7 +232,7 @@ const TerminalView = ({ blockId, model }: { blockId: string; model: TermViewMode termWrap.dispose(); rszObs.disconnect(); }; - }, []); + }, [blockId, termSettings]); const handleHtmlKeyDown = (event: React.KeyboardEvent) => { const waveEvent = keyutil.adaptFromReactOrNativeKeyEvent(event); @@ -331,6 +324,7 @@ const TerminalView = ({ blockId, model }: { blockId: string; model: TermViewMode className={clsx("view-term", "term-mode-" + termMode, isFocused ? "is-focused" : null)} onKeyDown={handleKeyDown} > +
; +} + +const TermThemeUpdater = ({ blockId, termRef }: TermThemeProps) => { + const { termthemes } = useAtomValue(atoms.settingsConfigAtom); + const [blockData] = WOS.useWaveObjectValue(WOS.makeORef("block", blockId)); + let defaultThemeName = "default-dark"; + let themeName = blockData.meta?.termtheme ?? "default-dark"; + + const defaultTheme: TermThemeType = termthemes?.[defaultThemeName] || ({} as any); + const theme: TermThemeType = termthemes?.[themeName] || ({} as any); + + useEffect(() => { + const combinedTheme = { ...defaultTheme }; + for (const key in theme) { + if (!util.isBlank(theme[key])) { + combinedTheme[key] = theme[key]; + } + } + if (termRef.current?.terminal) { + termRef.current.terminal.options.theme = combinedTheme; + } + }, [defaultTheme, theme]); + + return null; +}; + +export { TermThemeUpdater }; diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts index 34cc5e18..f5f18532 100644 --- a/frontend/types/gotypes.d.ts +++ b/frontend/types/gotypes.d.ts @@ -226,6 +226,7 @@ declare global { widgets: WidgetsConfigType[]; blockheader: BlockHeaderOpts; autoupdate: AutoUpdateOpts; + termthemes: {[key: string]: TermThemeType}; }; // wstore.StickerClickOptsType @@ -263,6 +264,32 @@ declare global { cols: number; }; + // wconfig.TermThemeType + type TermThemeType = { + black: string; + red: string; + green: string; + yellow: string; + blue: string; + magenta: string; + cyan: string; + white: string; + brightBlack: string; + brightRed: string; + brightGreen: string; + brightYellow: string; + brightBlue: string; + brightMagenta: string; + brightCyan: string; + brightWhite: string; + gray: string; + cmdtext: string; + foreground: string; + selectionBackground: string; + background: string; + cursorAccent: string; + }; + // wconfig.TerminalConfigType type TerminalConfigType = { fontsize?: number; @@ -354,8 +381,7 @@ declare global { // wconfig.WatcherUpdate type WatcherUpdate = { - file: string; - update: SettingsConfigType; + settings: SettingsConfigType; error: string; }; diff --git a/pkg/tsgen/tsgen.go b/pkg/tsgen/tsgen.go index f6a24418..7381e892 100644 --- a/pkg/tsgen/tsgen.go +++ b/pkg/tsgen/tsgen.go @@ -38,6 +38,7 @@ var ExtraTypes = []any{ eventbus.WSLayoutActionData{}, filestore.WaveFile{}, wconfig.SettingsConfigType{}, + wconfig.TermThemesConfigType{}, wconfig.WatcherUpdate{}, wshutil.RpcMessage{}, wshrpc.WshServerCommandMeta{}, diff --git a/pkg/wconfig/filewatcher.go b/pkg/wconfig/filewatcher.go index 1eeb791d..fe9dcbb6 100644 --- a/pkg/wconfig/filewatcher.go +++ b/pkg/wconfig/filewatcher.go @@ -5,10 +5,12 @@ package wconfig import ( "encoding/json" + "errors" "fmt" "log" "os" "path/filepath" + "strings" "sync" "github.com/fsnotify/fsnotify" @@ -19,42 +21,99 @@ import ( const configDir = "config" var configDirAbsPath = filepath.Join(wavebase.GetWaveHomeDir(), configDir) +var termThemesDirAbsPath = filepath.Join(configDirAbsPath, termThemesDir) var instance *Watcher var once sync.Once type Watcher struct { - initialized bool - watcher *fsnotify.Watcher - mutex sync.Mutex - settingsFile string - getSettingsDefaults func() SettingsConfigType - settingsData SettingsConfigType + initialized bool + watcher *fsnotify.Watcher + mutex sync.Mutex + settingsData SettingsConfigType } type WatcherUpdate struct { - File string `json:"file"` - Update SettingsConfigType `json:"update"` - Error string `json:"error"` + Settings SettingsConfigType `json:"settings"` + Error string `json:"error"` } -func readFileContents(filePath string, getDefaults func() SettingsConfigType) (*SettingsConfigType, error) { - if getDefaults == nil { - log.Printf("should not happen") - return nil, fmt.Errorf("watcher started without defaults") - } - content := getDefaults() - data, err := os.ReadFile(filePath) +func LoadFullSettings() (*SettingsConfigType, error) { + // first load settings.json + // then load themes + // then apply defaults + settings, err := readFileContents[SettingsConfigType](settingsAbsPath, false) if err != nil { - log.Printf("could not read settings file: %v", err) + return nil, err + } + themes, err := readThemes() + if err != nil { + return nil, err + } + if settings.TermThemes == nil { + settings.TermThemes = make(map[string]TermThemeType) + } + for k, v := range themes { + settings.TermThemes[k] = v + } + applyDefaultSettings(settings) + return settings, nil +} + +func readThemes() (map[string]TermThemeType, error) { + files, err := os.ReadDir(termThemesDirAbsPath) + if errors.Is(err, os.ErrNotExist) { + return nil, nil + } + if err != nil { + return nil, fmt.Errorf("error reading themes directory: %v", err) + } + themes := make(map[string]TermThemeType) + for _, file := range files { + if !file.IsDir() && filepath.Ext(file.Name()) == ".json" { + log.Printf("reading theme file %s\n", file.Name()) + theme, err := readFileContents[TermThemeType](filepath.Join(termThemesDirAbsPath, file.Name()), true) + if err != nil { + log.Printf("error reading theme file %s: %v", file.Name(), err) + continue + } + if theme == nil { + continue + } + themeName := getThemeName(file.Name()) + themes[themeName] = *theme + } + } + return themes, nil + +} + +func readFileContents[T any](filePath string, nilOnNotExist bool) (*T, error) { + var content T + data, err := os.ReadFile(filePath) + if errors.Is(err, os.ErrNotExist) { + if nilOnNotExist { + return nil, nil + } else { + return &content, nil + } + } + if err != nil { + log.Printf("could not read file %s: %v", filePath, err) return nil, err } if err := json.Unmarshal(data, &content); err != nil { + log.Printf("could not unmarshal file %s: %v", filePath, err) return nil, err } return &content, nil } +func isInDirectory(fileName, directory string) bool { + rel, err := filepath.Rel(directory, fileName) + return err == nil && !strings.HasPrefix(rel, "..") +} + // GetWatcher returns the singleton instance of the Watcher func GetWatcher() *Watcher { once.Do(func() { @@ -63,36 +122,50 @@ func GetWatcher() *Watcher { log.Printf("failed to create file watcher: %v", err) return } - os.MkdirAll(configDirAbsPath, 0751) instance = &Watcher{watcher: watcher} - log.Printf("started config watcher: %v\n", configDirAbsPath) - if err := instance.addSettingsFile(settingsAbsPath, getSettingsConfigDefaults); err != nil { - log.Printf("failed to add path %s to watcher: %v", configDirAbsPath, err) + if err := instance.addSettingsFile(settingsAbsPath); err != nil { + log.Printf("failed to add path %s to watcher: %v", settingsAbsPath, err) + return + } + if err := instance.addTermThemesDir(termThemesDirAbsPath); err != nil { + log.Printf("failed to add terminal themes path %s to watcher: %v", termThemesDirAbsPath, err) return } }) return instance } -func (w *Watcher) addSettingsFile(filename string, getDefaults func() SettingsConfigType) error { +func (w *Watcher) addSettingsFile(filePath string) error { w.mutex.Lock() defer w.mutex.Unlock() - w.getSettingsDefaults = getDefaults - filename = filepath.ToSlash(filename) - - stat, err := os.Lstat(filename) + dir := filepath.Dir(filePath) + err := os.MkdirAll(dir, 0751) if err != nil { - fmt.Printf("warning: cannot stat file: %v", err) - } else { - if stat.IsDir() { - return fmt.Errorf("warning: can't watch directory instead of file: %v", err) - } - + return fmt.Errorf("error creating config directory: %v", err) } - w.settingsFile = filename - w.watcher.Add(filepath.Dir(filename)) + w.watcher.Add(filePath) + log.Printf("started config watcher: %v\n", filePath) + return nil +} + +func (w *Watcher) addTermThemesDir(dir string) error { + w.mutex.Lock() + defer w.mutex.Unlock() + + _, err := os.Stat(dir) + if os.IsNotExist(err) { + if err := os.MkdirAll(dir, 0751); err != nil { + return fmt.Errorf("error creating themes directory: %v", err) + } + } else if err != nil { + return fmt.Errorf("error accessing themes directory: %v", err) + } + if err := w.watcher.Add(dir); err != nil { + return fmt.Errorf("error adding themes directory to watcher: %v", err) + } + log.Printf("started termthemes watcher: %v\n", dir) return nil } @@ -105,7 +178,6 @@ func (w *Watcher) Start() { w.sendInitialValues() go func() { - for { select { case event, ok := <-w.watcher.Events: @@ -125,23 +197,15 @@ func (w *Watcher) Start() { // for initial values, exit on first error func (w *Watcher) sendInitialValues() error { - filename := w.settingsFile - - content, err := readFileContents(w.settingsFile, w.getSettingsDefaults) - if os.IsNotExist(err) || os.IsPermission(err) { - log.Printf("settings file cannot be read: using defaults") - defaults := w.getSettingsDefaults() - content = &defaults - } else if err != nil { + settings, err := LoadFullSettings() + if err != nil { return err } - + w.settingsData = *settings message := WatcherUpdate{ - File: filename, - Update: *content, + Settings: w.settingsData, } - - w.settingsData = message.Update + w.broadcast(message) return nil } @@ -162,14 +226,10 @@ func (w *Watcher) broadcast(message WatcherUpdate) { Data: message, }) - if message.File == w.settingsFile { - w.settingsData = message.Update - } - if message.Error != "" { - log.Printf("watcher: error processing %s. sending defaults: %v", message.File, message.Error) + log.Printf("watcher: error processing update: %v. error: %s", message.Settings, message.Error) } else { - log.Printf("watcher: update: %s -> %v", message.File, message.Update) + log.Printf("watcher: update: %v", message.Settings) } } @@ -186,38 +246,35 @@ func (w *Watcher) handleEvent(event fsnotify.Event) { fileName := filepath.ToSlash(event.Name) - // only consider events for the tracked files - if fileName != w.settingsFile { + if isInDirectory(fileName, termThemesDirAbsPath) { + w.handleTermThemesEvent(event, fileName) + } else if filepath.Base(fileName) == filepath.Base(settingsAbsPath) { + w.handleSettingsFileEvent(event, fileName) + } +} + +func (w *Watcher) handleTermThemesEvent(event fsnotify.Event, fileName string) { + settings, err := LoadFullSettings() + if err != nil { + log.Printf("error loading settings after term-themes event: %v", err) return } - defaults := w.getSettingsDefaults() + w.settingsData = *settings + w.broadcast(WatcherUpdate{Settings: w.settingsData}) +} - if event.Op&fsnotify.Remove == fsnotify.Remove || event.Op&fsnotify.Rename == fsnotify.Rename { - message := WatcherUpdate{ - File: fileName, - Update: defaults, - } - w.broadcast(message) +func (w *Watcher) handleSettingsFileEvent(event fsnotify.Event, fileName string) { + settings, err := LoadFullSettings() + if err != nil { + log.Printf("error loading settings after settings file event: %v", err) + return } + w.settingsData = *settings + w.broadcast(WatcherUpdate{Settings: w.settingsData}) +} - if event.Op&fsnotify.Write == fsnotify.Write || event.Op&fsnotify.Create == fsnotify.Create { - content, err := readFileContents(fileName, w.getSettingsDefaults) - if err != nil { - message := WatcherUpdate{ - File: fileName, - Update: defaults, - Error: err.Error(), - } - w.broadcast(message) - return - } - message := WatcherUpdate{ - File: fileName, - Update: *content, - } - - w.broadcast(message) - } +func getThemeName(fileName string) string { + return strings.TrimSuffix(filepath.Base(fileName), filepath.Ext(fileName)) } func (w *Watcher) AddWidget(newWidget WidgetsConfigType) error { @@ -228,8 +285,8 @@ func (w *Watcher) AddWidget(newWidget WidgetsConfigType) error { return err } - os.MkdirAll(filepath.Dir(w.settingsFile), 0751) - return os.WriteFile(w.settingsFile, update, 0644) + os.MkdirAll(filepath.Dir(settingsFile), 0751) + return os.WriteFile(settingsFile, update, 0644) } func (w *Watcher) RmWidget(idx uint) error { @@ -240,6 +297,6 @@ func (w *Watcher) RmWidget(idx uint) error { return err } - os.MkdirAll(filepath.Dir(w.settingsFile), 0751) - return os.WriteFile(w.settingsFile, update, 0644) + os.MkdirAll(filepath.Dir(settingsFile), 0751) + return os.WriteFile(settingsFile, update, 0644) } diff --git a/pkg/wconfig/settingsconfig.go b/pkg/wconfig/settingsconfig.go index 2671a3bb..b5dd0262 100644 --- a/pkg/wconfig/settingsconfig.go +++ b/pkg/wconfig/settingsconfig.go @@ -10,6 +10,7 @@ import ( "github.com/wavetermdev/thenextwave/pkg/wstore" ) +const termThemesDir = "terminal-themes" const settingsFile = "settings.json" var settingsAbsPath = filepath.Join(configDirAbsPath, settingsFile) @@ -41,71 +42,144 @@ type AutoUpdateOpts struct { IntervalMs uint32 `json:"intervalms"` } +type TermThemeType struct { + 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"` + CursorAccent string `json:"cursorAccent"` +} + +type TermThemesConfigType map[string]TermThemeType + +// TODO add default term theme settings + type SettingsConfigType struct { MimeTypes map[string]MimeTypeConfigType `json:"mimetypes"` Term TerminalConfigType `json:"term"` Widgets []WidgetsConfigType `json:"widgets"` BlockHeader BlockHeaderOpts `json:"blockheader"` - AutoUpdate AutoUpdateOpts `json:"autoupdate"` + AutoUpdate *AutoUpdateOpts `json:"autoupdate"` + TermThemes TermThemesConfigType `json:"termthemes"` } -func getSettingsConfigDefaults() SettingsConfigType { - return SettingsConfigType{ - MimeTypes: map[string]MimeTypeConfigType{ - "audio": {Icon: "file-audio"}, - "application/pdf": {Icon: "file-pdf"}, - "application/json": {Icon: "file-lines"}, - "directory": {Icon: "folder", Color: "var(--term-bright-blue)"}, - "font": {Icon: "book-font"}, - "image": {Icon: "file-image"}, - "text": {Icon: "file-lines"}, - "text/css": {Icon: "css3-alt fa-brands"}, - "text/javascript": {Icon: "js fa-brands"}, - "text/typescript": {Icon: "js fa-brands"}, - "text/golang": {Icon: "golang fa-brands"}, - "text/html": {Icon: "html5 fa-brands"}, - "text/less": {Icon: "less fa-brands"}, - "text/markdown": {Icon: "markdown fa-brands"}, - "text/rust": {Icon: "rust fa-brands"}, - "text/scss": {Icon: "sass fa-brands"}, - "video": {Icon: "file-video"}, - "text/csv": {Icon: "file-csv"}, - }, - Widgets: []WidgetsConfigType{ - { - Icon: "files", - Label: "files", - BlockDef: wstore.BlockDef{ - View: "preview", - Meta: map[string]any{"file": wavebase.GetHomeDir()}, - }, - }, - { - Icon: "chart-simple", - Label: "chart", - BlockDef: wstore.BlockDef{ - View: "plot", - }, - }, - { - Icon: "globe", - Label: "web", - BlockDef: wstore.BlockDef{ - View: "web", - Meta: map[string]any{"url": "https://waveterm.dev/"}, - }, - }, - { - Icon: "sparkles", - Label: "waveai", - BlockDef: wstore.BlockDef{ - View: "waveai", - }, - }, - }, - AutoUpdate: AutoUpdateOpts{ +var DefaultTermDarkTheme = TermThemeType{ + Black: "#757575", + Red: "#cc685c", + Green: "#76c266", + Yellow: "#cbca9b", + Blue: "#85aacb", + Magenta: "#cc72ca", + Cyan: "#74a7cb", + White: "#c1c1c1", + BrightBlack: "#727272", + BrightRed: "#cc9d97", + BrightGreen: "#a3dd97", + BrightYellow: "#cbcaaa", + BrightBlue: "#9ab6cb", + BrightMagenta: "#cc8ecb", + BrightCyan: "#b7b8cb", + BrightWhite: "#f0f0f0", + Gray: "#8b918a", + CmdText: "#f0f0f0", + Foreground: "#c1c1c1", + SelectionBackground: "", + Background: "#00000000", + CursorAccent: "", +} + +func applyDefaultSettings(settings *SettingsConfigType) { + defaultMimeTypes := map[string]MimeTypeConfigType{ + "audio": {Icon: "file-audio"}, + "application/pdf": {Icon: "file-pdf"}, + "application/json": {Icon: "file-lines"}, + "directory": {Icon: "folder", Color: "var(--term-bright-blue)"}, + "font": {Icon: "book-font"}, + "image": {Icon: "file-image"}, + "text": {Icon: "file-lines"}, + "text/css": {Icon: "css3-alt fa-brands"}, + "text/javascript": {Icon: "js fa-brands"}, + "text/typescript": {Icon: "js fa-brands"}, + "text/golang": {Icon: "golang fa-brands"}, + "text/html": {Icon: "html5 fa-brands"}, + "text/less": {Icon: "less fa-brands"}, + "text/markdown": {Icon: "markdown fa-brands"}, + "text/rust": {Icon: "rust fa-brands"}, + "text/scss": {Icon: "sass fa-brands"}, + "video": {Icon: "file-video"}, + "text/csv": {Icon: "file-csv"}, + } + if settings.MimeTypes == nil { + settings.MimeTypes = defaultMimeTypes + } else { + for k, v := range defaultMimeTypes { + if _, found := settings.MimeTypes[k]; !found { + settings.MimeTypes[k] = v + } + } + } + if settings.AutoUpdate == nil { + settings.AutoUpdate = &AutoUpdateOpts{ Enabled: true, IntervalMs: 3600000, + } + } + defaultWidgets := []WidgetsConfigType{ + { + Icon: "files", + Label: "files", + BlockDef: wstore.BlockDef{ + View: "preview", + Meta: map[string]any{"file": wavebase.GetHomeDir()}, + }, + }, + { + Icon: "chart-simple", + Label: "chart", + BlockDef: wstore.BlockDef{ + View: "plot", + }, + }, + { + Icon: "globe", + Label: "web", + BlockDef: wstore.BlockDef{ + View: "web", + Meta: map[string]any{"url": "https://waveterm.dev/"}, + }, + }, + { + Icon: "sparkles", + Label: "waveai", + BlockDef: wstore.BlockDef{ + View: "waveai", + }, }, } + if settings.Widgets == nil { + settings.Widgets = defaultWidgets + } + if settings.TermThemes == nil { + settings.TermThemes = make(map[string]TermThemeType) + } + if _, found := settings.TermThemes["default-dark"]; !found { + settings.TermThemes["default-dark"] = DefaultTermDarkTheme + } }