mirror of
https://github.com/wavetermdev/backup.git
synced 2026-08-05 13:57:07 -07:00
+1
-1
@@ -15,7 +15,7 @@ import { debounce } from "throttle-debounce";
|
|||||||
import * as util from "util";
|
import * as util from "util";
|
||||||
import winston from "winston";
|
import winston from "winston";
|
||||||
import * as services from "../frontend/app/store/services";
|
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 * as keyutil from "../frontend/util/keyutil";
|
||||||
import { fireAndForget } from "../frontend/util/util";
|
import { fireAndForget } from "../frontend/util/util";
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import { CenteredDiv } from "./element/quickelems";
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import "overlayscrollbars/overlayscrollbars.css";
|
import "overlayscrollbars/overlayscrollbars.css";
|
||||||
import "./app.less";
|
import "./app.less";
|
||||||
import "./term.less";
|
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
let Provider = jotai.Provider;
|
let Provider = jotai.Provider;
|
||||||
|
|||||||
@@ -385,7 +385,8 @@ const BlockFrame_Default_Component = ({
|
|||||||
"block-frame-default",
|
"block-frame-default",
|
||||||
isFocused ? "block-focused" : null,
|
isFocused ? "block-focused" : null,
|
||||||
preview ? "block-preview" : null,
|
preview ? "block-preview" : null,
|
||||||
numBlocksInTab == 1 ? "block-no-highlight" : null
|
numBlocksInTab == 1 ? "block-no-highlight" : null,
|
||||||
|
"block-" + blockId
|
||||||
)}
|
)}
|
||||||
onClick={blockModel?.onClick}
|
onClick={blockModel?.onClick}
|
||||||
onFocusCapture={blockModel?.onFocusCapture}
|
onFocusCapture={blockModel?.onFocusCapture}
|
||||||
|
|||||||
@@ -225,10 +225,7 @@ function handleWSEventMessage(msg: WSEventType) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (msg.eventtype == "config") {
|
if (msg.eventtype == "config") {
|
||||||
const data: WatcherUpdate = msg.data;
|
globalStore.set(atoms.settingsConfigAtom, msg.data.settings);
|
||||||
globalStore.set(atoms.settingsConfigAtom, data.update);
|
|
||||||
|
|
||||||
console.log("config", data);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (msg.eventtype == "userinput") {
|
if (msg.eventtype == "userinput") {
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -157,7 +157,7 @@ function DirectoryTable({
|
|||||||
const getIconFromMimeType = React.useCallback(
|
const getIconFromMimeType = React.useCallback(
|
||||||
(mimeType: string): string => {
|
(mimeType: string): string => {
|
||||||
while (mimeType.length > 0) {
|
while (mimeType.length > 0) {
|
||||||
let icon = settings.mimetypes[mimeType]?.icon ?? null;
|
let icon = settings.mimetypes?.[mimeType]?.icon ?? null;
|
||||||
if (isIconValid(icon)) {
|
if (isIconValid(icon)) {
|
||||||
return `fa fa-solid fa-${icon} fa-fw`;
|
return `fa fa-solid fa-${icon} fa-fw`;
|
||||||
}
|
}
|
||||||
@@ -169,7 +169,7 @@ function DirectoryTable({
|
|||||||
);
|
);
|
||||||
const getIconColor = React.useCallback(
|
const getIconColor = React.useCallback(
|
||||||
(mimeType: string): string => {
|
(mimeType: string): string => {
|
||||||
let iconColor = settings.mimetypes[mimeType]?.color ?? "inherit";
|
let iconColor = settings.mimetypes?.[mimeType]?.color ?? "inherit";
|
||||||
return iconColor;
|
return iconColor;
|
||||||
},
|
},
|
||||||
[settings.mimetypes]
|
[settings.mimetypes]
|
||||||
|
|||||||
@@ -1,48 +1,34 @@
|
|||||||
// Copyright 2024, Command Line Inc.
|
// Copyright 2024, Command Line Inc.
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// 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 { WOS, atoms, getEventORefSubject, globalStore, useBlockAtom, useSettingsAtom } from "@/store/global";
|
||||||
import * as services from "@/store/services";
|
import * as services from "@/store/services";
|
||||||
import * as keyutil from "@/util/keyutil";
|
import * as keyutil from "@/util/keyutil";
|
||||||
import type { ITheme } from "@xterm/xterm";
|
import * as util from "@/util/util";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { produce } from "immer";
|
import { produce } from "immer";
|
||||||
import * as jotai from "jotai";
|
import * as jotai from "jotai";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { TermStickers } from "./termsticker";
|
import { TermStickers } from "./termsticker";
|
||||||
|
import { TermThemeUpdater } from "./termtheme";
|
||||||
import { TermWrap } from "./termwrap";
|
import { TermWrap } from "./termwrap";
|
||||||
|
|
||||||
import { WshServer } from "@/app/store/wshserver";
|
|
||||||
import { VDomView } from "@/app/view/term/vdom";
|
|
||||||
import "public/xterm.css";
|
import "public/xterm.css";
|
||||||
import "./term.less";
|
|
||||||
|
|
||||||
function getThemeFromCSSVars(el: Element): ITheme {
|
function computeTheme(settings: SettingsConfigType, themeName: string) {
|
||||||
const theme: ITheme = {};
|
let defaultThemeName = "default-dark";
|
||||||
const elemStyle = getComputedStyle(el);
|
themeName = themeName ?? "default-dark";
|
||||||
theme.foreground = elemStyle.getPropertyValue("--term-foreground");
|
const defaultTheme: TermThemeType = settings?.termthemes?.[defaultThemeName] || ({} as any);
|
||||||
theme.background = elemStyle.getPropertyValue("--term-background");
|
const theme: TermThemeType = settings?.termthemes?.[themeName] || ({} as any);
|
||||||
theme.black = elemStyle.getPropertyValue("--term-black");
|
const combinedTheme = { ...defaultTheme };
|
||||||
theme.red = elemStyle.getPropertyValue("--term-red");
|
for (const key in theme) {
|
||||||
theme.green = elemStyle.getPropertyValue("--term-green");
|
if (!util.isBlank(theme[key])) {
|
||||||
theme.yellow = elemStyle.getPropertyValue("--term-yellow");
|
combinedTheme[key] = theme[key];
|
||||||
theme.blue = elemStyle.getPropertyValue("--term-blue");
|
}
|
||||||
theme.magenta = elemStyle.getPropertyValue("--term-magenta");
|
}
|
||||||
theme.cyan = elemStyle.getPropertyValue("--term-cyan");
|
return combinedTheme;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const keyMap = {
|
const keyMap = {
|
||||||
@@ -172,7 +158,12 @@ function makeTerminalModel(blockId: string): TermViewModel {
|
|||||||
return new TermViewModel(blockId);
|
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<HTMLDivElement>(null);
|
const connectElemRef = React.useRef<HTMLDivElement>(null);
|
||||||
const termRef = React.useRef<TermWrap>(null);
|
const termRef = React.useRef<TermWrap>(null);
|
||||||
model.termRef = termRef;
|
model.termRef = termRef;
|
||||||
@@ -194,6 +185,7 @@ const TerminalView = ({ blockId, model }: { blockId: string; model: TermViewMode
|
|||||||
});
|
});
|
||||||
const termSettings = jotai.useAtomValue(termSettingsAtom);
|
const termSettings = jotai.useAtomValue(termSettingsAtom);
|
||||||
const isFocused = jotai.useAtomValue(isFocusedAtom);
|
const isFocused = jotai.useAtomValue(isFocusedAtom);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
function handleTerminalKeydown(event: KeyboardEvent) {
|
function handleTerminalKeydown(event: KeyboardEvent) {
|
||||||
const waveEvent = keyutil.adaptFromReactOrNativeKeyEvent(event);
|
const waveEvent = keyutil.adaptFromReactOrNativeKeyEvent(event);
|
||||||
@@ -209,12 +201,13 @@ const TerminalView = ({ blockId, model }: { blockId: string; model: TermViewMode
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const settings = globalStore.get(atoms.settingsConfigAtom);
|
||||||
|
const termTheme = computeTheme(settings, blockData?.meta?.termtheme);
|
||||||
const termWrap = new TermWrap(
|
const termWrap = new TermWrap(
|
||||||
blockId,
|
blockId,
|
||||||
connectElemRef.current,
|
connectElemRef.current,
|
||||||
{
|
{
|
||||||
theme: getThemeFromCSSVars(connectElemRef.current),
|
theme: termTheme,
|
||||||
fontSize: termSettings?.fontsize ?? 12,
|
fontSize: termSettings?.fontsize ?? 12,
|
||||||
fontFamily: termSettings?.fontfamily ?? "Hack",
|
fontFamily: termSettings?.fontfamily ?? "Hack",
|
||||||
drawBoldTextInBrightColors: false,
|
drawBoldTextInBrightColors: false,
|
||||||
@@ -239,7 +232,7 @@ const TerminalView = ({ blockId, model }: { blockId: string; model: TermViewMode
|
|||||||
termWrap.dispose();
|
termWrap.dispose();
|
||||||
rszObs.disconnect();
|
rszObs.disconnect();
|
||||||
};
|
};
|
||||||
}, []);
|
}, [blockId, termSettings]);
|
||||||
|
|
||||||
const handleHtmlKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
const handleHtmlKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
const waveEvent = keyutil.adaptFromReactOrNativeKeyEvent(event);
|
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)}
|
className={clsx("view-term", "term-mode-" + termMode, isFocused ? "is-focused" : null)}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
>
|
>
|
||||||
|
<TermThemeUpdater blockId={blockId} termRef={termRef} />
|
||||||
<TermStickers config={stickerConfig} />
|
<TermStickers config={stickerConfig} />
|
||||||
<div key="conntectElem" className="term-connectelem" ref={connectElemRef}></div>
|
<div key="conntectElem" className="term-connectelem" ref={connectElemRef}></div>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
// Copyright 2024, Command Line Inc.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import { TermWrap } from "@/app/view/term/termwrap";
|
||||||
|
import { atoms, WOS } from "@/store/global";
|
||||||
|
import * as util from "@/util/util";
|
||||||
|
import { useAtomValue } from "jotai";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
interface TermThemeProps {
|
||||||
|
blockId: string;
|
||||||
|
termRef: React.RefObject<TermWrap>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TermThemeUpdater = ({ blockId, termRef }: TermThemeProps) => {
|
||||||
|
const { termthemes } = useAtomValue(atoms.settingsConfigAtom);
|
||||||
|
const [blockData] = WOS.useWaveObjectValue<Block>(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 };
|
||||||
Vendored
+28
-2
@@ -226,6 +226,7 @@ declare global {
|
|||||||
widgets: WidgetsConfigType[];
|
widgets: WidgetsConfigType[];
|
||||||
blockheader: BlockHeaderOpts;
|
blockheader: BlockHeaderOpts;
|
||||||
autoupdate: AutoUpdateOpts;
|
autoupdate: AutoUpdateOpts;
|
||||||
|
termthemes: {[key: string]: TermThemeType};
|
||||||
};
|
};
|
||||||
|
|
||||||
// wstore.StickerClickOptsType
|
// wstore.StickerClickOptsType
|
||||||
@@ -263,6 +264,32 @@ declare global {
|
|||||||
cols: number;
|
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
|
// wconfig.TerminalConfigType
|
||||||
type TerminalConfigType = {
|
type TerminalConfigType = {
|
||||||
fontsize?: number;
|
fontsize?: number;
|
||||||
@@ -354,8 +381,7 @@ declare global {
|
|||||||
|
|
||||||
// wconfig.WatcherUpdate
|
// wconfig.WatcherUpdate
|
||||||
type WatcherUpdate = {
|
type WatcherUpdate = {
|
||||||
file: string;
|
settings: SettingsConfigType;
|
||||||
update: SettingsConfigType;
|
|
||||||
error: string;
|
error: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ var ExtraTypes = []any{
|
|||||||
eventbus.WSLayoutActionData{},
|
eventbus.WSLayoutActionData{},
|
||||||
filestore.WaveFile{},
|
filestore.WaveFile{},
|
||||||
wconfig.SettingsConfigType{},
|
wconfig.SettingsConfigType{},
|
||||||
|
wconfig.TermThemesConfigType{},
|
||||||
wconfig.WatcherUpdate{},
|
wconfig.WatcherUpdate{},
|
||||||
wshutil.RpcMessage{},
|
wshutil.RpcMessage{},
|
||||||
wshrpc.WshServerCommandMeta{},
|
wshrpc.WshServerCommandMeta{},
|
||||||
|
|||||||
+142
-85
@@ -5,10 +5,12 @@ package wconfig
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/fsnotify/fsnotify"
|
"github.com/fsnotify/fsnotify"
|
||||||
@@ -19,42 +21,99 @@ import (
|
|||||||
const configDir = "config"
|
const configDir = "config"
|
||||||
|
|
||||||
var configDirAbsPath = filepath.Join(wavebase.GetWaveHomeDir(), configDir)
|
var configDirAbsPath = filepath.Join(wavebase.GetWaveHomeDir(), configDir)
|
||||||
|
var termThemesDirAbsPath = filepath.Join(configDirAbsPath, termThemesDir)
|
||||||
|
|
||||||
var instance *Watcher
|
var instance *Watcher
|
||||||
var once sync.Once
|
var once sync.Once
|
||||||
|
|
||||||
type Watcher struct {
|
type Watcher struct {
|
||||||
initialized bool
|
initialized bool
|
||||||
watcher *fsnotify.Watcher
|
watcher *fsnotify.Watcher
|
||||||
mutex sync.Mutex
|
mutex sync.Mutex
|
||||||
settingsFile string
|
settingsData SettingsConfigType
|
||||||
getSettingsDefaults func() SettingsConfigType
|
|
||||||
settingsData SettingsConfigType
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type WatcherUpdate struct {
|
type WatcherUpdate struct {
|
||||||
File string `json:"file"`
|
Settings SettingsConfigType `json:"settings"`
|
||||||
Update SettingsConfigType `json:"update"`
|
Error string `json:"error"`
|
||||||
Error string `json:"error"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func readFileContents(filePath string, getDefaults func() SettingsConfigType) (*SettingsConfigType, error) {
|
func LoadFullSettings() (*SettingsConfigType, error) {
|
||||||
if getDefaults == nil {
|
// first load settings.json
|
||||||
log.Printf("should not happen")
|
// then load themes
|
||||||
return nil, fmt.Errorf("watcher started without defaults")
|
// then apply defaults
|
||||||
}
|
settings, err := readFileContents[SettingsConfigType](settingsAbsPath, false)
|
||||||
content := getDefaults()
|
|
||||||
data, err := os.ReadFile(filePath)
|
|
||||||
if err != nil {
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := json.Unmarshal(data, &content); err != nil {
|
if err := json.Unmarshal(data, &content); err != nil {
|
||||||
|
log.Printf("could not unmarshal file %s: %v", filePath, err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &content, nil
|
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
|
// GetWatcher returns the singleton instance of the Watcher
|
||||||
func GetWatcher() *Watcher {
|
func GetWatcher() *Watcher {
|
||||||
once.Do(func() {
|
once.Do(func() {
|
||||||
@@ -63,36 +122,50 @@ func GetWatcher() *Watcher {
|
|||||||
log.Printf("failed to create file watcher: %v", err)
|
log.Printf("failed to create file watcher: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
os.MkdirAll(configDirAbsPath, 0751)
|
|
||||||
instance = &Watcher{watcher: watcher}
|
instance = &Watcher{watcher: watcher}
|
||||||
log.Printf("started config watcher: %v\n", configDirAbsPath)
|
if err := instance.addSettingsFile(settingsAbsPath); err != nil {
|
||||||
if err := instance.addSettingsFile(settingsAbsPath, getSettingsConfigDefaults); err != nil {
|
log.Printf("failed to add path %s to watcher: %v", settingsAbsPath, err)
|
||||||
log.Printf("failed to add path %s to watcher: %v", configDirAbsPath, 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
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return instance
|
return instance
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *Watcher) addSettingsFile(filename string, getDefaults func() SettingsConfigType) error {
|
func (w *Watcher) addSettingsFile(filePath string) error {
|
||||||
w.mutex.Lock()
|
w.mutex.Lock()
|
||||||
defer w.mutex.Unlock()
|
defer w.mutex.Unlock()
|
||||||
|
|
||||||
w.getSettingsDefaults = getDefaults
|
dir := filepath.Dir(filePath)
|
||||||
filename = filepath.ToSlash(filename)
|
err := os.MkdirAll(dir, 0751)
|
||||||
|
|
||||||
stat, err := os.Lstat(filename)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("warning: cannot stat file: %v", err)
|
return fmt.Errorf("error creating config directory: %v", err)
|
||||||
} else {
|
|
||||||
if stat.IsDir() {
|
|
||||||
return fmt.Errorf("warning: can't watch directory instead of file: %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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,7 +178,6 @@ func (w *Watcher) Start() {
|
|||||||
w.sendInitialValues()
|
w.sendInitialValues()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case event, ok := <-w.watcher.Events:
|
case event, ok := <-w.watcher.Events:
|
||||||
@@ -125,23 +197,15 @@ func (w *Watcher) Start() {
|
|||||||
|
|
||||||
// for initial values, exit on first error
|
// for initial values, exit on first error
|
||||||
func (w *Watcher) sendInitialValues() error {
|
func (w *Watcher) sendInitialValues() error {
|
||||||
filename := w.settingsFile
|
settings, err := LoadFullSettings()
|
||||||
|
if err != nil {
|
||||||
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 {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
w.settingsData = *settings
|
||||||
message := WatcherUpdate{
|
message := WatcherUpdate{
|
||||||
File: filename,
|
Settings: w.settingsData,
|
||||||
Update: *content,
|
|
||||||
}
|
}
|
||||||
|
w.broadcast(message)
|
||||||
w.settingsData = message.Update
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,14 +226,10 @@ func (w *Watcher) broadcast(message WatcherUpdate) {
|
|||||||
Data: message,
|
Data: message,
|
||||||
})
|
})
|
||||||
|
|
||||||
if message.File == w.settingsFile {
|
|
||||||
w.settingsData = message.Update
|
|
||||||
}
|
|
||||||
|
|
||||||
if message.Error != "" {
|
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 {
|
} 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)
|
fileName := filepath.ToSlash(event.Name)
|
||||||
|
|
||||||
// only consider events for the tracked files
|
if isInDirectory(fileName, termThemesDirAbsPath) {
|
||||||
if fileName != w.settingsFile {
|
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
|
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 {
|
func (w *Watcher) handleSettingsFileEvent(event fsnotify.Event, fileName string) {
|
||||||
message := WatcherUpdate{
|
settings, err := LoadFullSettings()
|
||||||
File: fileName,
|
if err != nil {
|
||||||
Update: defaults,
|
log.Printf("error loading settings after settings file event: %v", err)
|
||||||
}
|
return
|
||||||
w.broadcast(message)
|
|
||||||
}
|
}
|
||||||
|
w.settingsData = *settings
|
||||||
|
w.broadcast(WatcherUpdate{Settings: w.settingsData})
|
||||||
|
}
|
||||||
|
|
||||||
if event.Op&fsnotify.Write == fsnotify.Write || event.Op&fsnotify.Create == fsnotify.Create {
|
func getThemeName(fileName string) string {
|
||||||
content, err := readFileContents(fileName, w.getSettingsDefaults)
|
return strings.TrimSuffix(filepath.Base(fileName), filepath.Ext(fileName))
|
||||||
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 (w *Watcher) AddWidget(newWidget WidgetsConfigType) error {
|
func (w *Watcher) AddWidget(newWidget WidgetsConfigType) error {
|
||||||
@@ -228,8 +285,8 @@ func (w *Watcher) AddWidget(newWidget WidgetsConfigType) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
os.MkdirAll(filepath.Dir(w.settingsFile), 0751)
|
os.MkdirAll(filepath.Dir(settingsFile), 0751)
|
||||||
return os.WriteFile(w.settingsFile, update, 0644)
|
return os.WriteFile(settingsFile, update, 0644)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *Watcher) RmWidget(idx uint) error {
|
func (w *Watcher) RmWidget(idx uint) error {
|
||||||
@@ -240,6 +297,6 @@ func (w *Watcher) RmWidget(idx uint) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
os.MkdirAll(filepath.Dir(w.settingsFile), 0751)
|
os.MkdirAll(filepath.Dir(settingsFile), 0751)
|
||||||
return os.WriteFile(w.settingsFile, update, 0644)
|
return os.WriteFile(settingsFile, update, 0644)
|
||||||
}
|
}
|
||||||
|
|||||||
+130
-56
@@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/wavetermdev/thenextwave/pkg/wstore"
|
"github.com/wavetermdev/thenextwave/pkg/wstore"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const termThemesDir = "terminal-themes"
|
||||||
const settingsFile = "settings.json"
|
const settingsFile = "settings.json"
|
||||||
|
|
||||||
var settingsAbsPath = filepath.Join(configDirAbsPath, settingsFile)
|
var settingsAbsPath = filepath.Join(configDirAbsPath, settingsFile)
|
||||||
@@ -41,71 +42,144 @@ type AutoUpdateOpts struct {
|
|||||||
IntervalMs uint32 `json:"intervalms"`
|
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 {
|
type SettingsConfigType struct {
|
||||||
MimeTypes map[string]MimeTypeConfigType `json:"mimetypes"`
|
MimeTypes map[string]MimeTypeConfigType `json:"mimetypes"`
|
||||||
Term TerminalConfigType `json:"term"`
|
Term TerminalConfigType `json:"term"`
|
||||||
Widgets []WidgetsConfigType `json:"widgets"`
|
Widgets []WidgetsConfigType `json:"widgets"`
|
||||||
BlockHeader BlockHeaderOpts `json:"blockheader"`
|
BlockHeader BlockHeaderOpts `json:"blockheader"`
|
||||||
AutoUpdate AutoUpdateOpts `json:"autoupdate"`
|
AutoUpdate *AutoUpdateOpts `json:"autoupdate"`
|
||||||
|
TermThemes TermThemesConfigType `json:"termthemes"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSettingsConfigDefaults() SettingsConfigType {
|
var DefaultTermDarkTheme = TermThemeType{
|
||||||
return SettingsConfigType{
|
Black: "#757575",
|
||||||
MimeTypes: map[string]MimeTypeConfigType{
|
Red: "#cc685c",
|
||||||
"audio": {Icon: "file-audio"},
|
Green: "#76c266",
|
||||||
"application/pdf": {Icon: "file-pdf"},
|
Yellow: "#cbca9b",
|
||||||
"application/json": {Icon: "file-lines"},
|
Blue: "#85aacb",
|
||||||
"directory": {Icon: "folder", Color: "var(--term-bright-blue)"},
|
Magenta: "#cc72ca",
|
||||||
"font": {Icon: "book-font"},
|
Cyan: "#74a7cb",
|
||||||
"image": {Icon: "file-image"},
|
White: "#c1c1c1",
|
||||||
"text": {Icon: "file-lines"},
|
BrightBlack: "#727272",
|
||||||
"text/css": {Icon: "css3-alt fa-brands"},
|
BrightRed: "#cc9d97",
|
||||||
"text/javascript": {Icon: "js fa-brands"},
|
BrightGreen: "#a3dd97",
|
||||||
"text/typescript": {Icon: "js fa-brands"},
|
BrightYellow: "#cbcaaa",
|
||||||
"text/golang": {Icon: "golang fa-brands"},
|
BrightBlue: "#9ab6cb",
|
||||||
"text/html": {Icon: "html5 fa-brands"},
|
BrightMagenta: "#cc8ecb",
|
||||||
"text/less": {Icon: "less fa-brands"},
|
BrightCyan: "#b7b8cb",
|
||||||
"text/markdown": {Icon: "markdown fa-brands"},
|
BrightWhite: "#f0f0f0",
|
||||||
"text/rust": {Icon: "rust fa-brands"},
|
Gray: "#8b918a",
|
||||||
"text/scss": {Icon: "sass fa-brands"},
|
CmdText: "#f0f0f0",
|
||||||
"video": {Icon: "file-video"},
|
Foreground: "#c1c1c1",
|
||||||
"text/csv": {Icon: "file-csv"},
|
SelectionBackground: "",
|
||||||
},
|
Background: "#00000000",
|
||||||
Widgets: []WidgetsConfigType{
|
CursorAccent: "",
|
||||||
{
|
}
|
||||||
Icon: "files",
|
|
||||||
Label: "files",
|
func applyDefaultSettings(settings *SettingsConfigType) {
|
||||||
BlockDef: wstore.BlockDef{
|
defaultMimeTypes := map[string]MimeTypeConfigType{
|
||||||
View: "preview",
|
"audio": {Icon: "file-audio"},
|
||||||
Meta: map[string]any{"file": wavebase.GetHomeDir()},
|
"application/pdf": {Icon: "file-pdf"},
|
||||||
},
|
"application/json": {Icon: "file-lines"},
|
||||||
},
|
"directory": {Icon: "folder", Color: "var(--term-bright-blue)"},
|
||||||
{
|
"font": {Icon: "book-font"},
|
||||||
Icon: "chart-simple",
|
"image": {Icon: "file-image"},
|
||||||
Label: "chart",
|
"text": {Icon: "file-lines"},
|
||||||
BlockDef: wstore.BlockDef{
|
"text/css": {Icon: "css3-alt fa-brands"},
|
||||||
View: "plot",
|
"text/javascript": {Icon: "js fa-brands"},
|
||||||
},
|
"text/typescript": {Icon: "js fa-brands"},
|
||||||
},
|
"text/golang": {Icon: "golang fa-brands"},
|
||||||
{
|
"text/html": {Icon: "html5 fa-brands"},
|
||||||
Icon: "globe",
|
"text/less": {Icon: "less fa-brands"},
|
||||||
Label: "web",
|
"text/markdown": {Icon: "markdown fa-brands"},
|
||||||
BlockDef: wstore.BlockDef{
|
"text/rust": {Icon: "rust fa-brands"},
|
||||||
View: "web",
|
"text/scss": {Icon: "sass fa-brands"},
|
||||||
Meta: map[string]any{"url": "https://waveterm.dev/"},
|
"video": {Icon: "file-video"},
|
||||||
},
|
"text/csv": {Icon: "file-csv"},
|
||||||
},
|
}
|
||||||
{
|
if settings.MimeTypes == nil {
|
||||||
Icon: "sparkles",
|
settings.MimeTypes = defaultMimeTypes
|
||||||
Label: "waveai",
|
} else {
|
||||||
BlockDef: wstore.BlockDef{
|
for k, v := range defaultMimeTypes {
|
||||||
View: "waveai",
|
if _, found := settings.MimeTypes[k]; !found {
|
||||||
},
|
settings.MimeTypes[k] = v
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
AutoUpdate: AutoUpdateOpts{
|
}
|
||||||
|
if settings.AutoUpdate == nil {
|
||||||
|
settings.AutoUpdate = &AutoUpdateOpts{
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
IntervalMs: 3600000,
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user