mirror of
https://github.com/wavetermdev/waveterm.git
synced 2024-12-22 16:48:23 +01:00
preview refactor for keyboard/focus (#303)
This commit is contained in:
parent
226bc4ee6f
commit
e3b7ab73c0
@ -32,7 +32,7 @@ function makeViewModel(blockId: string, blockView: string, nodeModel: NodeModel)
|
|||||||
return makeTerminalModel(blockId);
|
return makeTerminalModel(blockId);
|
||||||
}
|
}
|
||||||
if (blockView === "preview") {
|
if (blockView === "preview") {
|
||||||
return makePreviewModel(blockId);
|
return makePreviewModel(blockId, nodeModel);
|
||||||
}
|
}
|
||||||
if (blockView === "web") {
|
if (blockView === "web") {
|
||||||
return makeWebViewModel(blockId, nodeModel);
|
return makeWebViewModel(blockId, nodeModel);
|
||||||
@ -231,7 +231,7 @@ const BlockFull = React.memo(({ nodeModel, viewModel }: FullBlockProps) => {
|
|||||||
type="text"
|
type="text"
|
||||||
value=""
|
value=""
|
||||||
ref={focusElemRef}
|
ref={focusElemRef}
|
||||||
id={`${nodeModel.blockId}-dummy-focus`}
|
id={`${nodeModel.blockId}-dummy-focus`} // don't change this name (used in refocusNode)
|
||||||
className="dummy-focus"
|
className="dummy-focus"
|
||||||
onChange={() => {}}
|
onChange={() => {}}
|
||||||
/>
|
/>
|
||||||
|
@ -142,7 +142,6 @@ const BlockFrame_Header = ({
|
|||||||
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", nodeModel.blockId));
|
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", nodeModel.blockId));
|
||||||
const viewName = util.useAtomValueSafe(viewModel.viewName) ?? blockViewToName(blockData?.meta?.view);
|
const viewName = util.useAtomValueSafe(viewModel.viewName) ?? blockViewToName(blockData?.meta?.view);
|
||||||
const showBlockIds = jotai.useAtomValue(useSettingsKeyAtom("blockheader:showblockids"));
|
const showBlockIds = jotai.useAtomValue(useSettingsKeyAtom("blockheader:showblockids"));
|
||||||
const settingsConfig = jotai.useAtomValue(atoms.settingsAtom);
|
|
||||||
const viewIconUnion = util.useAtomValueSafe(viewModel.viewIcon) ?? blockViewToIcon(blockData?.meta?.view);
|
const viewIconUnion = util.useAtomValueSafe(viewModel.viewIcon) ?? blockViewToIcon(blockData?.meta?.view);
|
||||||
const preIconButton = util.useAtomValueSafe(viewModel.preIconButton);
|
const preIconButton = util.useAtomValueSafe(viewModel.preIconButton);
|
||||||
const headerTextUnion = util.useAtomValueSafe(viewModel.viewText);
|
const headerTextUnion = util.useAtomValueSafe(viewModel.viewText);
|
||||||
@ -186,14 +185,14 @@ const BlockFrame_Header = ({
|
|||||||
const connButtonElem = (
|
const connButtonElem = (
|
||||||
<ConnectionButton
|
<ConnectionButton
|
||||||
ref={connBtnRef}
|
ref={connBtnRef}
|
||||||
key={nodeModel.blockId}
|
key="connbutton"
|
||||||
connection={blockData?.meta?.connection}
|
connection={blockData?.meta?.connection}
|
||||||
changeConnModalAtom={changeConnModalAtom}
|
changeConnModalAtom={changeConnModalAtom}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
headerTextElems.unshift(connButtonElem);
|
headerTextElems.unshift(connButtonElem);
|
||||||
}
|
}
|
||||||
headerTextElems.unshift(<ControllerStatusIcon blockId={nodeModel.blockId} />);
|
headerTextElems.unshift(<ControllerStatusIcon key="connstatus" blockId={nodeModel.blockId} />);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="block-frame-default-header" ref={dragHandleRef} onContextMenu={onContextMenu}>
|
<div className="block-frame-default-header" ref={dragHandleRef} onContextMenu={onContextMenu}>
|
||||||
@ -327,6 +326,7 @@ const BlockFrame_Default_Component = (props: BlockFrameProps) => {
|
|||||||
{preview ? null : (
|
{preview ? null : (
|
||||||
<ChangeConnectionBlockModal
|
<ChangeConnectionBlockModal
|
||||||
blockId={nodeModel.blockId}
|
blockId={nodeModel.blockId}
|
||||||
|
nodeModel={nodeModel}
|
||||||
viewModel={viewModel}
|
viewModel={viewModel}
|
||||||
blockRef={blockModel?.blockRef}
|
blockRef={blockModel?.blockRef}
|
||||||
changeConnModalAtom={changeConnModalAtom}
|
changeConnModalAtom={changeConnModalAtom}
|
||||||
@ -344,16 +344,19 @@ const ChangeConnectionBlockModal = React.memo(
|
|||||||
blockRef,
|
blockRef,
|
||||||
connBtnRef,
|
connBtnRef,
|
||||||
changeConnModalAtom,
|
changeConnModalAtom,
|
||||||
|
nodeModel,
|
||||||
}: {
|
}: {
|
||||||
blockId: string;
|
blockId: string;
|
||||||
viewModel: ViewModel;
|
viewModel: ViewModel;
|
||||||
blockRef: React.RefObject<HTMLDivElement>;
|
blockRef: React.RefObject<HTMLDivElement>;
|
||||||
connBtnRef: React.RefObject<HTMLDivElement>;
|
connBtnRef: React.RefObject<HTMLDivElement>;
|
||||||
changeConnModalAtom: jotai.PrimitiveAtom<boolean>;
|
changeConnModalAtom: jotai.PrimitiveAtom<boolean>;
|
||||||
|
nodeModel: NodeModel;
|
||||||
}) => {
|
}) => {
|
||||||
const [connSelected, setConnSelected] = React.useState("");
|
const [connSelected, setConnSelected] = React.useState("");
|
||||||
const changeConnModalOpen = jotai.useAtomValue(changeConnModalAtom);
|
const changeConnModalOpen = jotai.useAtomValue(changeConnModalAtom);
|
||||||
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
|
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
|
||||||
|
const isNodeFocused = jotai.useAtomValue(nodeModel.isFocused);
|
||||||
const changeConnection = React.useCallback(
|
const changeConnection = React.useCallback(
|
||||||
async (connName: string) => {
|
async (connName: string) => {
|
||||||
const oldCwd = blockData?.meta?.file ?? "";
|
const oldCwd = blockData?.meta?.file ?? "";
|
||||||
@ -401,6 +404,7 @@ const ChangeConnectionBlockModal = React.memo(
|
|||||||
changeConnection(selected);
|
changeConnection(selected);
|
||||||
globalStore.set(changeConnModalAtom, false);
|
globalStore.set(changeConnModalAtom, false);
|
||||||
}}
|
}}
|
||||||
|
autoFocus={isNodeFocused}
|
||||||
onKeyDown={(e) => keyutil.keydownWrapper(handleTypeAheadKeyDown)(e)}
|
onKeyDown={(e) => keyutil.keydownWrapper(handleTypeAheadKeyDown)(e)}
|
||||||
onChange={(current: string) => setConnSelected(current)}
|
onChange={(current: string) => setConnSelected(current)}
|
||||||
value={connSelected}
|
value={connSelected}
|
||||||
|
@ -189,6 +189,7 @@ export const ControllerStatusIcon = React.memo(({ blockId }: { blockId: string }
|
|||||||
}
|
}
|
||||||
const controllerStatusElem = (
|
const controllerStatusElem = (
|
||||||
<i
|
<i
|
||||||
|
key="controller-status"
|
||||||
className="fa-sharp fa-solid fa-triangle-exclamation"
|
className="fa-sharp fa-solid fa-triangle-exclamation"
|
||||||
title="Controller Is Not Running"
|
title="Controller Is Not Running"
|
||||||
style={{ color: "var(--error-color)" }}
|
style={{ color: "var(--error-color)" }}
|
||||||
|
@ -24,6 +24,7 @@ interface InputProps {
|
|||||||
autoFocus?: boolean;
|
autoFocus?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
isNumber?: boolean;
|
isNumber?: boolean;
|
||||||
|
inputRef?: React.MutableRefObject<HTMLInputElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Input = forwardRef<HTMLDivElement, InputProps>(
|
const Input = forwardRef<HTMLDivElement, InputProps>(
|
||||||
@ -44,6 +45,7 @@ const Input = forwardRef<HTMLDivElement, InputProps>(
|
|||||||
autoFocus,
|
autoFocus,
|
||||||
disabled,
|
disabled,
|
||||||
isNumber,
|
isNumber,
|
||||||
|
inputRef,
|
||||||
}: InputProps,
|
}: InputProps,
|
||||||
ref
|
ref
|
||||||
) => {
|
) => {
|
||||||
@ -51,7 +53,7 @@ const Input = forwardRef<HTMLDivElement, InputProps>(
|
|||||||
const [internalValue, setInternalValue] = useState(defaultValue);
|
const [internalValue, setInternalValue] = useState(defaultValue);
|
||||||
const [error, setError] = useState(false);
|
const [error, setError] = useState(false);
|
||||||
const [hasContent, setHasContent] = useState(Boolean(value || defaultValue));
|
const [hasContent, setHasContent] = useState(Boolean(value || defaultValue));
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const internalInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (value !== undefined) {
|
if (value !== undefined) {
|
||||||
@ -60,25 +62,32 @@ const Input = forwardRef<HTMLDivElement, InputProps>(
|
|||||||
}, [value]);
|
}, [value]);
|
||||||
|
|
||||||
const handleComponentFocus = () => {
|
const handleComponentFocus = () => {
|
||||||
if (inputRef.current && !inputRef.current.contains(document.activeElement)) {
|
if (internalInputRef.current && !internalInputRef.current.contains(document.activeElement)) {
|
||||||
inputRef.current.focus();
|
internalInputRef.current.focus();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleComponentBlur = () => {
|
const handleComponentBlur = () => {
|
||||||
if (inputRef.current?.contains(document.activeElement)) {
|
if (internalInputRef.current?.contains(document.activeElement)) {
|
||||||
inputRef.current.blur();
|
internalInputRef.current.blur();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSetInputRef = (elem: HTMLInputElement) => {
|
||||||
|
if (inputRef) {
|
||||||
|
inputRef.current = elem;
|
||||||
|
}
|
||||||
|
internalInputRef.current = elem;
|
||||||
|
};
|
||||||
|
|
||||||
const handleFocus = () => {
|
const handleFocus = () => {
|
||||||
setFocused(true);
|
setFocused(true);
|
||||||
onFocus && onFocus();
|
onFocus && onFocus();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBlur = () => {
|
const handleBlur = () => {
|
||||||
if (inputRef.current) {
|
if (internalInputRef.current) {
|
||||||
const inputValue = inputRef.current.value;
|
const inputValue = internalInputRef.current.value;
|
||||||
if (required && !inputValue) {
|
if (required && !inputValue) {
|
||||||
setError(true);
|
setError(true);
|
||||||
setFocused(false);
|
setFocused(false);
|
||||||
@ -144,7 +153,7 @@ const Input = forwardRef<HTMLDivElement, InputProps>(
|
|||||||
className={clsx("input-inner-input", {
|
className={clsx("input-inner-input", {
|
||||||
"offset-left": decoration?.startDecoration,
|
"offset-left": decoration?.startDecoration,
|
||||||
})}
|
})}
|
||||||
ref={inputRef}
|
ref={handleSetInputRef}
|
||||||
id={label}
|
id={label}
|
||||||
value={inputValue}
|
value={inputValue}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
|
@ -3,7 +3,7 @@ import { InputDecoration } from "@/app/element/inputdecoration";
|
|||||||
import { useDimensions } from "@/app/hook/useDimensions";
|
import { useDimensions } from "@/app/hook/useDimensions";
|
||||||
import { makeIconClass } from "@/util/util";
|
import { makeIconClass } from "@/util/util";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import React, { forwardRef, useEffect, useRef, useState } from "react";
|
import React, { forwardRef, useEffect, useLayoutEffect, useRef, useState } from "react";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
|
|
||||||
import "./typeaheadmodal.less";
|
import "./typeaheadmodal.less";
|
||||||
@ -82,6 +82,8 @@ interface TypeAheadModalProps {
|
|||||||
onSelect?: (_: string) => void;
|
onSelect?: (_: string) => void;
|
||||||
onClickBackdrop?: () => void;
|
onClickBackdrop?: () => void;
|
||||||
onKeyDown?: (_) => void;
|
onKeyDown?: (_) => void;
|
||||||
|
giveFocusRef?: React.MutableRefObject<() => boolean>;
|
||||||
|
autoFocus?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TypeAheadModal = ({
|
const TypeAheadModal = ({
|
||||||
@ -95,10 +97,13 @@ const TypeAheadModal = ({
|
|||||||
onSelect,
|
onSelect,
|
||||||
onKeyDown,
|
onKeyDown,
|
||||||
onClickBackdrop,
|
onClickBackdrop,
|
||||||
|
giveFocusRef,
|
||||||
|
autoFocus,
|
||||||
}: TypeAheadModalProps) => {
|
}: TypeAheadModalProps) => {
|
||||||
const { width, height } = useDimensions(blockRef);
|
const { width, height } = useDimensions(blockRef);
|
||||||
const modalRef = useRef<HTMLDivElement>(null);
|
const modalRef = useRef<HTMLDivElement>(null);
|
||||||
const inputRef = useRef<HTMLDivElement>(null);
|
const inputRef = useRef<HTMLDivElement>(null);
|
||||||
|
const realInputRef = useRef<HTMLInputElement>(null);
|
||||||
const suggestionsRef = useRef<HTMLDivElement>(null);
|
const suggestionsRef = useRef<HTMLDivElement>(null);
|
||||||
const [suggestionsHeight, setSuggestionsHeight] = useState<number | undefined>(undefined);
|
const [suggestionsHeight, setSuggestionsHeight] = useState<number | undefined>(undefined);
|
||||||
const [modalHeight, setModalHeight] = useState<string | undefined>(undefined);
|
const [modalHeight, setModalHeight] = useState<string | undefined>(undefined);
|
||||||
@ -125,6 +130,20 @@ const TypeAheadModal = ({
|
|||||||
}
|
}
|
||||||
}, [height, suggestions]);
|
}, [height, suggestions]);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (giveFocusRef) {
|
||||||
|
giveFocusRef.current = () => {
|
||||||
|
realInputRef.current?.focus();
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
if (giveFocusRef) {
|
||||||
|
giveFocusRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [giveFocusRef]);
|
||||||
|
|
||||||
const renderBackdrop = (onClick) => <div className="type-ahead-modal-backdrop" onClick={onClick}></div>;
|
const renderBackdrop = (onClick) => <div className="type-ahead-modal-backdrop" onClick={onClick}></div>;
|
||||||
|
|
||||||
const handleKeyDown = (e) => {
|
const handleKeyDown = (e) => {
|
||||||
@ -167,9 +186,10 @@ const TypeAheadModal = ({
|
|||||||
<div className={clsx("content-wrapper", { "has-suggestions": suggestions?.length })}>
|
<div className={clsx("content-wrapper", { "has-suggestions": suggestions?.length })}>
|
||||||
<Input
|
<Input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
|
inputRef={realInputRef}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
value={value}
|
value={value}
|
||||||
autoFocus
|
autoFocus={autoFocus}
|
||||||
placeholder={label}
|
placeholder={label}
|
||||||
decoration={{
|
decoration={{
|
||||||
endDecoration: (
|
endDecoration: (
|
||||||
|
@ -537,6 +537,24 @@ function getViewModel(blockId: string): ViewModel {
|
|||||||
return blockViewModelMap.get(blockId);
|
return blockViewModelMap.get(blockId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function refocusNode(blockId: string) {
|
||||||
|
if (blockId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const layoutModel = getLayoutModelForActiveTab();
|
||||||
|
const layoutNodeId = layoutModel.getNodeByBlockId(blockId);
|
||||||
|
if (layoutNodeId?.id == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
layoutModel.focusNode(layoutNodeId.id);
|
||||||
|
const viewModel = getViewModel(blockId);
|
||||||
|
const ok = viewModel?.giveFocus?.();
|
||||||
|
if (!ok) {
|
||||||
|
const inputElem = document.getElementById(`${blockId}-dummy-focus`);
|
||||||
|
inputElem?.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function countersClear() {
|
function countersClear() {
|
||||||
Counters.clear();
|
Counters.clear();
|
||||||
}
|
}
|
||||||
@ -615,6 +633,7 @@ export {
|
|||||||
loadConnStatus,
|
loadConnStatus,
|
||||||
openLink,
|
openLink,
|
||||||
PLATFORM,
|
PLATFORM,
|
||||||
|
refocusNode,
|
||||||
registerViewModel,
|
registerViewModel,
|
||||||
sendWSCommand,
|
sendWSCommand,
|
||||||
setNodeFocus,
|
setNodeFocus,
|
||||||
|
@ -35,20 +35,20 @@ function unsetControlShift() {
|
|||||||
globalStore.set(atoms.controlShiftDelayAtom, false);
|
globalStore.set(atoms.controlShiftDelayAtom, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
function shouldDispatchToBlock(): boolean {
|
function shouldDispatchToBlock(e: WaveKeyboardEvent): boolean {
|
||||||
if (globalStore.get(atoms.modalOpen)) {
|
if (globalStore.get(atoms.modalOpen)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const activeElem = document.activeElement;
|
const activeElem = document.activeElement;
|
||||||
if (activeElem != null && activeElem instanceof HTMLElement) {
|
if (activeElem != null && activeElem instanceof HTMLElement) {
|
||||||
if (activeElem.tagName == "INPUT" || activeElem.tagName == "TEXTAREA") {
|
if (activeElem.tagName == "INPUT" || activeElem.tagName == "TEXTAREA" || activeElem.contentEditable == "true") {
|
||||||
if (activeElem.classList.contains("dummy-focus")) {
|
if (activeElem.classList.contains("dummy-focus")) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
if (keyutil.isInputEvent(e)) {
|
||||||
}
|
return false;
|
||||||
if (activeElem.contentEditable == "true") {
|
}
|
||||||
return false;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@ -144,7 +144,7 @@ function appHandleKeyDown(waveEvent: WaveKeyboardEvent): boolean {
|
|||||||
const layoutModel = getLayoutModelForActiveTab();
|
const layoutModel = getLayoutModelForActiveTab();
|
||||||
const focusedNode = globalStore.get(layoutModel.focusedNode);
|
const focusedNode = globalStore.get(layoutModel.focusedNode);
|
||||||
const blockId = focusedNode?.data?.blockId;
|
const blockId = focusedNode?.data?.blockId;
|
||||||
if (blockId != null && shouldDispatchToBlock()) {
|
if (blockId != null && shouldDispatchToBlock(waveEvent)) {
|
||||||
const viewModel = getViewModel(blockId);
|
const viewModel = getViewModel(blockId);
|
||||||
if (viewModel?.keyDownHandler) {
|
if (viewModel?.keyDownHandler) {
|
||||||
const handledByBlock = viewModel.keyDownHandler(waveEvent);
|
const handledByBlock = viewModel.keyDownHandler(waveEvent);
|
||||||
@ -172,6 +172,10 @@ function registerElectronReinjectKeyHandler() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function tryReinjectKey(event: WaveKeyboardEvent): boolean {
|
||||||
|
return appHandleKeyDown(event);
|
||||||
|
}
|
||||||
|
|
||||||
function registerGlobalKeys() {
|
function registerGlobalKeys() {
|
||||||
globalKeyMap.set("Cmd:]", () => {
|
globalKeyMap.set("Cmd:]", () => {
|
||||||
switchTab(1);
|
switchTab(1);
|
||||||
@ -275,5 +279,6 @@ export {
|
|||||||
registerControlShiftStateUpdateHandler,
|
registerControlShiftStateUpdateHandler,
|
||||||
registerElectronReinjectKeyHandler,
|
registerElectronReinjectKeyHandler,
|
||||||
registerGlobalKeys,
|
registerGlobalKeys,
|
||||||
|
tryReinjectKey,
|
||||||
unsetControlShift,
|
unsetControlShift,
|
||||||
};
|
};
|
||||||
|
@ -5,15 +5,14 @@ import { useHeight } from "@/app/hook/useHeight";
|
|||||||
import loader from "@monaco-editor/loader";
|
import loader from "@monaco-editor/loader";
|
||||||
import { Editor, Monaco } from "@monaco-editor/react";
|
import { Editor, Monaco } from "@monaco-editor/react";
|
||||||
import type * as MonacoTypes from "monaco-editor/esm/vs/editor/editor.api";
|
import type * as MonacoTypes from "monaco-editor/esm/vs/editor/editor.api";
|
||||||
import { useEffect, useRef } from "react";
|
import React, { useRef } from "react";
|
||||||
|
|
||||||
import { adaptFromReactOrNativeKeyEvent, checkKeyPressed } from "@/util/keyutil";
|
|
||||||
import "./codeeditor.less";
|
import "./codeeditor.less";
|
||||||
|
|
||||||
// there is a global monaco variable (TODO get the correct TS type)
|
// there is a global monaco variable (TODO get the correct TS type)
|
||||||
declare var monaco: Monaco;
|
declare var monaco: Monaco;
|
||||||
|
|
||||||
function loadMonaco() {
|
export function loadMonaco() {
|
||||||
loader.config({ paths: { vs: "monaco" } });
|
loader.config({ paths: { vs: "monaco" } });
|
||||||
loader
|
loader
|
||||||
.init()
|
.init()
|
||||||
@ -40,11 +39,6 @@ function loadMonaco() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: need to update these on theme change (pull from CSS vars)
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
|
||||||
setTimeout(loadMonaco, 30);
|
|
||||||
});
|
|
||||||
|
|
||||||
function defaultEditorOptions(): MonacoTypes.editor.IEditorOptions {
|
function defaultEditorOptions(): MonacoTypes.editor.IEditorOptions {
|
||||||
const opts: MonacoTypes.editor.IEditorOptions = {
|
const opts: MonacoTypes.editor.IEditorOptions = {
|
||||||
scrollBeyondLastLine: false,
|
scrollBeyondLastLine: false,
|
||||||
@ -66,59 +60,23 @@ interface CodeEditorProps {
|
|||||||
filename: string;
|
filename: string;
|
||||||
language?: string;
|
language?: string;
|
||||||
onChange?: (text: string) => void;
|
onChange?: (text: string) => void;
|
||||||
onSave?: () => void;
|
onMount?: (monacoPtr: MonacoTypes.editor.IStandaloneCodeEditor, monaco: Monaco) => () => void;
|
||||||
onCancel?: () => void;
|
|
||||||
onEdit?: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CodeEditor({
|
export function CodeEditor({ parentRef, text, language, filename, onChange, onMount }: CodeEditorProps) {
|
||||||
parentRef,
|
|
||||||
text,
|
|
||||||
language,
|
|
||||||
filename,
|
|
||||||
onChange,
|
|
||||||
onSave,
|
|
||||||
onCancel,
|
|
||||||
onEdit,
|
|
||||||
}: CodeEditorProps) {
|
|
||||||
const divRef = useRef<HTMLDivElement>(null);
|
const divRef = useRef<HTMLDivElement>(null);
|
||||||
|
const unmountRef = useRef<() => void>(null);
|
||||||
const parentHeight = useHeight(parentRef);
|
const parentHeight = useHeight(parentRef);
|
||||||
const theme = "wave-theme-dark";
|
const theme = "wave-theme-dark";
|
||||||
|
|
||||||
useEffect(() => {
|
React.useEffect(() => {
|
||||||
function handleKeyDown(e: KeyboardEvent) {
|
|
||||||
const waveEvent = adaptFromReactOrNativeKeyEvent(e);
|
|
||||||
if (onSave) {
|
|
||||||
if (checkKeyPressed(waveEvent, "Cmd:s")) {
|
|
||||||
e.preventDefault();
|
|
||||||
onSave();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (onCancel) {
|
|
||||||
if (checkKeyPressed(waveEvent, "Cmd:r")) {
|
|
||||||
e.preventDefault();
|
|
||||||
onCancel();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (onEdit) {
|
|
||||||
if (checkKeyPressed(waveEvent, "Cmd:e")) {
|
|
||||||
e.preventDefault();
|
|
||||||
onEdit();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentParentRef = parentRef.current;
|
|
||||||
currentParentRef.addEventListener("keydown", handleKeyDown);
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
currentParentRef.removeEventListener("keydown", handleKeyDown);
|
// unmount function
|
||||||
|
if (unmountRef.current) {
|
||||||
|
unmountRef.current();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, [onSave, onCancel, onEdit]);
|
}, []);
|
||||||
|
|
||||||
function handleEditorChange(text: string, ev: MonacoTypes.editor.IModelContentChangedEvent) {
|
function handleEditorChange(text: string, ev: MonacoTypes.editor.IModelContentChangedEvent) {
|
||||||
if (onChange) {
|
if (onChange) {
|
||||||
@ -127,24 +85,9 @@ export function CodeEditor({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleEditorOnMount(editor: MonacoTypes.editor.IStandaloneCodeEditor, monaco: Monaco) {
|
function handleEditorOnMount(editor: MonacoTypes.editor.IStandaloneCodeEditor, monaco: Monaco) {
|
||||||
// bind Cmd:e
|
if (onMount) {
|
||||||
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyE, () => {
|
unmountRef.current = onMount(editor, monaco);
|
||||||
if (onEdit) {
|
}
|
||||||
onEdit();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// bind Cmd:s
|
|
||||||
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => {
|
|
||||||
if (onSave) {
|
|
||||||
onSave();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// bind Cmd:r
|
|
||||||
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyR, () => {
|
|
||||||
if (onCancel) {
|
|
||||||
onCancel();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const editorOpts = defaultEditorOptions();
|
const editorOpts = defaultEditorOptions();
|
||||||
|
@ -37,6 +37,7 @@ interface State {
|
|||||||
|
|
||||||
const columnHelper = createColumnHelper<any>();
|
const columnHelper = createColumnHelper<any>();
|
||||||
|
|
||||||
|
// TODO remove parentRef dependency -- use own height
|
||||||
const CSVView = ({ parentRef, filename, content }: CSVViewProps) => {
|
const CSVView = ({ parentRef, filename, content }: CSVViewProps) => {
|
||||||
const csvCacheRef = useRef(new Map<string, string>());
|
const csvCacheRef = useRef(new Map<string, string>());
|
||||||
const rowRef = useRef<(HTMLTableRowElement | null)[]>([]);
|
const rowRef = useRef<(HTMLTableRowElement | null)[]>([]);
|
||||||
|
@ -530,16 +530,15 @@ const MemoizedTableBody = React.memo(
|
|||||||
) as typeof TableBody;
|
) as typeof TableBody;
|
||||||
|
|
||||||
interface DirectoryPreviewProps {
|
interface DirectoryPreviewProps {
|
||||||
fileNameAtom: jotai.Atom<string>;
|
|
||||||
model: PreviewModel;
|
model: PreviewModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DirectoryPreview({ fileNameAtom, model }: DirectoryPreviewProps) {
|
function DirectoryPreview({ model }: DirectoryPreviewProps) {
|
||||||
const [searchText, setSearchText] = useState("");
|
const [searchText, setSearchText] = useState("");
|
||||||
const [focusIndex, setFocusIndex] = useState(0);
|
const [focusIndex, setFocusIndex] = useState(0);
|
||||||
const [unfilteredData, setUnfilteredData] = useState<FileInfo[]>([]);
|
const [unfilteredData, setUnfilteredData] = useState<FileInfo[]>([]);
|
||||||
const [filteredData, setFilteredData] = useState<FileInfo[]>([]);
|
const [filteredData, setFilteredData] = useState<FileInfo[]>([]);
|
||||||
const fileName = jotai.useAtomValue(fileNameAtom);
|
const fileName = jotai.useAtomValue(model.metaFilePath);
|
||||||
const showHiddenFiles = jotai.useAtomValue(model.showHiddenFiles);
|
const showHiddenFiles = jotai.useAtomValue(model.showHiddenFiles);
|
||||||
const [selectedPath, setSelectedPath] = useState("");
|
const [selectedPath, setSelectedPath] = useState("");
|
||||||
const [refreshVersion, setRefreshVersion] = jotai.useAtom(model.refreshVersion);
|
const [refreshVersion, setRefreshVersion] = jotai.useAtom(model.refreshVersion);
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
import { WshServer } from "@/app/store/wshserver";
|
import { WshServer } from "@/app/store/wshserver";
|
||||||
import { VDomView } from "@/app/view/term/vdom";
|
import { VDomView } from "@/app/view/term/vdom";
|
||||||
import { WOS, atoms, getEventORefSubject, globalStore, useBlockAtom, useSettingsPrefixAtom } from "@/store/global";
|
import { WOS, atoms, getEventORefSubject, globalStore, useSettingsPrefixAtom } 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 * as util from "@/util/util";
|
import * as util from "@/util/util";
|
||||||
@ -205,9 +205,6 @@ const TerminalView = ({ blockId, model }: TerminalViewProps) => {
|
|||||||
const termRef = React.useRef<TermWrap>(null);
|
const termRef = React.useRef<TermWrap>(null);
|
||||||
model.termRef = termRef;
|
model.termRef = termRef;
|
||||||
const shellProcStatusRef = React.useRef<string>(null);
|
const shellProcStatusRef = React.useRef<string>(null);
|
||||||
const blockIconOverrideAtom = useBlockAtom<string>(blockId, "blockicon:override", () => {
|
|
||||||
return jotai.atom<string>(null);
|
|
||||||
}) as jotai.PrimitiveAtom<string>;
|
|
||||||
const htmlElemFocusRef = React.useRef<HTMLInputElement>(null);
|
const htmlElemFocusRef = React.useRef<HTMLInputElement>(null);
|
||||||
model.htmlElemFocusRef = htmlElemFocusRef;
|
model.htmlElemFocusRef = htmlElemFocusRef;
|
||||||
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
|
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
|
||||||
@ -310,10 +307,8 @@ const TerminalView = ({ blockId, model }: TerminalViewProps) => {
|
|||||||
shellProcStatusRef.current = status;
|
shellProcStatusRef.current = status;
|
||||||
if (status == "running") {
|
if (status == "running") {
|
||||||
termRef.current?.setIsRunning(true);
|
termRef.current?.setIsRunning(true);
|
||||||
globalStore.set(blockIconOverrideAtom, "terminal");
|
|
||||||
} else {
|
} else {
|
||||||
termRef.current?.setIsRunning(false);
|
termRef.current?.setIsRunning(false);
|
||||||
globalStore.set(blockIconOverrideAtom, "regular@terminal");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const initialRTStatus = services.BlockService.GetControllerStatus(blockId);
|
const initialRTStatus = services.BlockService.GetControllerStatus(blockId);
|
||||||
|
2
frontend/types/gotypes.d.ts
vendored
2
frontend/types/gotypes.d.ts
vendored
@ -175,6 +175,7 @@ declare global {
|
|||||||
// wshrpc.FileInfo
|
// wshrpc.FileInfo
|
||||||
type FileInfo = {
|
type FileInfo = {
|
||||||
path: string;
|
path: string;
|
||||||
|
dir: string;
|
||||||
name: string;
|
name: string;
|
||||||
notfound?: boolean;
|
notfound?: boolean;
|
||||||
size: number;
|
size: number;
|
||||||
@ -183,6 +184,7 @@ declare global {
|
|||||||
modtime: number;
|
modtime: number;
|
||||||
isdir?: boolean;
|
isdir?: boolean;
|
||||||
mimetype?: string;
|
mimetype?: string;
|
||||||
|
readonly?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
// filestore.FileOptsType
|
// filestore.FileOptsType
|
||||||
|
@ -14,6 +14,10 @@ function setKeyUtilPlatform(platform: NodeJS.Platform) {
|
|||||||
PLATFORM = platform;
|
PLATFORM = platform;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getKeyUtilPlatform(): NodeJS.Platform {
|
||||||
|
return PLATFORM;
|
||||||
|
}
|
||||||
|
|
||||||
function keydownWrapper(
|
function keydownWrapper(
|
||||||
fn: (waveEvent: WaveKeyboardEvent) => boolean
|
fn: (waveEvent: WaveKeyboardEvent) => boolean
|
||||||
): (event: KeyboardEvent | React.KeyboardEvent) => void {
|
): (event: KeyboardEvent | React.KeyboardEvent) => void {
|
||||||
@ -85,6 +89,53 @@ function isCharacterKeyEvent(event: WaveKeyboardEvent): boolean {
|
|||||||
return util.countGraphemes(event.key) == 1;
|
return util.countGraphemes(event.key) == 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const inputKeyMap = new Map<string, boolean>([
|
||||||
|
["Backspace", true],
|
||||||
|
["Delete", true],
|
||||||
|
["Enter", true],
|
||||||
|
["Space", true],
|
||||||
|
["Tab", true],
|
||||||
|
["ArrowLeft", true],
|
||||||
|
["ArrowRight", true],
|
||||||
|
["ArrowUp", true],
|
||||||
|
["ArrowDown", true],
|
||||||
|
["Home", true],
|
||||||
|
["End", true],
|
||||||
|
["PageUp", true],
|
||||||
|
["PageDown", true],
|
||||||
|
["Cmd:a", true],
|
||||||
|
["Cmd:c", true],
|
||||||
|
["Cmd:v", true],
|
||||||
|
["Cmd:x", true],
|
||||||
|
["Cmd:z", true],
|
||||||
|
["Cmd:Shift:z", true],
|
||||||
|
["Cmd:ArrowLeft", true],
|
||||||
|
["Cmd:ArrowRight", true],
|
||||||
|
["Cmd:Backspace", true],
|
||||||
|
["Cmd:Delete", true],
|
||||||
|
["Shift:ArrowLeft", true],
|
||||||
|
["Shift:ArrowRight", true],
|
||||||
|
["Shift:ArrowUp", true],
|
||||||
|
["Shift:ArrowDown", true],
|
||||||
|
["Shift:Home", true],
|
||||||
|
["Shift:End", true],
|
||||||
|
["Cmd:Shift:ArrowLeft", true],
|
||||||
|
["Cmd:Shift:ArrowRight", true],
|
||||||
|
["Cmd:Shift:ArrowUp", true],
|
||||||
|
["Cmd:Shift:ArrowDown", true],
|
||||||
|
]);
|
||||||
|
|
||||||
|
function isInputEvent(event: WaveKeyboardEvent): boolean {
|
||||||
|
if (isCharacterKeyEvent(event)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
for (let key of inputKeyMap.keys()) {
|
||||||
|
if (checkKeyPressed(event, key)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function checkKeyPressed(event: WaveKeyboardEvent, keyDescription: string): boolean {
|
function checkKeyPressed(event: WaveKeyboardEvent, keyDescription: string): boolean {
|
||||||
let keyPress = parseKeyDescription(keyDescription);
|
let keyPress = parseKeyDescription(keyDescription);
|
||||||
if (!keyPress.mods.Alt && notMod(keyPress.mods.Option, event.option)) {
|
if (!keyPress.mods.Alt && notMod(keyPress.mods.Option, event.option)) {
|
||||||
@ -174,7 +225,9 @@ export {
|
|||||||
adaptFromElectronKeyEvent,
|
adaptFromElectronKeyEvent,
|
||||||
adaptFromReactOrNativeKeyEvent,
|
adaptFromReactOrNativeKeyEvent,
|
||||||
checkKeyPressed,
|
checkKeyPressed,
|
||||||
|
getKeyUtilPlatform,
|
||||||
isCharacterKeyEvent,
|
isCharacterKeyEvent,
|
||||||
|
isInputEvent,
|
||||||
keydownWrapper,
|
keydownWrapper,
|
||||||
parseKeyDescription,
|
parseKeyDescription,
|
||||||
setKeyUtilPlatform,
|
setKeyUtilPlatform,
|
||||||
|
@ -7,6 +7,8 @@ import {
|
|||||||
registerGlobalKeys,
|
registerGlobalKeys,
|
||||||
} from "@/app/store/keymodel";
|
} from "@/app/store/keymodel";
|
||||||
import { WshServer } from "@/app/store/wshserver";
|
import { WshServer } from "@/app/store/wshserver";
|
||||||
|
import { loadMonaco } from "@/app/view/codeeditor/codeeditor";
|
||||||
|
import { getLayoutModelForActiveTab } from "@/layout/index";
|
||||||
import {
|
import {
|
||||||
atoms,
|
atoms,
|
||||||
countersClear,
|
countersClear,
|
||||||
@ -48,6 +50,7 @@ loadFonts();
|
|||||||
(window as any).isFullScreen = false;
|
(window as any).isFullScreen = false;
|
||||||
(window as any).countersPrint = countersPrint;
|
(window as any).countersPrint = countersPrint;
|
||||||
(window as any).countersClear = countersClear;
|
(window as any).countersClear = countersClear;
|
||||||
|
(window as any).getLayoutModelForActiveTab = getLayoutModelForActiveTab;
|
||||||
|
|
||||||
document.title = `The Next Wave (${windowId.substring(0, 8)})`;
|
document.title = `The Next Wave (${windowId.substring(0, 8)})`;
|
||||||
|
|
||||||
@ -65,6 +68,7 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
registerGlobalKeys();
|
registerGlobalKeys();
|
||||||
registerElectronReinjectKeyHandler();
|
registerElectronReinjectKeyHandler();
|
||||||
registerControlShiftStateUpdateHandler();
|
registerControlShiftStateUpdateHandler();
|
||||||
|
setTimeout(loadMonaco, 30);
|
||||||
const fullConfig = await services.FileService.GetFullConfig();
|
const fullConfig = await services.FileService.GetFullConfig();
|
||||||
console.log("fullconfig", fullConfig);
|
console.log("fullconfig", fullConfig);
|
||||||
globalStore.set(atoms.fullConfigAtom, fullConfig);
|
globalStore.set(atoms.fullConfigAtom, fullConfig);
|
||||||
|
@ -13,6 +13,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"io/fs"
|
||||||
"math"
|
"math"
|
||||||
mathrand "math/rand"
|
mathrand "math/rand"
|
||||||
"mime"
|
"mime"
|
||||||
@ -627,7 +628,8 @@ func CopyToChannel(outputCh chan<- []byte, reader io.Reader) error {
|
|||||||
|
|
||||||
// on error just returns ""
|
// on error just returns ""
|
||||||
// does not return "application/octet-stream" as this is considered a detection failure
|
// does not return "application/octet-stream" as this is considered a detection failure
|
||||||
func DetectMimeType(path string) string {
|
// can pass an existing fileInfo to avoid re-statting the file
|
||||||
|
func DetectMimeType(path string, fileInfo fs.FileInfo) string {
|
||||||
ext := filepath.Ext(path)
|
ext := filepath.Ext(path)
|
||||||
if mimeType, ok := StaticMimeTypeMap[ext]; ok {
|
if mimeType, ok := StaticMimeTypeMap[ext]; ok {
|
||||||
return mimeType
|
return mimeType
|
||||||
@ -635,21 +637,24 @@ func DetectMimeType(path string) string {
|
|||||||
if mimeType := mime.TypeByExtension(ext); mimeType != "" {
|
if mimeType := mime.TypeByExtension(ext); mimeType != "" {
|
||||||
return mimeType
|
return mimeType
|
||||||
}
|
}
|
||||||
stats, err := os.Stat(path)
|
if fileInfo == nil {
|
||||||
if err != nil {
|
statRtn, err := os.Stat(path)
|
||||||
return ""
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
fileInfo = statRtn
|
||||||
}
|
}
|
||||||
if stats.IsDir() {
|
if fileInfo.IsDir() {
|
||||||
return "directory"
|
return "directory"
|
||||||
}
|
}
|
||||||
if stats.Mode()&os.ModeNamedPipe == os.ModeNamedPipe {
|
if fileInfo.Mode()&os.ModeNamedPipe == os.ModeNamedPipe {
|
||||||
return "pipe"
|
return "pipe"
|
||||||
}
|
}
|
||||||
charDevice := os.ModeDevice | os.ModeCharDevice
|
charDevice := os.ModeDevice | os.ModeCharDevice
|
||||||
if stats.Mode()&charDevice == charDevice {
|
if fileInfo.Mode()&charDevice == charDevice {
|
||||||
return "character-special"
|
return "character-special"
|
||||||
}
|
}
|
||||||
if stats.Mode()&os.ModeDevice == os.ModeDevice {
|
if fileInfo.Mode()&os.ModeDevice == os.ModeDevice {
|
||||||
return "block-special"
|
return "block-special"
|
||||||
}
|
}
|
||||||
fd, err := os.Open(path)
|
fd, err := os.Open(path)
|
||||||
|
@ -9,9 +9,11 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"io/fs"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/wavetermdev/thenextwave/pkg/util/utilfn"
|
"github.com/wavetermdev/thenextwave/pkg/util/utilfn"
|
||||||
"github.com/wavetermdev/thenextwave/pkg/wavebase"
|
"github.com/wavetermdev/thenextwave/pkg/wavebase"
|
||||||
@ -88,7 +90,7 @@ func (impl *ServerImpl) remoteStreamFileDir(ctx context.Context, path string, by
|
|||||||
}
|
}
|
||||||
var fileInfoArr []*wshrpc.FileInfo
|
var fileInfoArr []*wshrpc.FileInfo
|
||||||
parent := filepath.Dir(path)
|
parent := filepath.Dir(path)
|
||||||
parentFileInfo, err := impl.RemoteFileInfoCommand(ctx, parent)
|
parentFileInfo, err := impl.fileInfoInternal(parent, false)
|
||||||
if err == nil && parent != path {
|
if err == nil && parent != path {
|
||||||
parentFileInfo.Name = ".."
|
parentFileInfo.Name = ".."
|
||||||
parentFileInfo.Size = -1
|
parentFileInfo.Size = -1
|
||||||
@ -102,24 +104,8 @@ func (impl *ServerImpl) remoteStreamFileDir(ctx context.Context, path string, by
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
mimeType := utilfn.DetectMimeType(filepath.Join(path, innerFileInfoInt.Name()))
|
innerFileInfo := statToFileInfo(filepath.Join(path, innerFileInfoInt.Name()), innerFileInfoInt)
|
||||||
var fileSize int64
|
fileInfoArr = append(fileInfoArr, innerFileInfo)
|
||||||
if mimeType == "directory" {
|
|
||||||
fileSize = -1
|
|
||||||
} else {
|
|
||||||
fileSize = innerFileInfoInt.Size()
|
|
||||||
}
|
|
||||||
innerFileInfo := wshrpc.FileInfo{
|
|
||||||
Path: filepath.Join(path, innerFileInfoInt.Name()),
|
|
||||||
Name: innerFileInfoInt.Name(),
|
|
||||||
Size: fileSize,
|
|
||||||
Mode: innerFileInfoInt.Mode(),
|
|
||||||
ModeStr: innerFileInfoInt.Mode().String(),
|
|
||||||
ModTime: innerFileInfoInt.ModTime().UnixMilli(),
|
|
||||||
IsDir: innerFileInfoInt.IsDir(),
|
|
||||||
MimeType: mimeType,
|
|
||||||
}
|
|
||||||
fileInfoArr = append(fileInfoArr, &innerFileInfo)
|
|
||||||
if len(fileInfoArr) >= DirChunkSize {
|
if len(fileInfoArr) >= DirChunkSize {
|
||||||
dataCallback(fileInfoArr, nil)
|
dataCallback(fileInfoArr, nil)
|
||||||
fileInfoArr = nil
|
fileInfoArr = nil
|
||||||
@ -179,7 +165,7 @@ func (impl *ServerImpl) remoteStreamFileInternal(ctx context.Context, data wshrp
|
|||||||
}
|
}
|
||||||
path := data.Path
|
path := data.Path
|
||||||
path = wavebase.ExpandHomeDir(path)
|
path = wavebase.ExpandHomeDir(path)
|
||||||
finfo, err := impl.RemoteFileInfoCommand(ctx, path)
|
finfo, err := impl.fileInfoInternal(path, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot stat file %q: %w", path, err)
|
return fmt.Errorf("cannot stat file %q: %w", path, err)
|
||||||
}
|
}
|
||||||
@ -214,18 +200,11 @@ func (impl *ServerImpl) RemoteStreamFileCommand(ctx context.Context, data wshrpc
|
|||||||
return ch
|
return ch
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*ServerImpl) RemoteFileInfoCommand(ctx context.Context, path string) (*wshrpc.FileInfo, error) {
|
func statToFileInfo(fullPath string, finfo fs.FileInfo) *wshrpc.FileInfo {
|
||||||
cleanedPath := filepath.Clean(wavebase.ExpandHomeDir(path))
|
mimeType := utilfn.DetectMimeType(fullPath, finfo)
|
||||||
finfo, err := os.Stat(cleanedPath)
|
rtn := &wshrpc.FileInfo{
|
||||||
if os.IsNotExist(err) {
|
Path: wavebase.ReplaceHomeDir(fullPath),
|
||||||
return &wshrpc.FileInfo{Path: wavebase.ReplaceHomeDir(path), NotFound: true}, nil
|
Dir: computeDirPart(fullPath, finfo.IsDir()),
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot stat file %q: %w", path, err)
|
|
||||||
}
|
|
||||||
mimeType := utilfn.DetectMimeType(cleanedPath)
|
|
||||||
return &wshrpc.FileInfo{
|
|
||||||
Path: cleanedPath,
|
|
||||||
Name: finfo.Name(),
|
Name: finfo.Name(),
|
||||||
Size: finfo.Size(),
|
Size: finfo.Size(),
|
||||||
Mode: finfo.Mode(),
|
Mode: finfo.Mode(),
|
||||||
@ -233,7 +212,75 @@ func (*ServerImpl) RemoteFileInfoCommand(ctx context.Context, path string) (*wsh
|
|||||||
ModTime: finfo.ModTime().UnixMilli(),
|
ModTime: finfo.ModTime().UnixMilli(),
|
||||||
IsDir: finfo.IsDir(),
|
IsDir: finfo.IsDir(),
|
||||||
MimeType: mimeType,
|
MimeType: mimeType,
|
||||||
}, nil
|
}
|
||||||
|
if finfo.IsDir() {
|
||||||
|
rtn.Size = -1
|
||||||
|
}
|
||||||
|
return rtn
|
||||||
|
}
|
||||||
|
|
||||||
|
// fileInfo might be null
|
||||||
|
func checkIsReadOnly(path string, fileInfo fs.FileInfo, exists bool) bool {
|
||||||
|
if !exists || fileInfo.Mode().IsDir() {
|
||||||
|
dirName := filepath.Dir(path)
|
||||||
|
randHexStr, err := utilfn.RandomHexString(12)
|
||||||
|
if err != nil {
|
||||||
|
// we're not sure, just return false
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
tmpFileName := filepath.Join(dirName, "wsh-tmp-"+randHexStr)
|
||||||
|
_, err = os.Create(tmpFileName)
|
||||||
|
if err != nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
os.Remove(tmpFileName)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// try to open for writing, if this fails then it is read-only
|
||||||
|
file, err := os.OpenFile(path, os.O_WRONLY|os.O_APPEND, 0666)
|
||||||
|
if err != nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
file.Close()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func computeDirPart(path string, isDir bool) string {
|
||||||
|
path = filepath.Clean(wavebase.ExpandHomeDir(path))
|
||||||
|
path = filepath.ToSlash(path)
|
||||||
|
if path == "/" {
|
||||||
|
return "/"
|
||||||
|
}
|
||||||
|
path = strings.TrimSuffix(path, "/")
|
||||||
|
if isDir {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
return filepath.Dir(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*ServerImpl) fileInfoInternal(path string, extended bool) (*wshrpc.FileInfo, error) {
|
||||||
|
cleanedPath := filepath.Clean(wavebase.ExpandHomeDir(path))
|
||||||
|
finfo, err := os.Stat(cleanedPath)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return &wshrpc.FileInfo{
|
||||||
|
Path: wavebase.ReplaceHomeDir(path),
|
||||||
|
Dir: computeDirPart(path, false),
|
||||||
|
NotFound: true,
|
||||||
|
ReadOnly: checkIsReadOnly(cleanedPath, finfo, false),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot stat file %q: %w", path, err)
|
||||||
|
}
|
||||||
|
rtn := statToFileInfo(cleanedPath, finfo)
|
||||||
|
if extended {
|
||||||
|
rtn.ReadOnly = checkIsReadOnly(cleanedPath, finfo, true)
|
||||||
|
}
|
||||||
|
return rtn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (impl *ServerImpl) RemoteFileInfoCommand(ctx context.Context, path string) (*wshrpc.FileInfo, error) {
|
||||||
|
return impl.fileInfoInternal(path, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*ServerImpl) RemoteWriteFileCommand(ctx context.Context, data wshrpc.CommandRemoteWriteFileData) error {
|
func (*ServerImpl) RemoteWriteFileCommand(ctx context.Context, data wshrpc.CommandRemoteWriteFileData) error {
|
||||||
|
@ -302,7 +302,8 @@ type CpuDataType struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type FileInfo struct {
|
type FileInfo struct {
|
||||||
Path string `json:"path"` // cleaned path
|
Path string `json:"path"` // cleaned path (may have "~")
|
||||||
|
Dir string `json:"dir"` // returns the directory part of the path (if this is a a directory, it will be equal to Path). "~" will be expanded, and separators will be normalized to "/"
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
NotFound bool `json:"notfound,omitempty"`
|
NotFound bool `json:"notfound,omitempty"`
|
||||||
Size int64 `json:"size"`
|
Size int64 `json:"size"`
|
||||||
@ -311,6 +312,7 @@ type FileInfo struct {
|
|||||||
ModTime int64 `json:"modtime"`
|
ModTime int64 `json:"modtime"`
|
||||||
IsDir bool `json:"isdir,omitempty"`
|
IsDir bool `json:"isdir,omitempty"`
|
||||||
MimeType string `json:"mimetype,omitempty"`
|
MimeType string `json:"mimetype,omitempty"`
|
||||||
|
ReadOnly bool `json:"readonly,omitempty"` // this is not set for fileinfo's returned from directory listings
|
||||||
}
|
}
|
||||||
|
|
||||||
type CommandRemoteStreamFileData struct {
|
type CommandRemoteStreamFileData struct {
|
||||||
|
Loading…
Reference in New Issue
Block a user