port to electron (#33)

This commit is contained in:
Mike Sawka
2024-06-11 17:42:10 -07:00
committed by GitHub
parent 9f32a53485
commit 1874d9a252
62 changed files with 6498 additions and 1615 deletions
+62
View File
@@ -0,0 +1,62 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
@import "./reset.less";
@import "./theme.less";
body {
display: flex;
flex-direction: row;
width: 100vw;
height: 100vh;
background-color: var(--main-bg-color);
color: var(--main-text-color);
font: var(--base-font);
overflow: hidden;
}
*::-webkit-scrollbar {
width: 4px;
height: 4px;
}
*::-webkit-scrollbar-track {
background-color: var(--scrollbar-background-color) !important;
}
*::-webkit-scrollbar-thumb {
background-color: var(--scrollbar-thumb-color) !important;
border-radius: 4px;
margin: 0 1px 0 1px;
}
*::-webkit-scrollbar-thumb:hover {
background-color: var(--scrollbar-thumb-hover-color) !important;
}
.flex-spacer {
flex-grow: 1;
}
.text-fixed {
font: var(--fixed-font);
}
#main,
.mainapp {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
}
.titlebar {
height: 35px;
border-bottom: 1px solid var(--border-color);
flex-shrink: 0;
-webkit-app-region: drag;
}
.error-boundary {
color: var(--error-color);
}
+1 -1
View File
@@ -8,7 +8,7 @@ import { Provider } from "jotai";
import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import "../../public/style.less";
import "./app.less";
import { CenteredDiv } from "./element/quickelems";
const App = () => {
+1
View File
@@ -1,6 +1,7 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import React from "react";
import "./quickelems.less";
function CenteredLoadingDiv() {
+29
View File
@@ -0,0 +1,29 @@
*,
*::before,
*::after {
box-sizing: border-box;
}
* {
margin: 0;
}
body {
line-height: 1.2;
-webkit-font-smoothing: antialiased;
}
img,
picture,
video,
canvas,
svg {
display: block;
}
input,
button,
textarea,
select {
font: inherit;
}
+60 -26
View File
@@ -1,15 +1,22 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { Events } from "@wailsio/runtime";
import * as jotai from "jotai";
import * as rxjs from "rxjs";
import * as WOS from "./wos";
import { WSControl } from "./ws";
// TODO remove the window dependency completely
// we should have the initialization be more orderly -- proceed directly from wave.ts instead of on its own.
const globalStore = jotai.createStore();
const urlParams = new URLSearchParams(window.location.search);
const globalWindowId = urlParams.get("windowid");
const globalClientId = urlParams.get("clientid");
let globalWindowId: string = null;
let globalClientId: string = null;
if (typeof window !== "undefined") {
// this if statement allows us to use the code in nodejs as well
const urlParams = new URLSearchParams(window.location.search);
globalWindowId = urlParams.get("windowid") || "74eba2d0-22fc-4221-82ad-d028dd496342";
globalClientId = urlParams.get("clientid") || "f4bc1713-a364-41b3-a5c4-b000ba10d622";
}
const windowIdAtom = jotai.atom(null) as jotai.PrimitiveAtom<string>;
const clientIdAtom = jotai.atom(null) as jotai.PrimitiveAtom<string>;
globalStore.set(windowIdAtom, globalWindowId);
@@ -18,7 +25,7 @@ const uiContextAtom = jotai.atom((get) => {
const windowData = get(windowDataAtom);
const uiContext: UIContext = {
windowid: get(atoms.windowId),
activetabid: windowData.activetabid,
activetabid: windowData?.activetabid,
};
return uiContext;
}) as jotai.Atom<UIContext>;
@@ -34,7 +41,8 @@ const windowDataAtom: jotai.Atom<WaveWindow> = jotai.atom((get) => {
if (windowId == null) {
return null;
}
return WOS.getObjectValue(WOS.makeORef("window", windowId), get);
const rtn = WOS.getObjectValue<WaveWindow>(WOS.makeORef("window", windowId), get);
return rtn;
});
const workspaceAtom: jotai.Atom<Workspace> = jotai.atom((get) => {
const windowData = get(windowDataAtom);
@@ -56,10 +64,10 @@ const atoms = {
type SubjectWithRef<T> = rxjs.Subject<T> & { refCount: number; release: () => void };
const blockSubjects = new Map<string, SubjectWithRef<any>>();
const orefSubjects = new Map<string, SubjectWithRef<any>>();
function getBlockSubject(blockId: string): SubjectWithRef<any> {
let subject = blockSubjects.get(blockId);
function getORefSubject(oref: string): SubjectWithRef<any> {
let subject = orefSubjects.get(oref);
if (subject == null) {
subject = new rxjs.Subject<any>() as any;
subject.refCount = 0;
@@ -67,29 +75,15 @@ function getBlockSubject(blockId: string): SubjectWithRef<any> {
subject.refCount--;
if (subject.refCount === 0) {
subject.complete();
blockSubjects.delete(blockId);
orefSubjects.delete(oref);
}
};
blockSubjects.set(blockId, subject);
orefSubjects.set(oref, subject);
}
subject.refCount++;
return subject;
}
Events.On("block:ptydata", (event: any) => {
const data = event?.data;
if (data?.blockid == null) {
console.log("block:ptydata with null blockid");
return;
}
// we don't use getBlockSubject here because we don't want to create a new subject
const subject = blockSubjects.get(data.blockid);
if (subject == null) {
return;
}
subject.next(data);
});
const blockCache = new Map<string, Map<string, any>>();
function useBlockCache<T>(blockId: string, name: string, makeFn: () => T): T {
@@ -123,4 +117,44 @@ function useBlockAtom<T>(blockId: string, name: string, makeFn: () => jotai.Atom
return atom as jotai.Atom<T>;
}
export { WOS, atoms, getBlockSubject, globalStore, useBlockAtom, useBlockCache };
function getBackendHostPort(): string {
// TODO deal with dev/production
return "http://localhost:8190";
}
function getBackendWSHostPort(): string {
return "ws://localhost:8191";
}
let globalWS: WSControl = null;
function handleWSEventMessage(msg: WSEventType) {
if (msg.oref == null) {
console.log("unsupported event", msg);
return;
}
// we don't use getORefSubject here because we don't want to create a new subject
const subject = orefSubjects.get(msg.oref);
if (subject == null) {
return;
}
subject.next(msg.data);
}
function handleWSMessage(msg: any) {
if (msg == null) {
return;
}
if (msg.eventtype != null) {
handleWSEventMessage(msg);
}
}
function initWS() {
globalWS = new WSControl(getBackendWSHostPort(), globalStore, globalWindowId, "", (msg) => {
handleWSMessage(msg);
});
globalWS.connectNow("initWS");
}
export { WOS, atoms, getBackendHostPort, getORefSubject, globalStore, globalWS, initWS, useBlockAtom, useBlockCache };
+100
View File
@@ -0,0 +1,100 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
// generated by cmd/generate/main-generate.go
import * as WOS from "./wos";
// blockservice.BlockService (block)
class BlockServiceType {
// send command to block
SendCommand(blockid: string, command: MetaType): Promise<void> {
return WOS.callBackendService("block", "SendCommand", Array.from(arguments))
}
}
export const BlockService = new BlockServiceType()
// clientservice.ClientService (client)
class ClientServiceType {
GetClientData(): Promise<Client> {
return WOS.callBackendService("client", "GetClientData", Array.from(arguments))
}
GetTab(arg1: string): Promise<Tab> {
return WOS.callBackendService("client", "GetTab", Array.from(arguments))
}
GetWindow(arg1: string): Promise<Window> {
return WOS.callBackendService("client", "GetWindow", Array.from(arguments))
}
GetWorkspace(arg1: string): Promise<Workspace> {
return WOS.callBackendService("client", "GetWorkspace", Array.from(arguments))
}
}
export const ClientService = new ClientServiceType()
// fileservice.FileService (file)
class FileServiceType {
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))
}
StatFile(arg1: string): Promise<FileInfo> {
return WOS.callBackendService("file", "StatFile", Array.from(arguments))
}
}
export const FileService = new FileServiceType()
// objectservice.ObjectService (object)
class ObjectServiceType {
// @returns tabId (and object updates)
AddTabToWorkspace(tabName: string, activateTab: boolean): Promise<string> {
return WOS.callBackendService("object", "AddTabToWorkspace", Array.from(arguments))
}
// @returns object updates
CloseTab(tabId: string): Promise<void> {
return WOS.callBackendService("object", "CloseTab", Array.from(arguments))
}
// @returns blockId (and object updates)
CreateBlock(blockDef: BlockDef, rtOpts: RuntimeOpts): Promise<string> {
return WOS.callBackendService("object", "CreateBlock", Array.from(arguments))
}
// @returns object updates
DeleteBlock(blockId: string): Promise<void> {
return WOS.callBackendService("object", "DeleteBlock", Array.from(arguments))
}
// get wave object by oref
GetObject(oref: string): Promise<WaveObj> {
return WOS.callBackendService("object", "GetObject", Array.from(arguments))
}
// @returns objects
GetObjects(orefs: string[]): Promise<WaveObj[]> {
return WOS.callBackendService("object", "GetObjects", Array.from(arguments))
}
// @returns object updates
SetActiveTab(tabId: string): Promise<void> {
return WOS.callBackendService("object", "SetActiveTab", Array.from(arguments))
}
// @returns object updates
UpdateObject(waveObj: WaveObj, returnUpdates: boolean): Promise<void> {
return WOS.callBackendService("object", "UpdateObject", Array.from(arguments))
}
// @returns object updates
UpdateObjectMeta(oref: string, meta: MetaType): Promise<void> {
return WOS.callBackendService("object", "UpdateObjectMeta", Array.from(arguments))
}
}
export const ObjectService = new ObjectServiceType()
+74 -68
View File
@@ -3,10 +3,13 @@
// WaveObjectStore
import { Call as $Call, Events } from "@wailsio/runtime";
// import { Call as $Call, Events } from "@wailsio/runtime";
import * as jotai from "jotai";
import * as React from "react";
import { atoms, globalStore } from "./global";
import { atoms, getBackendHostPort, globalStore } from "./global";
import * as services from "./services";
const IsElectron = true;
type WaveObjectDataItemType<T extends WaveObj> = {
value: T;
@@ -54,7 +57,51 @@ function makeORef(otype: string, oid: string): string {
}
function GetObject<T>(oref: string): Promise<T> {
return $Call.ByName("github.com/wavetermdev/thenextwave/pkg/service/objectservice.ObjectService.GetObject", oref);
return callBackendService("object", "GetObject", [oref], true);
}
function callBackendService(service: string, method: string, args: any[], noUIContext?: boolean): Promise<any> {
const startTs = Date.now();
let uiContext: UIContext = null;
if (!noUIContext) {
uiContext = globalStore.get(atoms.uiContext);
}
let waveCall: WebCallType = {
service: service,
method: method,
args: args,
uicontext: uiContext,
};
// usp is just for debugging (easier to filter URLs)
let methodName = service + "." + method;
let usp = new URLSearchParams();
usp.set("service", service);
usp.set("method", method);
let fetchPromise = fetch(getBackendHostPort() + "/wave/service?" + usp.toString(), {
method: "POST",
body: JSON.stringify(waveCall),
});
let prtn = fetchPromise
.then((resp) => {
if (!resp.ok) {
throw new Error(`call ${methodName} failed: ${resp.status} ${resp.statusText}`);
}
return resp.json();
})
.then((respData: WebReturnType) => {
if (respData == null) {
return null;
}
if (respData.updates != null) {
updateWaveObjects(respData.updates);
}
if (respData.error != null) {
throw new Error(`call ${methodName} error: ${respData.error}`);
}
console.log("Call", methodName, Date.now() - startTs + "ms");
return respData.data;
});
return prtn;
}
const waveObjectValueCache = new Map<string, WaveObjectValue<any>>();
@@ -75,6 +122,7 @@ function createWaveValueObject<T extends WaveObj>(oref: string, shouldFetch: boo
const localPromise = GetObject<T>(oref);
wov.pendingPromise = localPromise;
localPromise.then((val) => {
console.log("GetObject resolved", oref, val);
if (wov.pendingPromise != localPromise) {
return;
}
@@ -187,7 +235,7 @@ function useWaveObject<T extends WaveObj>(oref: string): [T, boolean, (val: T) =
const [atomVal, setAtomVal] = jotai.useAtom(wov.dataAtom);
const simpleSet = (val: T) => {
setAtomVal({ value: val, loading: false });
UpdateObject(val, false);
services.ObjectService.UpdateObject(val, false);
};
return [atomVal.value, atomVal.loading, simpleSet];
}
@@ -236,48 +284,32 @@ function cleanWaveObjectCache() {
}
}
Events.On("waveobj:update", (event: any) => {
const data: WaveObjUpdate[] = event?.data;
if (data == null) {
return;
}
if (!Array.isArray(data)) {
console.log("invalid waveobj:update, not an array", data);
return;
}
if (data.length == 0) {
return;
}
updateWaveObjects(data);
});
function wrapObjectServiceCall<T>(fnName: string, ...args: any[]): Promise<T> {
const uiContext = globalStore.get(atoms.uiContext);
const startTs = Date.now();
let prtn = $Call.ByName(
"github.com/wavetermdev/thenextwave/pkg/service/objectservice.ObjectService." + fnName,
uiContext,
...args
);
prtn = prtn.then((val) => {
console.log("Call", fnName, Date.now() - startTs + "ms");
if (val.updates) {
updateWaveObjects(val.updates);
}
return val;
});
return prtn;
}
// Events.On("waveobj:update", (event: any) => {
// const data: WaveObjUpdate[] = event?.data;
// if (data == null) {
// return;
// }
// if (!Array.isArray(data)) {
// console.log("invalid waveobj:update, not an array", data);
// return;
// }
// if (data.length == 0) {
// return;
// }
// updateWaveObjects(data);
// });
// gets the value of a WaveObject from the cache.
// should provide getFn if it is available (e.g. inside of a jotai atom)
// otherwise it will use the globalStore.get function
function getObjectValue<T>(oref: string, getFn?: jotai.Getter): T {
const wov = waveObjectValueCache.get(oref);
if (wov === undefined) {
return null;
let wov = waveObjectValueCache.get(oref);
if (wov == null) {
console.log("wov is null, creating new wov", oref);
wov = createWaveValueObject(oref, true);
waveObjectValueCache.set(oref, wov);
}
if (getFn === undefined) {
if (getFn == null) {
getFn = globalStore.get;
}
const atomVal = getFn(wov.dataAtom);
@@ -298,38 +330,12 @@ function setObjectValue<T extends WaveObj>(value: T, setFn?: jotai.Setter, pushT
}
setFn(wov.dataAtom, { value: value, loading: false });
if (pushToServer) {
UpdateObject(value, false);
services.ObjectService.UpdateObject(value, false);
}
}
export function AddTabToWorkspace(tabName: string, activateTab: boolean): Promise<{ tabId: string }> {
return wrapObjectServiceCall("AddTabToWorkspace", tabName, activateTab);
}
export function SetActiveTab(tabId: string): Promise<void> {
return wrapObjectServiceCall("SetActiveTab", tabId);
}
export function CreateBlock(blockDef: BlockDef, rtOpts: RuntimeOpts): Promise<{ blockId: string }> {
return wrapObjectServiceCall("CreateBlock", blockDef, rtOpts);
}
export function DeleteBlock(blockId: string): Promise<void> {
return wrapObjectServiceCall("DeleteBlock", blockId);
}
export function CloseTab(tabId: string): Promise<void> {
return wrapObjectServiceCall("CloseTab", tabId);
}
export function UpdateObjectMeta(blockId: string, meta: MetadataType): Promise<void> {
return wrapObjectServiceCall("UpdateObjectMeta", blockId, meta);
}
export function UpdateObject(waveObj: WaveObj, returnUpdates: boolean): Promise<WaveObjUpdate[]> {
return wrapObjectServiceCall("UpdateObject", waveObj, returnUpdates);
}
export {
callBackendService,
cleanWaveObjectCache,
clearWaveObjectCache,
getObjectValue,
+254
View File
@@ -0,0 +1,254 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import * as jotai from "jotai";
import { sprintf } from "sprintf-js";
import { v4 as uuidv4 } from "uuid";
const MaxWebSocketSendSize = 1024 * 1024; // 1MB
type RpcEntry = {
reqId: string;
startTs: number;
method: string;
resolve: (any) => void;
reject: (any) => void;
promise: Promise<any>;
};
type JotaiStore = {
get: <Value>(atom: jotai.Atom<Value>) => Value;
set: <Value>(atom: jotai.WritableAtom<Value, [Value], void>, value: Value) => void;
};
class WSControl {
wsConn: any;
open: jotai.WritableAtom<boolean, [boolean], void>;
opening: boolean = false;
reconnectTimes: number = 0;
msgQueue: any[] = [];
windowId: string;
messageCallback: (any) => void = null;
watchSessionId: string = null;
watchScreenId: string = null;
wsLog: string[] = [];
authKey: string;
baseHostPort: string;
lastReconnectTime: number = 0;
rpcMap: Map<string, RpcEntry> = new Map(); // reqId -> RpcEntry
jotaiStore: JotaiStore;
constructor(
baseHostPort: string,
jotaiStore: JotaiStore,
windowId: string,
authKey: string,
messageCallback: (any) => void
) {
this.baseHostPort = baseHostPort;
this.messageCallback = messageCallback;
this.windowId = windowId;
this.authKey = authKey;
this.open = jotai.atom(false);
this.jotaiStore = jotaiStore;
setInterval(this.sendPing.bind(this), 5000);
}
log(str: string) {
let ts = Date.now();
this.wsLog.push("[" + ts + "] " + str);
if (this.wsLog.length > 50) {
this.wsLog.splice(0, this.wsLog.length - 50);
}
}
setOpen(val: boolean) {
this.jotaiStore.set(this.open, val);
}
isOpen() {
return this.jotaiStore.get(this.open);
}
connectNow(desc: string) {
if (this.isOpen()) {
return;
}
this.lastReconnectTime = Date.now();
this.log(sprintf("try reconnect (%s)", desc));
this.opening = true;
this.wsConn = new WebSocket(this.baseHostPort + "/ws?windowid=" + this.windowId);
this.wsConn.onopen = this.onopen.bind(this);
this.wsConn.onmessage = this.onmessage.bind(this);
this.wsConn.onclose = this.onclose.bind(this);
// turns out onerror is not necessary (onclose always follows onerror)
// this.wsConn.onerror = this.onerror;
}
reconnect(forceClose?: boolean) {
if (this.isOpen()) {
if (forceClose) {
this.wsConn.close(); // this will force a reconnect
}
return;
}
this.reconnectTimes++;
if (this.reconnectTimes > 20) {
this.log("cannot connect, giving up");
return;
}
let timeoutArr = [0, 0, 2, 5, 10, 10, 30, 60];
let timeout = 60;
if (this.reconnectTimes < timeoutArr.length) {
timeout = timeoutArr[this.reconnectTimes];
}
if (Date.now() - this.lastReconnectTime < 500) {
timeout = 1;
}
if (timeout > 0) {
this.log(sprintf("sleeping %ds", timeout));
}
setTimeout(() => {
this.connectNow(String(this.reconnectTimes));
}, timeout * 1000);
}
onclose(event: any) {
// console.log("close", event);
if (event.wasClean) {
this.log("connection closed");
} else {
this.log("connection error/disconnected");
}
if (this.isOpen() || this.opening) {
this.setOpen(false);
this.opening = false;
this.reconnect();
}
}
onopen() {
this.log("connection open");
this.setOpen(true);
this.opening = false;
this.runMsgQueue();
// reconnectTimes is reset in onmessage:hello
}
runMsgQueue() {
if (!this.isOpen()) {
return;
}
if (this.msgQueue.length == 0) {
return;
}
let msg = this.msgQueue.shift();
this.sendMessage(msg);
setTimeout(() => {
this.runMsgQueue();
}, 100);
}
onmessage(event: any) {
let eventData = null;
if (event.data != null) {
eventData = JSON.parse(event.data);
}
if (eventData == null) {
return;
}
if (eventData.type == "ping") {
this.wsConn.send(JSON.stringify({ type: "pong", stime: Date.now() }));
return;
}
if (eventData.type == "pong") {
// nothing
return;
}
if (eventData.type == "hello") {
this.reconnectTimes = 0;
return;
}
if (eventData.type == "rpcresp") {
this.handleRpcResp(eventData);
return;
}
if (this.messageCallback) {
try {
this.messageCallback(eventData);
} catch (e) {
console.log("[error] messageCallback", e);
}
}
}
sendPing() {
if (!this.isOpen()) {
return;
}
this.wsConn.send(JSON.stringify({ type: "ping", stime: Date.now() }));
}
handleRpcResp(data: any) {
let reqId = data.reqid;
let rpcEntry = this.rpcMap.get(reqId);
if (rpcEntry == null) {
console.log("rpcresp for unknown reqid", reqId);
return;
}
this.rpcMap.delete(reqId);
console.log("rpcresp", rpcEntry.method, Math.round(performance.now() - rpcEntry.startTs) + "ms");
if (data.error != null) {
rpcEntry.reject(data.error);
} else {
rpcEntry.resolve(data.data);
}
}
doRpc(method: string, params: any[]): Promise<any> {
if (!this.isOpen()) {
return Promise.reject("not connected");
}
let reqId = uuidv4();
let req = { type: "rpc", method: method, params: params, reqid: reqId };
let rpcEntry: RpcEntry = {
method: method,
startTs: performance.now(),
reqId: reqId,
resolve: null,
reject: null,
promise: null,
};
let rpcPromise = new Promise((resolve, reject) => {
rpcEntry.resolve = resolve;
rpcEntry.reject = reject;
});
rpcEntry.promise = rpcPromise;
this.rpcMap.set(reqId, rpcEntry);
this.wsConn.send(JSON.stringify(req));
return rpcPromise;
}
sendMessage(data: any) {
if (!this.isOpen()) {
return;
}
let msg = JSON.stringify(data);
const byteSize = new Blob([msg]).size;
if (byteSize > MaxWebSocketSendSize) {
console.log("ws message too large", byteSize, data.type, msg.substring(0, 100));
return;
}
this.wsConn.send(msg);
}
pushMessage(data: any) {
if (!this.isOpen()) {
this.msgQueue.push(data);
return;
}
this.sendMessage(data);
}
}
export { WSControl };
+3 -2
View File
@@ -2,13 +2,14 @@
// SPDX-License-Identifier: Apache-2.0
import { Block, BlockHeader } from "@/app/block/block";
import * as services from "@/store/services";
import * as WOS from "@/store/wos";
import { CenteredDiv, CenteredLoadingDiv } from "@/element/quickelems";
import { TileLayout } from "@/faraday/index";
import { getLayoutStateAtomForTab } from "@/faraday/lib/layoutAtom";
import { useAtomValue } from "jotai";
import { useCallback, useMemo } from "react";
import { CenteredDiv, CenteredLoadingDiv } from "../element/quickelems";
import "./tab.less";
const TabContent = ({ tabId }: { tabId: string }) => {
@@ -34,7 +35,7 @@ const TabContent = ({ tabId }: { tabId: string }) => {
const onNodeDelete = useCallback((data: TabLayoutData) => {
console.log("onNodeDelete", data);
return WOS.DeleteBlock(data.blockId);
return services.ObjectService.DeleteBlock(data.blockId);
}, []);
if (tabLoading) {
+32
View File
@@ -0,0 +1,32 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
:root {
/*
* term colors (16 + 6) form the base terminal theme
* for consistency these colors should be used by plugins/applications
*/
--term-black: #000000;
--term-red: #cc0000;
--term-green: #4e9a06;
--term-yellow: #c4a000;
--term-blue: #3465a4;
--term-magenta: #bc3fbc;
--term-cyan: #06989a;
--term-white: #d0d0d0;
--term-bright-black: #555753;
--term-bright-red: #ef2929;
--term-bright-green: #58c142;
--term-bright-yellow: #fce94f;
--term-bright-blue: #32afff;
--term-bright-magenta: #ad7fa8;
--term-bright-cyan: #34e2e2;
--term-bright-white: #e7e7e7;
--term-gray: #8b918a; /* not an official terminal color */
--term-cmdtext: #ffffff;
--term-foreground: #d3d7cf;
--term-background: #000000;
--term-selection-background: #ffffff60;
--term-cursor-accent: #000000;
}
+25
View File
@@ -0,0 +1,25 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
:root {
--main-text-color: #f7f7f7;
--title-font-size: 18px;
--secondary-text-color: rgb(195, 200, 194);
--main-bg-color: #000000;
--border-color: #333333;
--base-font: normal 15px / normal "Lato", sans-serif;
--fixed-font: normal 12px / normal "Hack", monospace;
--accent-color: rgb(88, 193, 66);
--panel-bg-color: rgba(31, 33, 31, 1);
--highlight-bg-color: rgba(255, 255, 255, 0.2);
--markdown-font: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif,
"Apple Color Emoji", "Segoe UI Emoji";
--error-color: rgb(229, 77, 46);
--warning-color: rgb(224, 185, 86);
--success-color: rgb(78, 154, 6);
/* scrollbar colors */
--scrollbar-background-color: var(--main-bg-color);
--scrollbar-thumb-color: rgba(255, 255, 255, 0.3);
--scrollbar-thumb-hover-color: rgba(255, 255, 255, 0.5);
}
+1 -1
View File
@@ -15,7 +15,7 @@ declare var monaco: Monaco;
let monacoLoadedAtom = jotai.atom(false);
function loadMonaco() {
loader.config({ paths: { vs: "./monaco" } });
loader.config({ paths: { vs: "./dist-dev/monaco" } });
loader
.init()
.then(() => {
-1
View File
@@ -1,7 +1,6 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { FileInfo } from "@/bindings/fileservice";
import { Table, createColumnHelper, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
import clsx from "clsx";
import * as jotai from "jotai";
+8 -6
View File
@@ -1,9 +1,9 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { FileInfo, FileService, FullFile } from "@/bindings/fileservice";
import { Markdown } from "@/element/markdown";
import { useBlockAtom, useBlockCache } from "@/store/global";
import { getBackendHostPort, useBlockAtom, useBlockCache } from "@/store/global";
import * as services from "@/store/services";
import * as WOS from "@/store/wos";
import * as util from "@/util/util";
import clsx from "clsx";
@@ -69,7 +69,7 @@ function MarkdownPreview({ contentAtom }: { contentAtom: jotai.Atom<Promise<stri
function StreamingPreview({ fileInfo }: { fileInfo: FileInfo }) {
const filePath = fileInfo.path;
const streamingUrl = "/wave/stream-file?path=" + encodeURIComponent(filePath);
const streamingUrl = getBackendHostPort() + "/wave/stream-file?path=" + encodeURIComponent(filePath);
if (fileInfo.mimetype == "application/pdf") {
return (
<div className="view-preview view-preview-pdf">
@@ -114,7 +114,7 @@ function PreviewView({ blockId }: { blockId: string }) {
},
(get, set, update) => {
const blockId = get(blockAtom)?.oid;
WOS.UpdateObjectMeta(`block:${blockId}`, { file: update });
services.ObjectService.UpdateObjectMeta(`block:${blockId}`, { file: update });
}
)
);
@@ -124,7 +124,8 @@ function PreviewView({ blockId }: { blockId: string }) {
if (fileName == null) {
return null;
}
const statFile = await FileService.StatFile(fileName);
// const statFile = await FileService.StatFile(fileName);
const statFile = await services.FileService.StatFile(fileName);
return statFile;
})
);
@@ -134,7 +135,8 @@ function PreviewView({ blockId }: { blockId: string }) {
if (fileName == null) {
return null;
}
const file = await FileService.ReadFile(fileName);
// const file = await FileService.ReadFile(fileName);
const file = await services.FileService.ReadFile(fileName);
return file;
})
);
+22 -22
View File
@@ -1,15 +1,14 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { BlockService } from "@/bindings/blockservice";
import { getBlockSubject } from "@/store/global";
import { getBackendHostPort, getORefSubject, WOS } from "@/store/global";
import * as services from "@/store/services";
import { base64ToArray } from "@/util/util";
import { FitAddon } from "@xterm/addon-fit";
import type { ITheme } from "@xterm/xterm";
import { Terminal } from "@xterm/xterm";
import * as React from "react";
import useResizeObserver from "@react-hook/resize-observer";
import { debounce } from "throttle-debounce";
import "./view.less";
import "/public/xterm.css";
@@ -43,12 +42,15 @@ function getThemeFromCSSVars(el: Element): ITheme {
}
function handleResize(fitAddon: FitAddon, blockId: string, term: Terminal) {
if (term == null) {
return;
}
const oldRows = term.rows;
const oldCols = term.cols;
fitAddon.fit();
if (oldRows !== term.rows || oldCols !== term.cols) {
const resizeCommand = { command: "controller:input", termsize: { rows: term.rows, cols: term.cols } };
BlockService.SendCommand(blockId, resizeCommand);
services.BlockService.SendCommand(blockId, resizeCommand);
}
}
@@ -61,10 +63,6 @@ const TerminalView = ({ blockId }: { blockId: string }) => {
const connectElemRef = React.useRef<HTMLDivElement>(null);
const termRef = React.useRef<Terminal>(null);
const initialLoadRef = React.useRef<InitialLoadDataType>({ loaded: false, heldData: [] });
const [fitAddon, setFitAddon] = React.useState<FitAddon>(null);
const [term, setTerm] = React.useState<Terminal>(null);
React.useEffect(() => {
console.log("terminal created");
const newTerm = new Terminal({
@@ -80,18 +78,22 @@ const TerminalView = ({ blockId }: { blockId: string }) => {
newTerm.loadAddon(newFitAddon);
newTerm.open(connectElemRef.current);
newFitAddon.fit();
BlockService.SendCommand(blockId, {
// BlockService.SendCommand(blockId, {
// command: "controller:input",
// termsize: { rows: newTerm.rows, cols: newTerm.cols },
// });
services.BlockService.SendCommand(blockId, {
command: "controller:input",
termsize: { rows: newTerm.rows, cols: newTerm.cols },
});
newTerm.onData((data) => {
const b64data = btoa(data);
const inputCmd = { command: "controller:input", blockid: blockId, inputdata64: b64data };
BlockService.SendCommand(blockId, inputCmd);
services.BlockService.SendCommand(blockId, inputCmd);
});
// block subject
const blockSubject = getBlockSubject(blockId);
const blockSubject = getORefSubject(WOS.makeORef("block", blockId));
blockSubject.subscribe((data) => {
// base64 decode
const decodedData = base64ToArray(data.ptydata);
@@ -101,10 +103,6 @@ const TerminalView = ({ blockId }: { blockId: string }) => {
initialLoadRef.current.heldData.push(decodedData);
}
});
setTerm(newTerm);
setFitAddon(newFitAddon);
// load data from filestore
const startTs = Date.now();
let loadedBytes = 0;
@@ -112,7 +110,7 @@ const TerminalView = ({ blockId }: { blockId: string }) => {
const usp = new URLSearchParams();
usp.set("zoneid", blockId);
usp.set("name", "main");
fetch("/wave/file?" + usp.toString())
fetch(getBackendHostPort() + "/wave/file?" + usp.toString())
.then((resp) => {
if (resp.ok) {
return resp.arrayBuffer();
@@ -133,18 +131,20 @@ const TerminalView = ({ blockId }: { blockId: string }) => {
console.log(`terminal loaded file ${loadedBytes} bytes, ${Date.now() - startTs}ms`);
});
const resize_debounced = debounce(50, () => {
handleResize(newFitAddon, blockId, newTerm);
});
const rszObs = new ResizeObserver(() => {
resize_debounced();
});
rszObs.observe(connectElemRef.current);
return () => {
newTerm.dispose();
blockSubject.release();
};
}, []);
const handleResizeCallback = React.useCallback(() => {
debounce(50, () => handleResize(fitAddon, blockId, term));
}, [fitAddon, term]);
useResizeObserver(connectElemRef, handleResizeCallback);
return (
<div className="view-term">
<div key="conntectElem" className="term-connectelem" ref={connectElemRef}></div>
+8 -7
View File
@@ -3,6 +3,7 @@
import { TabContent } from "@/app/tab/tab";
import { atoms } from "@/store/global";
import * as services from "@/store/services";
import * as WOS from "@/store/wos";
import { clsx } from "clsx";
import * as jotai from "jotai";
@@ -21,10 +22,10 @@ function Tab({ tabId }: { tabId: string }) {
const windowData = jotai.useAtomValue(atoms.waveWindow);
const [tabData, tabLoading] = WOS.useWaveObjectValue<Tab>(WOS.makeORef("tab", tabId));
function setActiveTab() {
WOS.SetActiveTab(tabId);
services.ObjectService.SetActiveTab(tabId);
}
function handleCloseTab() {
WOS.CloseTab(tabId);
services.ObjectService.CloseTab(tabId);
deleteLayoutStateAtomForTab(tabId);
}
return (
@@ -45,7 +46,7 @@ function Tab({ tabId }: { tabId: string }) {
function TabBar({ workspace }: { workspace: Workspace }) {
function handleAddTab() {
const newTabName = `Tab-${workspace.tabids.length + 1}`;
WOS.AddTabToWorkspace(newTabName, true);
services.ObjectService.AddTabToWorkspace(newTabName, true);
}
const tabIds = workspace?.tabids ?? [];
return (
@@ -83,7 +84,7 @@ function Widgets() {
async function createBlock(blockDef: BlockDef) {
const rtOpts: RuntimeOpts = { termsize: { rows: 25, cols: 80 } };
const { blockId } = await WOS.CreateBlock(blockDef, rtOpts);
const blockId = await services.ObjectService.CreateBlock(blockDef, rtOpts);
addBlockToTab(blockId);
}
@@ -122,13 +123,13 @@ function Widgets() {
<div className="widget" onClick={() => clickTerminal()}>
<i className="fa fa-solid fa-square-terminal fa-fw" />
</div>
<div className="widget" onClick={() => clickPreview("README.md")}>
<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("go.mod")}>
<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("build/appicon.png")}>
<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("~")}>
+5 -5
View File
@@ -1,9 +1,9 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { TileLayout } from "./lib/TileLayout.jsx";
import { newLayoutTreeStateAtom, useLayoutTreeStateReducerAtom, withLayoutTreeState } from "./lib/layoutAtom.js";
import { newLayoutNode } from "./lib/layoutNode.js";
import { TileLayout } from "./lib/TileLayout";
import { newLayoutTreeStateAtom, useLayoutTreeStateReducerAtom, withLayoutTreeState } from "./lib/layoutAtom";
import { newLayoutNode } from "./lib/layoutNode";
import type {
LayoutNode,
LayoutTreeCommitPendingAction,
@@ -14,8 +14,8 @@ import type {
LayoutTreeState,
WritableLayoutNodeAtom,
WritableLayoutTreeStateAtom,
} from "./lib/model.js";
import { LayoutTreeActionType } from "./lib/model.js";
} from "./lib/model";
import { LayoutTreeActionType } from "./lib/model";
export {
LayoutTreeActionType,
+5 -11
View File
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
import clsx from "clsx";
import {
import React, {
CSSProperties,
ReactNode,
RefObject,
@@ -18,8 +18,8 @@ import { useDrag, useDragLayer, useDrop } from "react-dnd";
import useResizeObserver from "@react-hook/resize-observer";
import { toPng } from "html-to-image";
import { useLayoutTreeStateReducerAtom } from "./layoutAtom.js";
import { findNode } from "./layoutNode.js";
import { useLayoutTreeStateReducerAtom } from "./layoutAtom";
import { findNode } from "./layoutNode";
import {
ContentRenderer,
LayoutNode,
@@ -31,15 +31,9 @@ import {
LayoutTreeState,
PreviewRenderer,
WritableLayoutTreeStateAtom,
} from "./model.js";
} from "./model";
import "./tilelayout.less";
import {
Dimensions,
FlexDirection,
setTransform as createTransform,
debounce,
determineDropDirection,
} from "./utils.js";
import { Dimensions, FlexDirection, setTransform as createTransform, debounce, determineDropDirection } from "./utils";
export interface TileLayoutProps<T> {
layoutTreeStateAtom: WritableLayoutTreeStateAtom<T>;
+2 -2
View File
@@ -1,10 +1,10 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { WOS } from "@/app/store/global.js";
import { WOS } from "@/app/store/global";
import { Atom, Getter, PrimitiveAtom, WritableAtom, atom, useAtom } from "jotai";
import { useCallback } from "react";
import { layoutTreeStateReducer, newLayoutTreeState } from "./layoutState.js";
import { layoutTreeStateReducer, newLayoutTreeState } from "./layoutState";
import {
LayoutNode,
LayoutNodeWaveObj,
+2 -2
View File
@@ -1,8 +1,8 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { LayoutNode } from "./model.js";
import { FlexDirection, getCrypto, reverseFlexDirection } from "./utils.js";
import { LayoutNode } from "./model";
import { FlexDirection, getCrypto, reverseFlexDirection } from "./utils";
const crypto = getCrypto();

Some files were not shown because too many files have changed in this diff Show More