mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-01-31 23:11:28 +01:00
parent
502f9515d8
commit
473225d94b
@ -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}",
|
||||
|
@ -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<void> {
|
||||
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,
|
||||
};
|
||||
|
@ -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",
|
||||
});
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
@ -375,12 +375,24 @@ const BlockMask = React.memo(({ nodeModel }: { nodeModel: NodeModel }) => {
|
||||
const [blockData] = WOS.useWaveObjectValue<Block>(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) {
|
||||
|
@ -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<HTMLDivElement>(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 (
|
||||
<div className="code-editor-wrapper">
|
||||
|
@ -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<HTMLDivElement>;
|
||||
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) => (
|
||||
<div
|
||||
|
@ -62,6 +62,7 @@ const textApplicationMimetypes = [
|
||||
"application/x-latex",
|
||||
"application/x-sh",
|
||||
"application/x-python",
|
||||
"application/x-awk",
|
||||
];
|
||||
|
||||
function isTextFile(mimeType: string): boolean {
|
||||
@ -619,6 +620,7 @@ export class PreviewModel implements ViewModel {
|
||||
|
||||
getSettingsMenuItems(): ContextMenuItem[] {
|
||||
const menuItems: ContextMenuItem[] = [];
|
||||
const blockData = globalStore.get(this.blockAtom);
|
||||
menuItems.push({
|
||||
label: "Copy Full Path",
|
||||
click: async () => {
|
||||
@ -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) {
|
||||
<CodeEditor
|
||||
text={fileContent}
|
||||
filename={fileName}
|
||||
meta={blockMeta}
|
||||
onChange={(text) => setNewFileContent(text)}
|
||||
onMount={onMount}
|
||||
/>
|
||||
|
@ -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"];
|
||||
}
|
||||
|
1
frontend/types/custom.d.ts
vendored
1
frontend/types/custom.d.ts
vendored
@ -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 = {
|
||||
|
6
frontend/types/gotypes.d.ts
vendored
6
frontend/types/gotypes.d.ts
vendored
@ -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;
|
||||
|
@ -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"
|
||||
|
@ -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"`
|
||||
|
@ -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",
|
||||
|
Loading…
Reference in New Issue
Block a user