From 473225d94bdf2170ddc170b897507a2e1fceddc5 Mon Sep 17 00:00:00 2001 From: Mike Sawka Date: Thu, 31 Oct 2024 12:34:30 -0700 Subject: [PATCH] issue squashing #1175 #1038 #1086 #1081 #1066 #1020 (#1177) --- electron-builder.config.cjs | 4 ++ emain/emain.ts | 11 ++-- emain/menu.ts | 11 ++++ emain/preload.ts | 13 +++++ frontend/app/block/blockframe.tsx | 24 ++++++--- frontend/app/view/codeeditor/codeeditor.tsx | 6 ++- .../app/view/preview/directorypreview.tsx | 51 +++++++++++++++---- frontend/app/view/preview/preview.tsx | 18 ++++++- frontend/app/view/term/term.tsx | 2 +- frontend/types/custom.d.ts | 1 + frontend/types/gotypes.d.ts | 6 ++- pkg/waveobj/metaconsts.go | 7 ++- pkg/waveobj/wtypemeta.go | 27 ++++++---- pkg/wconfig/defaultconfig/presets.json | 6 ++- 14 files changed, 149 insertions(+), 38 deletions(-) diff --git a/electron-builder.config.cjs b/electron-builder.config.cjs index ca6226627..d71eedc98 100644 --- a/electron-builder.config.cjs +++ b/electron-builder.config.cjs @@ -53,6 +53,10 @@ const config = { minimumSystemVersion: "10.15.0", mergeASARs: true, singleArchFiles: "dist/bin/wavesrv.*", + plist: { + NSMicrophoneUsageDescription: + "This app requires microphone access to enable audio input for terminal applications.", + }, }, linux: { artifactName: "${name}-${platform}-${arch}-${version}.${ext}", diff --git a/emain/emain.ts b/emain/emain.ts index ded7d0488..a9985f4e2 100644 --- a/emain/emain.ts +++ b/emain/emain.ts @@ -95,7 +95,6 @@ console.log = log; console.log( sprintf( "waveterm-app starting, data_dir=%s, config_dir=%s electronpath=%s gopath=%s arch=%s/%s", - waveDataDir, waveConfigDir, getElectronAppBasePath(), getElectronAppUnpackedBasePath(), @@ -402,6 +401,13 @@ electron.ipcMain.on("quicklook", (event, filePath: string) => { } }); +electron.ipcMain.on("open-native-path", (event, filePath: string) => { + console.log("open-native-path", filePath); + electron.shell.openPath(filePath).catch((err) => { + console.error(`Failed to open path ${filePath}:`, err); + }); +}); + async function createNewWaveWindow(): Promise { const clientData = await services.ClientService.GetClientData(); const fullConfig = await services.FileService.GetFullConfig(); @@ -547,8 +553,7 @@ function convertMenuDefArrToMenu(menuDefArr: ElectronContextMenuItem[]): electro type: menuDef.type, click: (_, window) => { const ww = window as WaveBrowserWindow; - const tabView = ww.activeTabView; - tabView?.webContents?.send("contextmenu-click", menuDef.id); + ww?.activeTabView?.webContents?.send("contextmenu-click", menuDef.id); }, checked: menuDef.checked, }; diff --git a/emain/menu.ts b/emain/menu.ts index f5bfe6c51..a0f0c6c0e 100644 --- a/emain/menu.ts +++ b/emain/menu.ts @@ -78,6 +78,17 @@ function getAppMenu(callbacks: AppMenuCallbacks): Electron.Menu { } ); } + appMenu.push({ + label: "Permissions", + submenu: [ + { + label: "Request Audio Access", + click: (_, window) => { + getWindowWebContents(window)?.send("request-audio-access"); + }, + }, + ], + }); appMenu.push({ role: "quit", }); diff --git a/emain/preload.ts b/emain/preload.ts index d0e0acfa9..47864f1e8 100644 --- a/emain/preload.ts +++ b/emain/preload.ts @@ -47,6 +47,7 @@ contextBridge.exposeInMainWorld("api", { onWaveInit: (callback) => ipcRenderer.on("wave-init", (_event, initOpts) => callback(initOpts)), sendLog: (log) => ipcRenderer.send("fe-log", log), onQuicklook: (filePath: string) => ipcRenderer.send("quicklook", filePath), + openNativePath: (filePath: string) => ipcRenderer.send("open-native-path", filePath), }); // Custom event for "new-window" @@ -60,3 +61,15 @@ ipcRenderer.on("webcontentsid-from-blockid", (e, blockId, responseCh) => { const wcId = webviewElem?.dataset?.webcontentsid; ipcRenderer.send(responseCh, wcId); }); + +ipcRenderer.on("request-audio-access", (e) => { + ipcRenderer.send("fe-log", "Requesting audio access"); + navigator.mediaDevices + .getUserMedia({ audio: true }) + .then(() => { + ipcRenderer.send("fe-log", "Audio access granted"); + }) + .catch((err) => { + ipcRenderer.send("fe-log", "Audio access denied: " + err); + }); +}); diff --git a/frontend/app/block/blockframe.tsx b/frontend/app/block/blockframe.tsx index 6477261e5..0baf835fc 100644 --- a/frontend/app/block/blockframe.tsx +++ b/frontend/app/block/blockframe.tsx @@ -375,12 +375,24 @@ const BlockMask = React.memo(({ nodeModel }: { nodeModel: NodeModel }) => { const [blockData] = WOS.useWaveObjectValue(WOS.makeORef("block", nodeModel.blockId)); const style: React.CSSProperties = {}; let showBlockMask = false; - - if (!isFocused && blockData?.meta?.["frame:bordercolor"]) { - style.borderColor = blockData.meta["frame:bordercolor"]; - } - if (isFocused && blockData?.meta?.["frame:bordercolor:focused"]) { - style.borderColor = blockData.meta["frame:bordercolor:focused"]; + if (isFocused) { + const tabData = jotai.useAtomValue(atoms.tabAtom); + const tabActiveBorderColor = tabData?.meta?.["bg:activebordercolor"]; + if (tabActiveBorderColor) { + style.borderColor = tabActiveBorderColor; + } + if (blockData?.meta?.["frame:activebordercolor"]) { + style.borderColor = blockData.meta["frame:activebordercolor"]; + } + } else { + const tabData = jotai.useAtomValue(atoms.tabAtom); + const tabBorderColor = tabData?.meta?.["bg:bordercolor"]; + if (tabBorderColor) { + style.borderColor = tabBorderColor; + } + if (blockData?.meta?.["frame:bordercolor"]) { + style.borderColor = blockData.meta["frame:bordercolor"]; + } } let innerElem = null; if (isLayoutMode) { diff --git a/frontend/app/view/codeeditor/codeeditor.tsx b/frontend/app/view/codeeditor/codeeditor.tsx index 77a72fbaa..777dae484 100644 --- a/frontend/app/view/codeeditor/codeeditor.tsx +++ b/frontend/app/view/codeeditor/codeeditor.tsx @@ -111,6 +111,7 @@ interface CodeEditorProps { text: string; filename: string; language?: string; + meta?: MetaType; onChange?: (text: string) => void; onMount?: (monacoPtr: MonacoTypes.editor.IStandaloneCodeEditor, monaco: Monaco) => () => void; } @@ -125,7 +126,7 @@ const stickyScrollEnabledAtom = atom((get) => { return settings["editor:stickyscrollenabled"] ?? false; }); -export function CodeEditor({ text, language, filename, onChange, onMount }: CodeEditorProps) { +export function CodeEditor({ text, language, filename, meta, onChange, onMount }: CodeEditorProps) { const divRef = useRef(null); const unmountRef = useRef<() => void>(null); const minimapEnabled = useAtomValue(minimapEnabledAtom); @@ -157,8 +158,9 @@ export function CodeEditor({ text, language, filename, onChange, onMount }: Code const opts = defaultEditorOptions(); opts.minimap.enabled = minimapEnabled; opts.stickyScroll.enabled = stickyScrollEnabled; + opts.wordWrap = meta?.["editor:wordwrap"] ? "on" : "off"; return opts; - }, [minimapEnabled, stickyScrollEnabled]); + }, [minimapEnabled, stickyScrollEnabled, meta?.["editor:wordwrap"]]); return (
diff --git a/frontend/app/view/preview/directorypreview.tsx b/frontend/app/view/preview/directorypreview.tsx index 011cbfc52..b76b60baa 100644 --- a/frontend/app/view/preview/directorypreview.tsx +++ b/frontend/app/view/preview/directorypreview.tsx @@ -337,6 +337,13 @@ function DirectoryTable({ ); } +function getNormFilePath(finfo: FileInfo): string { + if (finfo.isdir) { + return finfo.dir; + } + return finfo.dir + "/" + finfo.name; +} + interface TableBodyProps { bodyRef: React.RefObject; model: PreviewModel; @@ -391,10 +398,25 @@ function TableBody({ }, [focusIndex]); const handleFileContextMenu = useCallback( - (e: any, path: string, mimetype: string) => { + (e: any, finfo: FileInfo) => { e.preventDefault(); e.stopPropagation(); - const fileName = path.split("/").pop(); + if (finfo == null) { + return; + } + const normPath = getNormFilePath(finfo); + const fileName = finfo.path.split("/").pop(); + let openNativeLabel = "Open File"; + if (finfo.isdir) { + openNativeLabel = "Open Directory in File Manager"; + if (PLATFORM == "darwin") { + openNativeLabel = "Open Directory in Finder"; + } else if (PLATFORM == "win32") { + openNativeLabel = "Open Directory in Explorer"; + } + } else { + openNativeLabel = "Open File in Default Application"; + } const menu: ContextMenuItem[] = [ { label: "Copy File Name", @@ -402,7 +424,7 @@ function TableBody({ }, { label: "Copy Full File Name", - click: () => navigator.clipboard.writeText(path), + click: () => navigator.clipboard.writeText(finfo.path), }, { label: "Copy File Name (Shell Quoted)", @@ -410,7 +432,7 @@ function TableBody({ }, { label: "Copy Full File Name (Shell Quoted)", - click: () => navigator.clipboard.writeText(shellQuote([path])), + click: () => navigator.clipboard.writeText(shellQuote([finfo.path])), }, { type: "separator", @@ -418,7 +440,16 @@ function TableBody({ { label: "Download File", click: async () => { - getApi().downloadFile(path); + getApi().downloadFile(normPath); + }, + }, + { + type: "separator", + }, + { + label: openNativeLabel, + click: async () => { + getApi().openNativePath(normPath); }, }, { @@ -430,14 +461,14 @@ function TableBody({ const blockDef: BlockDef = { meta: { view: "preview", - file: path, + file: finfo.path, }, }; await createBlock(blockDef); }, }, ]; - if (mimetype == "directory") { + if (finfo.mimetype == "directory") { menu.push({ label: "Open Terminal in New Block", click: async () => { @@ -445,7 +476,7 @@ function TableBody({ meta: { controller: "shell", view: "term", - "cmd:cwd": path, + "cmd:cwd": finfo.path, }, }; await createBlock(termBlockDef); @@ -456,7 +487,7 @@ function TableBody({ menu.push({ label: "Delete File", click: async () => { - await FileService.DeleteFile(conn, path).catch((e) => console.log(e)); + await FileService.DeleteFile(conn, finfo.path).catch((e) => console.log(e)); setRefreshVersion((current) => current + 1); }, }); @@ -477,7 +508,7 @@ function TableBody({ setSearch(""); }} onClick={() => setFocusIndex(idx)} - onContextMenu={(e) => handleFileContextMenu(e, row.getValue("path"), row.getValue("mimetype"))} + onContextMenu={(e) => handleFileContextMenu(e, row.original)} > {row.getVisibleCells().map((cell) => (
{ @@ -670,6 +672,18 @@ export class PreviewModel implements ViewModel { click: this.handleFileRevert.bind(this), }); } + menuItems.push({ type: "separator" }); + menuItems.push({ + label: "Word Wrap", + type: "checkbox", + checked: blockData?.meta?.["editor:wordwrap"] ?? false, + click: () => { + const blockOref = WOS.makeORef("block", this.blockId); + services.ObjectService.UpdateObjectMeta(blockOref, { + "editor:wordwrap": !blockData?.meta?.["editor:wordwrap"], + }); + }, + }); } } return menuItems; @@ -803,13 +817,14 @@ function CodeEditPreview({ model }: SpecializedViewProps) { const fileContent = useAtomValue(model.fileContent); const setNewFileContent = useSetAtom(model.newFileContent); const fileName = useAtomValue(model.statFilePath); + const blockMeta = useAtomValue(model.blockAtom)?.meta; function codeEditKeyDownHandler(e: WaveKeyboardEvent): boolean { if (checkKeyPressed(e, "Cmd:e")) { model.setEditMode(false); return true; } - if (checkKeyPressed(e, "Cmd:s")) { + if (checkKeyPressed(e, "Cmd:s") || checkKeyPressed(e, "Ctrl:s")) { model.handleFileSave(); return true; } @@ -852,6 +867,7 @@ function CodeEditPreview({ model }: SpecializedViewProps) { setNewFileContent(text)} onMount={onMount} /> diff --git a/frontend/app/view/term/term.tsx b/frontend/app/view/term/term.tsx index aa1f30359..28b2baf09 100644 --- a/frontend/app/view/term/term.tsx +++ b/frontend/app/view/term/term.tsx @@ -132,7 +132,7 @@ class TermViewModel { this.blockBg = jotai.atom((get) => { const blockData = get(this.blockAtom); const fullConfig = get(atoms.fullConfigAtom); - let themeName: string = globalStore.get(getSettingsKeyAtom("term:theme")); + let themeName: string = get(getSettingsKeyAtom("term:theme")); if (blockData?.meta?.["term:theme"]) { themeName = blockData.meta["term:theme"]; } diff --git a/frontend/types/custom.d.ts b/frontend/types/custom.d.ts index f0f0e0f6d..f80859197 100644 --- a/frontend/types/custom.d.ts +++ b/frontend/types/custom.d.ts @@ -94,6 +94,7 @@ declare global { onWaveInit: (callback: (initOpts: WaveInitOpts) => void) => void; sendLog: (log: string) => void; onQuicklook: (filePath: string) => void; + openNativePath(filePath: string): void; }; type ElectronContextMenuItem = { diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts index 37ced6401..cf26cbd80 100644 --- a/frontend/types/gotypes.d.ts +++ b/frontend/types/gotypes.d.ts @@ -315,7 +315,7 @@ declare global { "frame:*"?: boolean; frame?: boolean; "frame:bordercolor"?: string; - "frame:bordercolor:focused"?: string; + "frame:activebordercolor"?: string; "frame:title"?: string; "frame:icon"?: string; "frame:text"?: string; @@ -340,6 +340,8 @@ declare global { "ai:apiversion"?: string; "ai:maxtokens"?: number; "ai:timeoutms"?: number; + "editor:*"?: boolean; + "editor:wordwrap"?: boolean; "graph:*"?: boolean; "graph:numpoints"?: number; "graph:metrics"?: string[]; @@ -348,6 +350,8 @@ declare global { bg?: string; "bg:opacity"?: number; "bg:blendmode"?: string; + "bg:bordercolor"?: string; + "bg:activebordercolor"?: string; "term:*"?: boolean; "term:fontsize"?: number; "term:fontfamily"?: string; diff --git a/pkg/waveobj/metaconsts.go b/pkg/waveobj/metaconsts.go index fc3a73584..a906990b6 100644 --- a/pkg/waveobj/metaconsts.go +++ b/pkg/waveobj/metaconsts.go @@ -32,7 +32,7 @@ const ( MetaKey_FrameClear = "frame:*" MetaKey_Frame = "frame" MetaKey_FrameBorderColor = "frame:bordercolor" - MetaKey_FrameBorderColor_Focused = "frame:bordercolor:focused" + MetaKey_FrameActiveBorderColor = "frame:activebordercolor" MetaKey_FrameTitle = "frame:title" MetaKey_FrameIcon = "frame:icon" MetaKey_FrameText = "frame:text" @@ -60,6 +60,9 @@ const ( MetaKey_AiMaxTokens = "ai:maxtokens" MetaKey_AiTimeoutMs = "ai:timeoutms" + MetaKey_EditorClear = "editor:*" + MetaKey_EditorWordWrap = "editor:wordwrap" + MetaKey_GraphClear = "graph:*" MetaKey_GraphNumPoints = "graph:numpoints" MetaKey_GraphMetrics = "graph:metrics" @@ -70,6 +73,8 @@ const ( MetaKey_Bg = "bg" MetaKey_BgOpacity = "bg:opacity" MetaKey_BgBlendMode = "bg:blendmode" + MetaKey_BgBorderColor = "bg:bordercolor" + MetaKey_BgActiveBorderColor = "bg:activebordercolor" MetaKey_TermClear = "term:*" MetaKey_TermFontSize = "term:fontsize" diff --git a/pkg/waveobj/wtypemeta.go b/pkg/waveobj/wtypemeta.go index 367f44bc4..8007ce6a5 100644 --- a/pkg/waveobj/wtypemeta.go +++ b/pkg/waveobj/wtypemeta.go @@ -28,13 +28,13 @@ type MetaTSType struct { Icon string `json:"icon,omitempty"` IconColor string `json:"icon:color,omitempty"` - FrameClear bool `json:"frame:*,omitempty"` - Frame bool `json:"frame,omitempty"` - FrameBorderColor string `json:"frame:bordercolor,omitempty"` - FrameBorderColor_Focused string `json:"frame:bordercolor:focused,omitempty"` - FrameTitle string `json:"frame:title,omitempty"` - FrameIcon string `json:"frame:icon,omitempty"` - FrameText string `json:"frame:text,omitempty"` + FrameClear bool `json:"frame:*,omitempty"` + Frame bool `json:"frame,omitempty"` + FrameBorderColor string `json:"frame:bordercolor,omitempty"` + FrameActiveBorderColor string `json:"frame:activebordercolor,omitempty"` + FrameTitle string `json:"frame:title,omitempty"` + FrameIcon string `json:"frame:icon,omitempty"` + FrameText string `json:"frame:text,omitempty"` CmdClear bool `json:"cmd:*,omitempty"` Cmd string `json:"cmd,omitempty"` @@ -60,6 +60,9 @@ type MetaTSType struct { AiMaxTokens float64 `json:"ai:maxtokens,omitempty"` AiTimeoutMs float64 `json:"ai:timeoutms,omitempty"` + EditorClear bool `json:"editor:*,omitempty"` + EditorWordWrap bool `json:"editor:wordwrap,omitempty"` + GraphClear bool `json:"graph:*,omitempty"` GraphNumPoints int `json:"graph:numpoints,omitempty"` GraphMetrics []string `json:"graph:metrics,omitempty"` @@ -67,10 +70,12 @@ type MetaTSType struct { SysinfoType string `json:"sysinfo:type,omitempty"` // for tabs - BgClear bool `json:"bg:*,omitempty"` - Bg string `json:"bg,omitempty"` - BgOpacity float64 `json:"bg:opacity,omitempty"` - BgBlendMode string `json:"bg:blendmode,omitempty"` + BgClear bool `json:"bg:*,omitempty"` + Bg string `json:"bg,omitempty"` + BgOpacity float64 `json:"bg:opacity,omitempty"` + BgBlendMode string `json:"bg:blendmode,omitempty"` + BgBorderColor string `json:"bg:bordercolor,omitempty"` // frame:bordercolor + BgActiveBorderColor string `json:"bg:activebordercolor,omitempty"` // frame:activebordercolor TermClear bool `json:"term:*,omitempty"` TermFontSize int `json:"term:fontsize,omitempty"` diff --git a/pkg/wconfig/defaultconfig/presets.json b/pkg/wconfig/defaultconfig/presets.json index 3f1a38135..0cea85de6 100644 --- a/pkg/wconfig/defaultconfig/presets.json +++ b/pkg/wconfig/defaultconfig/presets.json @@ -23,14 +23,16 @@ "display:order": 1.1, "bg:*": true, "bg": "blue", - "bg:opacity": 0.3 + "bg:opacity": 0.3, + "bg:activebordercolor": "rgba(0, 0, 255, 1.0)" }, "bg@red": { "display:name": "Red", "display:order": 1.3, "bg:*": true, "bg": "red", - "bg:opacity": 0.3 + "bg:opacity": 0.3, + "bg:activebordercolor": "rgba(255, 0, 0, 1.0)" }, "bg@ocean-depths": { "display:name": "Ocean Depths",