diff --git a/package.json b/package.json index 82a6608..2aaff4b 100644 --- a/package.json +++ b/package.json @@ -76,5 +76,5 @@ "typescript": "^5.4.5", "vite": "^5.0.12" }, - "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" + "packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610" } diff --git a/src/main/framework/runtime.ts b/src/main/framework/runtime.ts index 214fa81..f241426 100644 --- a/src/main/framework/runtime.ts +++ b/src/main/framework/runtime.ts @@ -49,7 +49,11 @@ export class Prompt { set value(value: string) { this._value = value - this.parts = value.split(' ') + this.parts = value + .split('\n') + .map((line) => line.trim()) + .join(' ') + .split(' ') const [cmd, ...args] = this.parts diff --git a/src/renderer/src/assets/runner.css b/src/renderer/src/assets/runner.css index a2bf969..2a6ce9f 100644 --- a/src/renderer/src/assets/runner.css +++ b/src/renderer/src/assets/runner.css @@ -27,26 +27,67 @@ body { width: 100%; height: 100%; display: grid; - grid-template-columns: auto; + /* grid-template-columns: 100px; */ grid-template-rows: auto; justify-items: center; align-items: center; } .runner-input { - width: 100% + height: 100%; + padding: 10px; + display: flex; + flex-wrap: wrap; + max-height: 100%; + align-content: stretch; + justify-content: space-around; + flex-direction: column; +} + +.multiline-input-container { + display:flex; + align-items:flex-end; + height: 50%; +} + +.runner-input-field, +.runner-textarea-field { + font-size: 20px; + background: transparent; + border: 0; + font-family: "Roboto Mono", monospace; + flex-grow: 1; } .runner-input-field { height: 50px; text-align: center; width: 100%; - font-size: 20px; - background: transparent; - border: 0; + overflow-x: auto; + white-space: nowrap; font-family: "Roboto Mono", monospace } +.runner-textarea-field { + max-width:100%; + text-align: left; + padding-left: 20px; + flex: 1 1 auto; + overflow: auto; + resize: none; +} + +.runner-textarea-field:focus { + border: 0; + font-family: "Roboto Mono", monospace; + outline: none; + color: white; +} + +.multi-line { + text-align: left !important; +} + .runner-input-field { color: #cac7c7 } @@ -275,13 +316,13 @@ body { grid-template-columns: 15px auto; } -.runner-result::-webkit-scrollbar { +.multi-line::-webkit-scrollbar, .runner-result::-webkit-scrollbar { width: 15px; } -.runner-result::-webkit-scrollbar-corner { +.multi-line::-webkit-scrollbar-corner, .runner-result::-webkit-scrollbar-corner { background: rgba(0,0,0,0); } -.runner-result::-webkit-scrollbar-thumb { +.multi-line::-webkit-scrollbar-thumb, .runner-result::-webkit-scrollbar-thumb { background-color: #294794; border-radius: 6px; border: 4px solid rgba(0,0,0,0); diff --git a/src/renderer/src/runner/runner.tsx b/src/renderer/src/runner/runner.tsx index c9f6b7d..49dc744 100644 --- a/src/renderer/src/runner/runner.tsx +++ b/src/renderer/src/runner/runner.tsx @@ -20,8 +20,12 @@ export default function Runner(): ReactElement { const [suggestionSelection, setSuggestionSelection] = useState(0) const inputRef = useRef(null) + const textAreaRef = useRef(null) const historyRefs = useRef>>([]) + const [isMultiLine, setIsMultiLine] = useState(false) + const [multiLineArgs, setMultiLineArgs] = useState('') + const reloadRuntimesFromBackend = async (): Promise => { const isCommanderMode = await window.electron.ipcRenderer.invoke('runner.isCommanderMode') const runtimesFetch: Runtime[] = await window.electron.ipcRenderer.invoke('runtimes') @@ -57,7 +61,7 @@ export default function Runner(): ReactElement { const command: Command = await window.electron.ipcRenderer.invoke( 'runtime.prepareExecute', runtime.id, - historicalExecution ? historicalExecution.prompt : runtime.prompt, + runtime.prompt, 'default' ) @@ -66,6 +70,9 @@ export default function Runner(): ReactElement { list: [] }) + setMultiLineArgs('') + setIsMultiLine(false) + await reloadRuntimesFromBackend() // renderer -> "backend" @@ -97,7 +104,9 @@ export default function Runner(): ReactElement { setSuggestionSelection(0) }) } - const handlePromptChange = (event: ChangeEvent): void => { + const handlePromptChange = ( + event: ChangeEvent | ChangeEvent + ): void => { const value = event.target.value if (historyIndex !== -1) { applyHistoryIndex(-1) @@ -152,25 +161,79 @@ export default function Runner(): ReactElement { applyHistoryIndex(historyIndex) } + const handleLineBreak = (e): void => { + const cursorPosition = e.target?.selectionStart + if (e.target === inputRef.current) { + if (!isMultiLine) setIsMultiLine(true) + e.preventDefault() + setMultiLineArgs( + runtime?.prompt.slice(cursorPosition, runtime.prompt.length) + + (multiLineArgs ? '\n' + multiLineArgs : '') + ) + if (runtime) setPrompt(runtime.prompt.slice(0, cursorPosition)) + textAreaRef.current?.focus() + } + } + const handleKeyDown = (e): void => { - if (e.key === 'Enter') { - execute(runtime).catch((error) => console.error(error)) + if (e.key === 'Enter' && e.shiftKey) { + handleLineBreak(e) + } + if ((e.code === 'Backslash' || e.code === 'Backquote') && !e.shiftKey) { + e.preventDefault() + setIsMultiLine(!isMultiLine) + } + if (e.code === 'Backspace' && isMultiLine) { + if (e.target === textAreaRef.current && e.target.selectionStart === 0) { + e.preventDefault() + const multiLineSplit = multiLineArgs.trim().split('\n') + const _firstLine = runtime?.prompt.trim() + setPrompt(_firstLine + (_firstLine ? ' ' : '') + multiLineSplit.shift()) + setMultiLineArgs(multiLineSplit.join('\n')) + + if (multiLineSplit.length < 1) { + setIsMultiLine(false) + } + + inputRef.current?.focus() + } + } + if (e.key === 'Enter' && !e.shiftKey) { + if (!runtime) return + execute({ + ...runtime, + prompt: + runtime.prompt.trim() + + (isMultiLine && multiLineArgs ? '\n' + multiLineArgs : multiLineArgs) + }).catch((error) => console.error(error)) } if (e.code === 'ArrowDown') { - if (runtime && historyIndex > -1) { - applyHistoryIndex(historyIndex - 1) - } else if (historyIndex === -1) { + if (historyIndex === -1) { if (suggestionSelection < suggestion.list.length - 1) { setSuggestionSelection(suggestionSelection + 1) + } else if (isMultiLine && !e.ctrlKey && e.target === inputRef.current) { + const linebreakIndex = multiLineArgs.indexOf('\n') + const cursorPosition = e.target?.selectionStart + window.electron.ipcRenderer.invoke('runtime.complete', runtime?.prompt, 0).then(() => { + textAreaRef.current?.focus() + cursorPosition < linebreakIndex || linebreakIndex === -1 + ? textAreaRef.current?.setSelectionRange(cursorPosition, cursorPosition) + : textAreaRef.current?.setSelectionRange(linebreakIndex, linebreakIndex) + }) + } + } else if (runtime && historyIndex > -1) { + if (historyIndex === 0) { + setPrompt('') + setIsMultiLine(false) + setMultiLineArgs('') } + applyHistoryIndex(historyIndex - 1) } } if (e.code === 'ArrowLeft' || e.code === 'ArrowRight') { let cursor = inputRef?.current?.selectionEnd ?? -1 - console.log(cursor, runtime?.prompt.length) - // the selectionEnd hasn't updated yet. add and go if (e.code === 'ArrowLeft') { cursor-- @@ -202,9 +265,25 @@ export default function Runner(): ReactElement { } } + if (e.code === 'Escape') { + //exit suggestions + setSuggestion({ + list: [] + }) + } + if (e.code === 'ArrowUp') { if (suggestionSelection > 0) { setSuggestionSelection(suggestionSelection - 1) + } else if (isMultiLine && !e.ctrlKey) { + const linebreakIndex = multiLineArgs.indexOf('\n') + const cursorPosition = e.target?.selectionStart + if (linebreakIndex < 0 || cursorPosition <= linebreakIndex) { + window.electron.ipcRenderer.invoke('runtime.complete', runtime?.prompt, 0).then(() => { + inputRef.current?.focus() + inputRef.current?.setSelectionRange(cursorPosition, cursorPosition) + }) + } } else if (runtime && historyIndex < runtime.history.length - 1) { applyHistoryIndex(historyIndex + 1) setSuggestion({ @@ -349,6 +428,37 @@ export default function Runner(): ReactElement { return () => {} }, [runtimeList]) + useEffect(() => { + if (isMultiLine) { + textAreaRef.current?.focus() + return + } + + if (runtime) setPrompt(runtime.prompt.trim() + (multiLineArgs ? ' ' : '') + multiLineArgs) + setMultiLineArgs('') + inputRef.current?.focus() + }, [isMultiLine]) + + useEffect(() => { + if (!historicalExecution || !historicalExecution.prompt) { + return + } + + const splitPoint = historicalExecution?.prompt.indexOf('\n') + if (!splitPoint || splitPoint === -1) { + setIsMultiLine(false) + setMultiLineArgs('') + setPrompt(historicalExecution.prompt) + return + } + + setIsMultiLine(true) + setPrompt(historicalExecution.prompt.substring(0, splitPoint)) + setMultiLineArgs( + historicalExecution.prompt.substring(splitPoint + 1, historicalExecution.prompt.length) + ) + }, [historicalExecution]) + if (!runtime) { return

Loading

} @@ -464,15 +574,29 @@ export default function Runner(): ReactElement {
- +
+ +
+ + {isMultiLine ? ( +