mirror of
https://github.com/wavetermdev/waveterm.git
synced 2024-12-21 16:38:23 +01:00
Add filewatcher for config files (#63)
This adds the filewatcher and forwards events to the frontend. It also sets up the widgets as something that can be controlled with a config file.
This commit is contained in:
parent
c7f76d5ced
commit
21fa9a601f
@ -4,3 +4,5 @@ bin
|
||||
frontend/dist
|
||||
frontend/node_modules
|
||||
*.min.*
|
||||
frontend/app/store/services.ts
|
||||
frontend/types/gotypes.d.ts
|
||||
|
@ -19,6 +19,7 @@ import (
|
||||
"github.com/wavetermdev/thenextwave/pkg/filestore"
|
||||
"github.com/wavetermdev/thenextwave/pkg/service"
|
||||
"github.com/wavetermdev/thenextwave/pkg/wavebase"
|
||||
"github.com/wavetermdev/thenextwave/pkg/wconfig"
|
||||
"github.com/wavetermdev/thenextwave/pkg/web"
|
||||
"github.com/wavetermdev/thenextwave/pkg/wstore"
|
||||
)
|
||||
@ -34,6 +35,10 @@ func doShutdown(reason string) {
|
||||
defer cancelFn()
|
||||
// TODO deal with flush in progress
|
||||
filestore.WFS.FlushCache(ctx)
|
||||
watcher := wconfig.GetWatcher()
|
||||
if watcher != nil {
|
||||
watcher.Close()
|
||||
}
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
os.Exit(0)
|
||||
})
|
||||
@ -62,6 +67,13 @@ func stdinReadWatch() {
|
||||
}
|
||||
}
|
||||
|
||||
func configWatcher() {
|
||||
watcher := wconfig.GetWatcher()
|
||||
if watcher != nil {
|
||||
watcher.Start()
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
log.SetFlags(log.LstdFlags | log.Lmicroseconds)
|
||||
log.SetPrefix("[wavesrv] ")
|
||||
@ -106,6 +118,7 @@ func main() {
|
||||
}
|
||||
installShutdownSignalHandlers()
|
||||
go stdinReadWatch()
|
||||
configWatcher()
|
||||
|
||||
go web.RunWebSocketServer()
|
||||
go func() {
|
||||
|
@ -56,6 +56,7 @@ const workspaceAtom: jotai.Atom<Workspace> = jotai.atom((get) => {
|
||||
}
|
||||
return WOS.getObjectValue(WOS.makeORef("workspace", windowData.workspaceid), get);
|
||||
});
|
||||
const settingsConfigAtom = jotai.atom(null) as jotai.PrimitiveAtom<SettingsConfigType>;
|
||||
const tabAtom: jotai.Atom<Tab> = jotai.atom((get) => {
|
||||
const windowData = get(windowDataAtom);
|
||||
if (windowData == null) {
|
||||
@ -72,6 +73,7 @@ const atoms = {
|
||||
client: clientAtom,
|
||||
waveWindow: windowDataAtom,
|
||||
workspace: workspaceAtom,
|
||||
settingsConfigAtom: settingsConfigAtom,
|
||||
tabAtom: tabAtom,
|
||||
};
|
||||
|
||||
@ -173,6 +175,13 @@ function handleWSEventMessage(msg: WSEventType) {
|
||||
console.log("unsupported event", msg);
|
||||
return;
|
||||
}
|
||||
if (msg.eventtype == "config") {
|
||||
const data: WatcherUpdate = msg.data;
|
||||
globalStore.set(settingsConfigAtom, data.update);
|
||||
|
||||
console.log("config", data);
|
||||
return;
|
||||
}
|
||||
if (msg.eventtype == "blockfile") {
|
||||
const fileData: WSFileEventData = msg.data;
|
||||
const fileSubject = getFileSubject(fileData.zoneid, fileData.filename);
|
||||
|
@ -45,12 +45,21 @@ export const ClientService = new ClientServiceType()
|
||||
|
||||
// fileservice.FileService (file)
|
||||
class FileServiceType {
|
||||
AddWidget(arg1: WidgetsConfigType): Promise<void> {
|
||||
return WOS.callBackendService("file", "AddWidget", Array.from(arguments))
|
||||
}
|
||||
GetSettingsConfig(): Promise<any> {
|
||||
return WOS.callBackendService("file", "GetSettingsConfig", Array.from(arguments))
|
||||
}
|
||||
GetWaveFile(arg1: string, arg2: string): Promise<any> {
|
||||
return WOS.callBackendService("file", "GetWaveFile", Array.from(arguments))
|
||||
}
|
||||
ReadFile(arg1: string): Promise<FullFile> {
|
||||
return WOS.callBackendService("file", "ReadFile", Array.from(arguments))
|
||||
}
|
||||
RemoveWidget(arg1: number): Promise<void> {
|
||||
return WOS.callBackendService("file", "RemoveWidget", Array.from(arguments))
|
||||
}
|
||||
StatFile(arg1: string): Promise<FileInfo> {
|
||||
return WOS.callBackendService("file", "StatFile", Array.from(arguments))
|
||||
}
|
||||
|
@ -4,12 +4,16 @@
|
||||
import { TabBar } from "@/app/tab/tabbar";
|
||||
import { TabContent } from "@/app/tab/tabcontent";
|
||||
import { atoms, createBlock } from "@/store/global";
|
||||
import * as services from "@/store/services";
|
||||
import * as jotai from "jotai";
|
||||
import * as React from "react";
|
||||
import { CenteredDiv } from "../element/quickelems";
|
||||
|
||||
import "./workspace.less";
|
||||
|
||||
function Widgets() {
|
||||
const settingsConfig = jotai.useAtomValue(atoms.settingsConfigAtom);
|
||||
const newWidgetModalVisible = React.useState(false);
|
||||
async function clickTerminal() {
|
||||
const termBlockDef = {
|
||||
controller: "shell",
|
||||
@ -18,51 +22,37 @@ function Widgets() {
|
||||
createBlock(termBlockDef);
|
||||
}
|
||||
|
||||
async function clickPreview(fileName: string) {
|
||||
const markdownDef = {
|
||||
view: "preview",
|
||||
meta: { file: fileName },
|
||||
};
|
||||
createBlock(markdownDef);
|
||||
}
|
||||
|
||||
async function clickPlot() {
|
||||
const plotDef: BlockDef = {
|
||||
view: "plot",
|
||||
};
|
||||
createBlock(plotDef);
|
||||
}
|
||||
|
||||
async function clickEdit() {
|
||||
const editDef: BlockDef = {
|
||||
view: "codeedit",
|
||||
};
|
||||
createBlock(editDef);
|
||||
}
|
||||
async function handleWidgetSelect(blockDef: BlockDef) {
|
||||
createBlock(blockDef);
|
||||
}
|
||||
|
||||
async function handleCreateWidget(newWidget: WidgetsConfigType) {
|
||||
await services.FileService.AddWidget(newWidget);
|
||||
}
|
||||
|
||||
async function handleRemoveWidget(idx: number) {
|
||||
await services.FileService.RmWidget(idx);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="workspace-widgets">
|
||||
<div className="widget" onClick={() => clickTerminal()}>
|
||||
<i className="fa fa-solid fa-square-terminal fa-fw" />
|
||||
</div>
|
||||
<div className="widget" onClick={() => clickPreview("~/work/wails/thenextwave/README.md")}>
|
||||
<i className="fa fa-solid fa-files fa-fw" />
|
||||
</div>
|
||||
<div className="widget" onClick={() => clickPreview("~/work/wails/thenextwave/go.mod")}>
|
||||
<i className="fa fa-solid fa-files fa-fw" />
|
||||
</div>
|
||||
<div className="widget" onClick={() => clickPreview("~/work/wails/thenextwave/build/appicon.png")}>
|
||||
<i className="fa fa-solid fa-files fa-fw" />
|
||||
</div>
|
||||
<div className="widget" onClick={() => clickPreview("~")}>
|
||||
<i className="fa fa-solid fa-files fa-fw" />
|
||||
</div>
|
||||
<div className="widget" onClick={() => clickPlot()}>
|
||||
<i className="fa fa-solid fa-chart-simple fa-fw" />
|
||||
</div>
|
||||
<div className="widget" onClick={() => clickEdit()}>
|
||||
<i className="fa-sharp fa-solid fa-pen-to-square"></i>
|
||||
</div>
|
||||
{settingsConfig.widgets.map((data, idx) => (
|
||||
<div className="widget" onClick={() => handleWidgetSelect(data.blockdef)} key={`widget-${idx}`}>
|
||||
<i className={data.icon}></i>
|
||||
</div>
|
||||
))}
|
||||
<div className="widget no-hover">
|
||||
<i className="fa fa-solid fa-plus fa-fw" />
|
||||
</div>
|
||||
|
6
frontend/types/gotypes.d.ts
vendored
6
frontend/types/gotypes.d.ts
vendored
@ -282,6 +282,12 @@ declare global {
|
||||
updates?: WaveObjUpdate[];
|
||||
};
|
||||
|
||||
// wconfig.WidgetsConfigType
|
||||
type WidgetsConfigType = {
|
||||
icon: string;
|
||||
blockdef: BlockDef;
|
||||
};
|
||||
|
||||
// wstore.WinSize
|
||||
type WinSize = {
|
||||
width: number;
|
||||
|
@ -1,7 +1,7 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { globalStore, globalWS, initWS } from "@/store/global";
|
||||
import { atoms, globalStore, globalWS, initWS } from "@/store/global";
|
||||
import * as services from "@/store/services";
|
||||
import * as WOS from "@/store/wos";
|
||||
import * as React from "react";
|
||||
@ -37,6 +37,7 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
const client = await WOS.loadAndPinWaveObject<Client>(WOS.makeORef("client", clientId));
|
||||
const waveWindow = await WOS.loadAndPinWaveObject<WaveWindow>(WOS.makeORef("window", windowId));
|
||||
await WOS.loadAndPinWaveObject<Workspace>(WOS.makeORef("workspace", waveWindow.workspaceid));
|
||||
globalStore.set(atoms.settingsConfigAtom, await services.FileService.GetSettingsConfig());
|
||||
services.ObjectService.SetActiveTab(waveWindow.activetabid); // no need to wait
|
||||
const reactElem = React.createElement(App, null, null);
|
||||
const elem = document.getElementById("main");
|
||||
|
1
go.mod
1
go.mod
@ -7,6 +7,7 @@ toolchain go1.22.1
|
||||
require (
|
||||
github.com/alexflint/go-filemutex v1.3.0
|
||||
github.com/creack/pty v1.1.18
|
||||
github.com/fsnotify/fsnotify v1.7.0
|
||||
github.com/golang-migrate/migrate/v4 v4.17.1
|
||||
github.com/google/uuid v1.4.0
|
||||
github.com/gorilla/handlers v1.5.2
|
||||
|
2
go.sum
2
go.sum
@ -10,6 +10,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
|
||||
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/golang-migrate/migrate/v4 v4.17.1 h1:4zQ6iqL6t6AiItphxJctQb3cFqWiSpMnX7wLTPnnYO4=
|
||||
|
@ -15,6 +15,7 @@ import (
|
||||
"github.com/wavetermdev/thenextwave/pkg/filestore"
|
||||
"github.com/wavetermdev/thenextwave/pkg/util/utilfn"
|
||||
"github.com/wavetermdev/thenextwave/pkg/wavebase"
|
||||
"github.com/wavetermdev/thenextwave/pkg/wconfig"
|
||||
)
|
||||
|
||||
const MaxFileSize = 10 * 1024 * 1024 // 10M
|
||||
@ -117,3 +118,18 @@ func (fs *FileService) GetWaveFile(id string, path string) (any, error) {
|
||||
}
|
||||
return file, nil
|
||||
}
|
||||
|
||||
func (fs *FileService) GetSettingsConfig() interface{} {
|
||||
watcher := wconfig.GetWatcher()
|
||||
return watcher.GetSettingsConfig()
|
||||
}
|
||||
|
||||
func (fs *FileService) AddWidget(newWidget wconfig.WidgetsConfigType) error {
|
||||
watcher := wconfig.GetWatcher()
|
||||
return watcher.AddWidget(newWidget)
|
||||
}
|
||||
|
||||
func (fs *FileService) RemoveWidget(idx uint) error {
|
||||
watcher := wconfig.GetWatcher()
|
||||
return watcher.RmWidget(idx)
|
||||
}
|
||||
|
@ -1180,8 +1180,12 @@ var StaticMimeTypeMap = map[string]string{
|
||||
".gl": "video/gl",
|
||||
".m4s": "video/iso.segment",
|
||||
".mj2": "video/mj2",
|
||||
".m4v": "video/mp4",
|
||||
".mkv": "video/mp4",
|
||||
".mov": "video/mp4",
|
||||
".mp4": "video/mp4",
|
||||
".mpeg": "video/mpeg",
|
||||
".mpg": "video/mpeg",
|
||||
".ogv": "video/ogg",
|
||||
".qt": "video/quicktime",
|
||||
".uvh": "video/vnd.dece.hd",
|
||||
|
244
pkg/wconfig/filewatcher.go
Normal file
244
pkg/wconfig/filewatcher.go
Normal file
@ -0,0 +1,244 @@
|
||||
package wconfig
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/wavetermdev/thenextwave/pkg/eventbus"
|
||||
"github.com/wavetermdev/thenextwave/pkg/wavebase"
|
||||
)
|
||||
|
||||
const configDir = "config"
|
||||
|
||||
var configDirAbsPath = filepath.Join(wavebase.GetWaveHomeDir(), configDir)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
type WatcherUpdate struct {
|
||||
File string `json:"file"`
|
||||
Update SettingsConfigType `json:"update"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
func readFileContents(filePath string, getDefaults func() SettingsConfigType) (*SettingsConfigType, error) {
|
||||
if getDefaults == nil {
|
||||
log.Printf("oopsie")
|
||||
return nil, fmt.Errorf("watcher started without defaults")
|
||||
}
|
||||
content := getDefaults()
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
log.Printf("doopsie: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
if err := json.Unmarshal(data, &content); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &content, nil
|
||||
}
|
||||
|
||||
// GetWatcher returns the singleton instance of the Watcher
|
||||
func GetWatcher() *Watcher {
|
||||
once.Do(func() {
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
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)
|
||||
return
|
||||
}
|
||||
})
|
||||
return instance
|
||||
}
|
||||
|
||||
func (w *Watcher) addSettingsFile(filename string, getDefaults func() SettingsConfigType) error {
|
||||
w.mutex.Lock()
|
||||
defer w.mutex.Unlock()
|
||||
|
||||
w.getSettingsDefaults = getDefaults
|
||||
filename = filepath.ToSlash(filename)
|
||||
|
||||
stat, err := os.Lstat(filename)
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
w.settingsFile = filename
|
||||
w.watcher.Add(filepath.Dir(filename))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *Watcher) Start() {
|
||||
w.mutex.Lock()
|
||||
defer w.mutex.Unlock()
|
||||
|
||||
log.Printf("starting file watcher\n")
|
||||
w.initialized = true
|
||||
w.sendInitialValues()
|
||||
|
||||
go func() {
|
||||
|
||||
for {
|
||||
select {
|
||||
case event, ok := <-w.watcher.Events:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
w.handleEvent(event)
|
||||
case err, ok := <-w.watcher.Errors:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
log.Println("watcher error:", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return err
|
||||
}
|
||||
|
||||
message := WatcherUpdate{
|
||||
File: filename,
|
||||
Update: *content,
|
||||
}
|
||||
|
||||
w.settingsData = message.Update
|
||||
|
||||
log.Printf("watcher: initial values: %s -> %v", filename, content)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *Watcher) Close() {
|
||||
w.mutex.Lock()
|
||||
defer w.mutex.Unlock()
|
||||
if w.watcher != nil {
|
||||
w.watcher.Close()
|
||||
w.watcher = nil
|
||||
log.Println("file watcher closed")
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Watcher) broadcast(message WatcherUpdate) {
|
||||
// send to frontend
|
||||
eventbus.SendEvent(eventbus.WSEventType{
|
||||
EventType: "config",
|
||||
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)
|
||||
} else {
|
||||
log.Printf("watcher: update: %s -> %v", message.File, message.Update)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Watcher) GetSettingsConfig() SettingsConfigType {
|
||||
w.mutex.Lock()
|
||||
defer w.mutex.Unlock()
|
||||
|
||||
return w.settingsData
|
||||
}
|
||||
|
||||
func (w *Watcher) handleEvent(event fsnotify.Event) {
|
||||
w.mutex.Lock()
|
||||
defer w.mutex.Unlock()
|
||||
|
||||
fileName := filepath.ToSlash(event.Name)
|
||||
|
||||
// only consider events for the tracked files
|
||||
if fileName != w.settingsFile {
|
||||
return
|
||||
}
|
||||
defaults := w.getSettingsDefaults()
|
||||
|
||||
if event.Op&fsnotify.Remove == fsnotify.Remove || event.Op&fsnotify.Rename == fsnotify.Rename {
|
||||
message := WatcherUpdate{
|
||||
File: fileName,
|
||||
Update: defaults,
|
||||
}
|
||||
w.broadcast(message)
|
||||
}
|
||||
|
||||
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 (w *Watcher) AddWidget(newWidget WidgetsConfigType) error {
|
||||
current := w.GetSettingsConfig()
|
||||
current.Widgets = append(current.Widgets, newWidget)
|
||||
update, err := json.Marshal(current)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
os.MkdirAll(filepath.Dir(w.settingsFile), 0751)
|
||||
return os.WriteFile(w.settingsFile, update, 0644)
|
||||
}
|
||||
|
||||
func (w *Watcher) RmWidget(idx uint) error {
|
||||
current := w.GetSettingsConfig().Widgets
|
||||
truncated := append(current[:idx], current[idx+1:]...)
|
||||
update, err := json.Marshal(truncated)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
os.MkdirAll(filepath.Dir(w.settingsFile), 0751)
|
||||
return os.WriteFile(w.settingsFile, update, 0644)
|
||||
}
|
41
pkg/wconfig/settingsconfig.go
Normal file
41
pkg/wconfig/settingsconfig.go
Normal file
@ -0,0 +1,41 @@
|
||||
package wconfig
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/wavetermdev/thenextwave/pkg/wavebase"
|
||||
"github.com/wavetermdev/thenextwave/pkg/wstore"
|
||||
)
|
||||
|
||||
const settingsFile = "settings.json"
|
||||
|
||||
var settingsAbsPath = filepath.Join(configDirAbsPath, settingsFile)
|
||||
|
||||
type WidgetsConfigType struct {
|
||||
Icon string `json:"icon"`
|
||||
BlockDef wstore.BlockDef `json:"blockdef"`
|
||||
}
|
||||
|
||||
type SettingsConfigType struct {
|
||||
Widgets []WidgetsConfigType `json:"widgets"`
|
||||
}
|
||||
|
||||
func getSettingsConfigDefaults() SettingsConfigType {
|
||||
return SettingsConfigType{
|
||||
Widgets: []WidgetsConfigType{
|
||||
{
|
||||
Icon: "fa fa-solid fa-files fa-fw",
|
||||
BlockDef: wstore.BlockDef{
|
||||
View: "preview",
|
||||
Meta: map[string]any{"file": wavebase.GetHomeDir()},
|
||||
},
|
||||
},
|
||||
{
|
||||
Icon: "fa fa-solid fa-chart-simple fa-fw",
|
||||
BlockDef: wstore.BlockDef{
|
||||
View: "plot",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user