Break out non-autocomplete changes from autocomplete PR (#618)

This improves the ephemeral command runner to allow for honoring of
timeouts and proper handling of overriding the current working
directory. It also fixes some partially transparent font colors in light
mode, making them solid instead. It also updates the InputModel to be
auto-observable and utilize some getters to ensure the cmdinput is
getting updated whenever necessary state changes take place.
This commit is contained in:
Evan Simkowitz 2024-04-29 18:29:27 -07:00 committed by GitHub
parent ac91fa8596
commit c6a8797ddd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 362 additions and 324 deletions

View File

@ -9,8 +9,8 @@
--app-accent-color: rgb(75, 166, 57);
--app-accent-bg-color: rgba(75, 166, 57, 0.2);
--app-text-color: rgb(0, 0, 0);
--app-text-primary-color: rgb(0, 0, 0, 0.9);
--app-text-secondary-color: rgb(0, 0, 0, 0.7);
--app-text-primary-color: rgb(23, 23, 23);
--app-text-secondary-color: rgb(76, 76, 76);
--app-border-color: rgb(139 145 138);
--app-panel-bg-color: rgb(224, 224, 224);
--app-panel-bg-color-dev: rgb(224, 224, 224);

View File

@ -88,7 +88,7 @@ class AIChat extends React.Component<{}, {}> {
}
submitChatMessage(messageStr: string) {
const curLine = GlobalModel.inputModel.getCurLine();
const curLine = GlobalModel.inputModel.curLine;
const prtn = GlobalModel.submitChatInfoCommand(messageStr, curLine, false);
prtn.then((rtn) => {
if (!rtn.success) {
@ -103,15 +103,19 @@ class AIChat extends React.Component<{}, {}> {
return { numLines, linePos };
}
@mobx.action.bound
onTextAreaFocused(e: any) {
GlobalModel.inputModel.setAuxViewFocus(true);
this.onTextAreaChange(e);
}
@mobx.action.bound
onTextAreaBlur(e: any) {
GlobalModel.inputModel.setAuxViewFocus(false);
}
// Adjust the height of the textarea to fit the text
@boundMethod
onTextAreaChange(e: any) {
// Calculate the bounding height of the text area
const textAreaMaxLines = 4;
@ -140,8 +144,10 @@ class AIChat extends React.Component<{}, {}> {
this.submitChatMessage(messageStr);
currentRef.value = "";
} else {
mobx.action(() => {
inputModel.grabCodeSelectSelection();
inputModel.setAuxViewFocus(false);
})();
}
}
@ -182,7 +188,6 @@ class AIChat extends React.Component<{}, {}> {
return true;
}
@mobx.action
@boundMethod
onKeyDown(e: any) {}
@ -254,9 +259,9 @@ class AIChat extends React.Component<{}, {}> {
autoComplete="off"
autoCorrect="off"
id="chat-cmd-input"
onFocus={this.onTextAreaFocused.bind(this)}
onBlur={this.onTextAreaBlur.bind(this)}
onChange={this.onTextAreaChange.bind(this)}
onFocus={this.onTextAreaFocused}
onBlur={this.onTextAreaBlur}
onChange={this.onTextAreaChange}
onKeyDown={this.onKeyDown}
style={{ fontSize: this.termFontSize }}
className="chat-textarea"

View File

@ -193,8 +193,8 @@
}
// This aligns the icons with the prompt field.
// We don't need right padding because the whole input field is already padded.
padding: 2px 0 0 12px;
// We don't need right margin because the whole input field is already padded.
margin: 2px 0 0 12px;
cursor: pointer;
}

View File

@ -56,7 +56,7 @@ class CmdInput extends React.Component<{}, {}> {
this.updateCmdInputHeight();
}
@boundMethod
@mobx.action.bound
clickFocusInputHint(): void {
GlobalModel.inputModel.giveFocus();
}
@ -75,7 +75,7 @@ class CmdInput extends React.Component<{}, {}> {
GlobalModel.inputModel.setAuxViewFocus(false);
}
@boundMethod
@mobx.action.bound
clickAIAction(e: any): void {
e.preventDefault();
e.stopPropagation();
@ -87,7 +87,7 @@ class CmdInput extends React.Component<{}, {}> {
}
}
@boundMethod
@mobx.action.bound
clickHistoryAction(e: any): void {
e.preventDefault();
e.stopPropagation();
@ -105,11 +105,9 @@ class CmdInput extends React.Component<{}, {}> {
GlobalCommandRunner.connectRemote(remoteId);
}
@boundMethod
@mobx.action.bound
toggleFilter(screen: Screen) {
mobx.action(() => {
screen.filterRunning.set(!screen.filterRunning.get());
})();
}
@boundMethod

View File

@ -22,6 +22,7 @@
color: var(--app-text-color);
display: flex;
flex-direction: column-reverse;
min-height: 100%;
.history-item {
cursor: pointer;

View File

@ -169,12 +169,12 @@ class HistoryInfo extends React.Component<{}, {}> {
}
}
@boundMethod
@mobx.action.bound
handleClose() {
GlobalModel.inputModel.closeAuxView();
}
@boundMethod
@mobx.action.bound
handleItemClick(hitem: HistoryItem) {
const inputModel = GlobalModel.inputModel;
const selItem = inputModel.getHistorySelectedItem();
@ -195,14 +195,14 @@ class HistoryInfo extends React.Component<{}, {}> {
}, 3000);
}
@boundMethod
@mobx.action.bound
handleClickType() {
const inputModel = GlobalModel.inputModel;
inputModel.setAuxViewFocus(true);
inputModel.toggleHistoryType();
}
@boundMethod
@mobx.action.bound
handleClickRemote() {
const inputModel = GlobalModel.inputModel;
inputModel.setAuxViewFocus(true);
@ -229,7 +229,7 @@ class HistoryInfo extends React.Component<{}, {}> {
render() {
const inputModel = GlobalModel.inputModel;
const selItem = inputModel.getHistorySelectedItem();
const hitems = inputModel.getFilteredHistoryItems();
const hitems = inputModel.filteredHistoryItems;
const opts = inputModel.historyQueryOpts.get();
let hitem: HistoryItem = null;
let snames: Record<string, string> = {};

View File

@ -117,7 +117,7 @@ class CmdInputKeybindings extends React.Component<{ inputObject: TextAreaInput }
const lastTab = this.lastTab;
this.lastTab = true;
this.curPress = "tab";
const curLine = inputModel.getCurLine();
const curLine = inputModel.curLine;
if (lastTab) {
GlobalModel.submitCommand(
"_compgen",
@ -250,9 +250,10 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
lastSP: StrWithPos = { str: "", pos: appconst.NoStrPos };
version: OV<number> = mobx.observable.box(0, { name: "textAreaInput-version" }); // forces render updates
@mobx.action
incVersion(): void {
const v = this.version.get();
mobx.action(() => this.version.set(v + 1))();
this.version.set(v + 1);
}
getCurSP(): StrWithPos {
@ -278,6 +279,7 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
GlobalModel.sendCmdInputText(this.props.screen.screenId, curSP);
}
@mobx.action
setFocus(): void {
GlobalModel.inputModel.giveFocus();
}
@ -311,6 +313,7 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
}
}
@mobx.action
componentDidMount() {
const activeScreen = GlobalModel.getActiveScreen();
if (activeScreen != null) {
@ -324,6 +327,7 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
this.updateSP();
}
@mobx.action
componentDidUpdate() {
const activeScreen = GlobalModel.getActiveScreen();
if (activeScreen != null) {
@ -340,7 +344,7 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
this.mainInputRef.current.selectionStart = fcpos;
this.mainInputRef.current.selectionEnd = fcpos;
}
mobx.action(() => inputModel.forceCursorPos.set(null))();
inputModel.forceCursorPos.set(null);
}
if (inputModel.forceInputFocus) {
inputModel.forceInputFocus = false;
@ -414,21 +418,18 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
return;
}
currentRef.setRangeText("\n", currentRef.selectionStart, currentRef.selectionEnd, "end");
GlobalModel.inputModel.setCurLine(currentRef.value);
GlobalModel.inputModel.curLine = currentRef.value;
}
@mobx.action
@boundMethod
onKeyDown(e: any) {}
@boundMethod
@mobx.action.bound
onChange(e: any) {
mobx.action(() => {
GlobalModel.inputModel.setCurLine(e.target.value);
})();
GlobalModel.inputModel.curLine = e.target.value;
}
@boundMethod
@mobx.action.bound
onSelect(e: any) {
this.incVersion();
}
@ -453,7 +454,7 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
GlobalModel.inputModel.updateCmdLine(cmdLineUpdate);
}
@boundMethod
@mobx.action.bound
controlP() {
const inputModel = GlobalModel.inputModel;
if (!inputModel.isHistoryLoaded()) {
@ -465,7 +466,7 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
this.lastHistoryUpDown = true;
}
@boundMethod
@mobx.action.bound
controlN() {
const inputModel = GlobalModel.inputModel;
inputModel.moveHistorySelection(-1);
@ -526,17 +527,15 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
});
}
@boundMethod
@mobx.action.bound
handleHistoryInput(e: any) {
const inputModel = GlobalModel.inputModel;
mobx.action(() => {
const opts = mobx.toJS(inputModel.historyQueryOpts.get());
opts.queryStr = e.target.value;
inputModel.setHistoryQueryOpts(opts);
})();
}
@boundMethod
@mobx.action.bound
handleFocus(e: any) {
e.preventDefault();
GlobalModel.inputModel.giveFocus();
@ -561,7 +560,7 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
render() {
const model = GlobalModel;
const inputModel = model.inputModel;
const curLine = inputModel.getCurLine();
const curLine = inputModel.curLine;
let displayLines = 1;
const numLines = curLine.split("\n").length;
const maxCols = this.getTextAreaMaxCols();

View File

@ -8,7 +8,6 @@ import { isBlank } from "@/util/util";
import * as appconst from "@/app/appconst";
import type { Model } from "./model";
import { GlobalCommandRunner, GlobalModel } from "./global";
import { app } from "electron";
function getDefaultHistoryQueryOpts(): HistoryQueryOpts {
return {
@ -48,7 +47,6 @@ class InputModel {
name: "history-items",
deep: false,
}); // sorted in reverse (most recent is index 0)
filteredHistoryItems: mobx.IComputedValue<HistoryItem[]> = null;
historyIndex: mobx.IObservableValue<number> = mobx.observable.box(0, {
name: "history-index",
}); // 1-indexed (because 0 is current)
@ -73,11 +71,10 @@ class InputModel {
physicalInputFocused: OV<boolean> = mobx.observable.box(false);
forceInputFocus: boolean = false;
lastCurLine: string = "";
constructor(globalModel: Model) {
this.globalModel = globalModel;
this.filteredHistoryItems = mobx.computed(() => {
return this._getFilteredHistoryItems();
});
mobx.action(() => {
this.codeSelectSelectedIndex.set(-1);
this.codeSelectBlockRefArray = [];
@ -85,12 +82,12 @@ class InputModel {
this.codeSelectUuid = "";
}
@mobx.action
setInputMode(inputMode: null | "comment" | "global"): void {
mobx.action(() => {
this.inputMode.set(inputMode);
})();
}
@mobx.action
toggleHistoryType(): void {
const opts = mobx.toJS(this.historyQueryOpts.get());
let htype = opts.queryType;
@ -104,6 +101,7 @@ class InputModel {
this.setHistoryType(htype);
}
@mobx.action
toggleRemoteType(): void {
const opts = mobx.toJS(this.historyQueryOpts.get());
if (opts.limitRemote) {
@ -116,33 +114,31 @@ class InputModel {
this.setHistoryQueryOpts(opts);
}
@mobx.action
onInputFocus(isFocused: boolean): void {
mobx.action(() => {
if (isFocused) {
this.inputFocused.set(true);
this.lineFocused.set(false);
} else if (this.inputFocused.get()) {
this.inputFocused.set(false);
}
})();
}
@mobx.action
onLineFocus(isFocused: boolean): void {
mobx.action(() => {
if (isFocused) {
this.inputFocused.set(false);
this.lineFocused.set(true);
} else if (this.lineFocused.get()) {
this.lineFocused.set(false);
}
})();
}
// Focuses the main input or the auxiliary view, depending on the active auxiliary view
@mobx.action
giveFocus(): void {
// Override active view to the main input if aux view does not have focus
const activeAuxView = this.getAuxViewFocus() ? this.getActiveAuxView() : null;
mobx.action(() => {
switch (activeAuxView) {
case appconst.InputAuxView_History: {
const elem: HTMLElement = document.querySelector(".cmd-input input.history-input");
@ -170,13 +166,11 @@ class InputModel {
break;
}
}
})();
}
@mobx.action
setPhysicalInputFocused(isFocused: boolean): void {
mobx.action(() => {
this.physicalInputFocused.set(isFocused);
})();
if (isFocused) {
const screen = this.globalModel.getActiveScreen();
if (screen != null) {
@ -203,6 +197,7 @@ class InputModel {
return false;
}
@mobx.action
setHistoryType(htype: HistoryTypeStrs): void {
if (this.historyQueryOpts.get().queryType == htype) {
return;
@ -214,7 +209,7 @@ class InputModel {
if (oldItem == null) {
return 0;
}
const newItems = this.getFilteredHistoryItems();
const newItems = this.filteredHistoryItems;
if (newItems.length == 0) {
return 0;
}
@ -234,15 +229,15 @@ class InputModel {
return bestIdx + 1;
}
@mobx.action
setHistoryQueryOpts(opts: HistoryQueryOpts): void {
mobx.action(() => {
const oldItem = this.getHistorySelectedItem();
this.historyQueryOpts.set(opts);
const bestIndex = this.findBestNewIndex(oldItem);
setTimeout(() => this.setHistoryIndex(bestIndex, true), 10);
})();
}
@mobx.action
setOpenAICmdInfoChat(chat: OpenAICmdInfoChatMessageType[]): void {
this.AICmdInfoChatItems.replace(chat);
this.codeSelectBlockRefArray = [];
@ -256,6 +251,7 @@ class InputModel {
return hitems != null;
}
@mobx.action
loadHistory(show: boolean, afterLoadIndex: number, htype: HistoryTypeStrs) {
if (this.historyLoading.get()) {
return;
@ -266,12 +262,11 @@ class InputModel {
}
}
this.historyAfterLoadIndex = afterLoadIndex;
mobx.action(() => {
this.historyLoading.set(true);
})();
GlobalCommandRunner.loadHistory(show, htype);
}
@mobx.action
openHistory(): void {
if (this.historyLoading.get()) {
return;
@ -287,13 +282,12 @@ class InputModel {
}
}
@mobx.action
updateCmdLine(cmdLine: StrWithPos): void {
mobx.action(() => {
this.setCurLine(cmdLine.str);
this.curLine = cmdLine.str;
if (cmdLine.pos != appconst.NoStrPos) {
this.forceCursorPos.set(cmdLine.pos);
}
})();
}
getHistorySelectedItem(): HistoryItem {
@ -301,7 +295,7 @@ class InputModel {
if (hidx == 0) {
return null;
}
const hitems = this.getFilteredHistoryItems();
const hitems = this.filteredHistoryItems;
if (hidx > hitems.length) {
return null;
}
@ -309,15 +303,16 @@ class InputModel {
}
getFirstHistoryItem(): HistoryItem {
const hitems = this.getFilteredHistoryItems();
const hitems = this.filteredHistoryItems;
if (hitems.length == 0) {
return null;
}
return hitems[0];
}
@mobx.action
setHistorySelectionNum(hnum: string): void {
const hitems = this.getFilteredHistoryItems();
const hitems = this.filteredHistoryItems;
for (const [i, hitem] of hitems.entries()) {
if (hitem.historynum == hnum) {
this.setHistoryIndex(i + 1);
@ -326,8 +321,8 @@ class InputModel {
}
}
@mobx.action
setHistoryInfo(hinfo: HistoryInfoType): void {
mobx.action(() => {
const oldItem = this.getHistorySelectedItem();
const hitems: HistoryItem[] = hinfo.items ?? [];
this.historyItems.set(hitems);
@ -349,14 +344,10 @@ class InputModel {
if (hinfo.show) {
this.openHistory();
}
})();
}
getFilteredHistoryItems(): HistoryItem[] {
return this.filteredHistoryItems.get();
}
_getFilteredHistoryItems(): HistoryItem[] {
@mobx.computed
get filteredHistoryItems(): HistoryItem[] {
const hitems: HistoryItem[] = this.historyItems.get() ?? [];
const rtn: HistoryItem[] = [];
const opts: HistoryQueryOpts = mobx.toJS(this.historyQueryOpts.get());
@ -416,16 +407,15 @@ class InputModel {
elem.scrollIntoView({ block: "nearest" });
}
@mobx.action
grabSelectedHistoryItem(): void {
const hitem = this.getHistorySelectedItem();
if (hitem == null) {
this.resetHistory();
return;
}
mobx.action(() => {
this.resetInput();
this.setCurLine(hitem.cmdstr);
})();
this.curLine = hitem.cmdstr;
}
// Closes the auxiliary view if it is open, focuses the main input
@ -449,8 +439,8 @@ class InputModel {
mobx.action(() => {
this.auxViewFocus.set(view != null);
this.activeAuxView.set(view);
})();
this.giveFocus();
})();
}
// Gets the focus state of the auxiliary view. If true, the view will get focus. Otherwise, the main input will get focus.
@ -463,16 +453,14 @@ class InputModel {
}
// Sets the focus state of the auxiliary view. If true, the view will get focus. Otherwise, the main input will get focus.
@mobx.action
setAuxViewFocus(focus: boolean): void {
mobx.action(() => {
this.auxViewFocus.set(focus);
})();
this.giveFocus();
}
@mobx.computed
shouldRenderAuxViewKeybindings(view: InputAuxViewType): boolean {
return mobx
.computed(() => {
if (view != null && this.getActiveAuxView() != view) {
return false;
}
@ -485,17 +473,13 @@ class InputModel {
if (view != null && this.getAuxViewFocus()) {
return true;
}
if (
GlobalModel.getActiveScreen().getFocusType() == "input" &&
GlobalModel.activeMainView.get() == "session"
) {
if (GlobalModel.getActiveScreen().getFocusType() == "input" && GlobalModel.activeMainView.get() == "session") {
return true;
}
return false;
})
.get();
}
@mobx.action
setHistoryIndex(hidx: number, force?: boolean): void {
if (hidx < 0) {
return;
@ -503,7 +487,6 @@ class InputModel {
if (!force && this.historyIndex.get() == hidx) {
return;
}
mobx.action(() => {
this.historyIndex.set(hidx);
if (this.getActiveAuxView() == appconst.InputAuxView_History) {
let hitem = this.getHistorySelectedItem();
@ -514,7 +497,6 @@ class InputModel {
this.scrollHistoryItemIntoView(hitem.historynum);
}
}
})();
}
moveHistorySelection(amt: number): void {
@ -524,7 +506,7 @@ class InputModel {
if (!this.isHistoryLoaded()) {
return;
}
const hitems = this.getFilteredHistoryItems();
const hitems = this.filteredHistoryItems;
let idx = this.historyIndex.get() + amt;
if (idx < 0) {
idx = 0;
@ -535,11 +517,10 @@ class InputModel {
this.setHistoryIndex(idx);
}
@mobx.action
flashInfoMsg(info: InfoType, timeoutMs: number): void {
this._clearInfoTimeout();
mobx.action(() => {
this.infoMsg.set(info);
})();
if (info == null && this.getActiveAuxView() == appconst.InputAuxView_Info) {
this.setActiveAuxView(null);
@ -578,7 +559,7 @@ class InputModel {
) {
const curBlockRef = this.codeSelectBlockRefArray[this.codeSelectSelectedIndex.get()];
const codeText = curBlockRef.current.innerText.replace(/\n$/, ""); // remove trailing newline
this.setCurLine(codeText);
this.curLine = codeText;
this.giveFocus();
}
}
@ -594,8 +575,8 @@ class InputModel {
return rtn;
}
@mobx.action
setCodeSelectSelectedCodeBlock(blockIndex: number) {
mobx.action(() => {
if (blockIndex >= 0 && blockIndex < this.codeSelectBlockRefArray.length) {
this.codeSelectSelectedIndex.set(blockIndex);
const currentRef = this.codeSelectBlockRefArray[blockIndex].current;
@ -606,20 +587,18 @@ class InputModel {
let elemBottom = elemTop - currentRef.offsetHeight;
const elementIsInView = elemBottom < chatWindowBottom && elemTop > chatWindowTop;
if (!elementIsInView) {
this.aiChatWindowRef.current.scrollTop =
elemBottom - this.aiChatWindowRef.current.clientHeight / 3;
this.aiChatWindowRef.current.scrollTop = elemBottom - this.aiChatWindowRef.current.clientHeight / 3;
}
}
}
this.codeSelectBlockRefArray = [];
this.setAIChatFocus();
})();
}
@mobx.action
codeSelectSelectNextNewestCodeBlock() {
// oldest code block = index 0 in array
// this decrements codeSelectSelected index
mobx.action(() => {
if (this.codeSelectSelectedIndex.get() == this.codeSelectTop) {
this.codeSelectSelectedIndex.set(this.codeSelectBottom);
} else if (this.codeSelectSelectedIndex.get() == this.codeSelectBottom) {
@ -635,11 +614,10 @@ class InputModel {
if (incBlockIndex >= 0 && incBlockIndex < this.codeSelectBlockRefArray.length) {
this.setCodeSelectSelectedCodeBlock(incBlockIndex);
}
})();
}
@mobx.action
codeSelectSelectNextOldestCodeBlock() {
mobx.action(() => {
if (this.codeSelectSelectedIndex.get() == this.codeSelectBottom) {
if (this.codeSelectBlockRefArray.length > 0) {
this.codeSelectSelectedIndex.set(this.codeSelectBlockRefArray.length);
@ -659,7 +637,6 @@ class InputModel {
if (decBlockIndex >= 0 && decBlockIndex < this.codeSelectBlockRefArray.length) {
this.setCodeSelectSelectedCodeBlock(decBlockIndex);
}
})();
}
getCodeSelectSelectedIndex() {
@ -684,6 +661,7 @@ class InputModel {
})();
}
@mobx.action
openAIAssistantChat(): void {
this.setActiveAuxView(appconst.InputAuxView_AIChat);
this.setAuxViewFocus(true);
@ -723,19 +701,19 @@ class InputModel {
}
}
@mobx.action
clearInfoMsg(setNull: boolean): void {
this._clearInfoTimeout();
if (this.getActiveAuxView() == appconst.InputAuxView_Info) {
this.setActiveAuxView(null);
}
mobx.action(() => {
if (setNull) {
this.infoMsg.set(null);
}
})();
}
@mobx.action
toggleInfoMsg(): void {
this._clearInfoTimeout();
if (this.activeAuxView.get() == appconst.InputAuxView_Info) {
@ -747,39 +725,28 @@ class InputModel {
@boundMethod
uiSubmitCommand(): void {
mobx.action(() => {
const commandStr = this.getCurLine();
const commandStr = this.curLine;
if (commandStr.trim() == "") {
return;
}
mobx.action(() => {
this.resetInput();
this.globalModel.submitRawCommand(commandStr, true, true);
})();
this.globalModel.submitRawCommand(commandStr, true, true);
}
isEmpty(): boolean {
return this.getCurLine().trim() == "";
return this.curLine.trim() == "";
}
@mobx.action
resetInputMode(): void {
mobx.action(() => {
this.setInputMode(null);
this.setCurLine("");
})();
}
setCurLine(val: string): void {
const hidx = this.historyIndex.get();
mobx.action(() => {
if (this.modHistory.length <= hidx) {
this.modHistory.length = hidx + 1;
}
this.modHistory[hidx] = val;
})();
this.curLine = "";
}
@mobx.action
resetInput(): void {
mobx.action(() => {
this.setActiveAuxView(null);
this.inputMode.set(null);
this.resetHistory();
@ -787,23 +754,22 @@ class InputModel {
this.infoMsg.set(null);
this.inputExpanded.set(false);
this._clearInfoTimeout();
})();
}
@mobx.action
@boundMethod
toggleExpandInput(): void {
mobx.action(() => {
this.inputExpanded.set(!this.inputExpanded.get());
this.forceInputFocus = true;
})();
}
getCurLine(): string {
@mobx.computed
get curLine(): string {
const hidx = this.historyIndex.get();
if (hidx < this.modHistory.length && this.modHistory[hidx] != null) {
return this.modHistory[hidx];
}
const hitems = this.getFilteredHistoryItems();
const hitems = this.filteredHistoryItems;
if (hidx == 0 || hitems == null || hidx > hitems.length) {
return "";
}
@ -814,8 +780,19 @@ class InputModel {
return hitem.cmdstr;
}
dropModHistory(keepLine0: boolean): void {
set curLine(val: string) {
this.lastCurLine = this.curLine;
const hidx = this.historyIndex.get();
mobx.action(() => {
if (this.modHistory.length <= hidx) {
this.modHistory.length = hidx + 1;
}
this.modHistory[hidx] = val;
})();
}
@mobx.action
dropModHistory(keepLine0: boolean): void {
if (keepLine0) {
if (this.modHistory.length > 1) {
this.modHistory.splice(1, this.modHistory.length - 1);
@ -823,11 +800,10 @@ class InputModel {
} else {
this.modHistory.replace([""]);
}
})();
}
@mobx.action
resetHistory(): void {
mobx.action(() => {
if (this.getActiveAuxView() == appconst.InputAuxView_History) {
this.setActiveAuxView(null);
}
@ -838,7 +814,6 @@ class InputModel {
this.historyQueryOpts.set(getDefaultHistoryQueryOpts());
this.historyAfterLoadIndex = 0;
this.dropModHistory(true);
})();
}
}

View File

@ -37,7 +37,6 @@ import { GlobalCommandRunner } from "./global";
import { clearMonoFontCache, getMonoFontSize } from "@/util/textmeasure";
import type { TermWrap } from "@/plugins/terminal/term";
import * as util from "@/util/util";
import { url } from "node:inspector";
type SWLinePtr = {
line: LineType;
@ -1327,7 +1326,21 @@ class Model {
}
}
submitCommandPacket(cmdPk: FeCmdPacketType, interactive: boolean): Promise<CommandRtnType> {
/**
* Submits a command packet to the server and processes the response.
* @param cmdPk The command packet to submit.
* @param interactive Whether the command is interactive.
* @param runUpdate Whether to run the update after the command is submitted. If true, the update will be processed and the frontend will be updated. If false, the update will be returned in the promise.
* @returns A promise that resolves to a CommandRtnType.
* @throws An error if the command fails.
* @see CommandRtnType
* @see FeCmdPacketType
**/
submitCommandPacket(
cmdPk: FeCmdPacketType,
interactive: boolean,
runUpdate: boolean = true
): Promise<CommandRtnType> {
if (this.debugCmds > 0) {
console.log("[cmd]", cmdPacketString(cmdPk));
if (this.debugCmds > 1) {
@ -1345,16 +1358,20 @@ class Model {
})
.then((resp) => handleJsonFetchResponse(url, resp))
.then((data) => {
mobx.action(() => {
return mobx.action(() => {
const update = data.data;
if (update != null) {
if (runUpdate) {
this.runUpdate(update, interactive);
} else {
return { success: true, update: update };
}
}
if (interactive && !this.isInfoUpdate(update)) {
this.inputModel.clearInfoMsg(true);
}
})();
return { success: true };
})();
})
.catch((err) => {
this.errorHandler("calling run-command", err, interactive);
@ -1367,12 +1384,23 @@ class Model {
return prtn;
}
/**
* Submits a command to the server and processes the response.
* @param metaCmd The meta command to run.
* @param metaSubCmd The meta subcommand to run.
* @param args The arguments to pass to the command.
* @param kwargs The keyword arguments to pass to the command.
* @param interactive Whether the command is interactive.
* @param runUpdate Whether to run the update after the command is submitted. If true, the update will be processed and the frontend will be updated. If false, the update will be returned in the promise.
* @returns A promise that resolves to a CommandRtnType.
*/
submitCommand(
metaCmd: string,
metaSubCmd: string,
args: string[],
kwargs: Record<string, string>,
interactive: boolean
interactive: boolean,
runUpdate: boolean = true
): Promise<CommandRtnType> {
const pk: FeCmdPacketType = {
type: "fecmd",
@ -1393,7 +1421,7 @@ class Model {
pk.interactive
);
*/
return this.submitCommandPacket(pk, interactive);
return this.submitCommandPacket(pk, interactive, runUpdate);
}
getSingleEphemeralCommandOutput(url: URL): Promise<string> {
@ -1412,12 +1440,10 @@ class Model {
let stderr = "";
if (ephemeralCommandResponse.stdouturl) {
const url = new URL(this.getBaseHostPort() + ephemeralCommandResponse.stdouturl);
console.log("stdouturl", url);
stdout = await this.getSingleEphemeralCommandOutput(url);
}
if (ephemeralCommandResponse.stderrurl) {
const url = new URL(this.getBaseHostPort() + ephemeralCommandResponse.stderrurl);
console.log("stderrurl", url);
stderr = await this.getSingleEphemeralCommandOutput(url);
}
return { stdout: stdout, stderr: stderr };
@ -1476,14 +1502,14 @@ class Model {
interactive: interactive,
ephemeralopts: ephemeralopts,
};
console.log(
"CMD",
pk.metacmd + (pk.metasubcmd != null ? ":" + pk.metasubcmd : ""),
pk.args,
pk.kwargs,
pk.interactive,
pk.ephemeralopts
);
// console.log(
// "CMD",
// pk.metacmd + (pk.metasubcmd != null ? ":" + pk.metasubcmd : ""),
// pk.args,
// pk.kwargs,
// pk.interactive,
// pk.ephemeralopts
// );
return this.submitEphemeralCommandPacket(pk, interactive);
}

View File

@ -821,6 +821,7 @@ declare global {
type CommandRtnType = {
success: boolean;
error?: string;
update?: UpdatePacket;
};
type EphemeralCommandOutputType = {

View File

@ -833,6 +833,7 @@ type RunPacketType struct {
Detached bool `json:"detached,omitempty"`
ReturnState bool `json:"returnstate,omitempty"`
IsSudo bool `json:"issudo,omitempty"`
Timeout time.Duration `json:"timeout"` // TODO: added vnext. This is the timeout for the command to run. If the command does not complete in this time, it will be killed. The default zero value will not impose a timeout.
}
func (*RunPacketType) GetType() string {

View File

@ -926,12 +926,11 @@ func RunCommandSimple(pk *packet.RunPacketType, sender *packet.PacketSender, fro
rcFileName = fmt.Sprintf("/dev/fd/%d", rcFileFdNum)
}
if cmd.TmpRcFileName != "" {
go func() {
time.AfterFunc(2*time.Second, func() {
// cmd.Close() will also remove rcFileName
// adding this to also try to proactively clean up after 2-seconds.
time.Sleep(2 * time.Second)
os.Remove(cmd.TmpRcFileName)
}()
})
}
fullCmdStr := pk.Command
if pk.ReturnState {
@ -1109,6 +1108,14 @@ func RunCommandSimple(pk *packet.RunPacketType, sender *packet.PacketSender, fro
if err != nil {
return nil, err
}
if pk.Timeout > 0 {
// Cancel the command if it takes too long
time.AfterFunc(pk.Timeout, func() {
cmd.Cmd.Cancel()
cmd.Close()
})
}
return cmd, nil
}

View File

@ -15,6 +15,7 @@ import (
"time"
"github.com/google/uuid"
"github.com/wavetermdev/waveterm/waveshell/pkg/wlog"
"github.com/wavetermdev/waveterm/wavesrv/pkg/scbase"
"github.com/wavetermdev/waveterm/wavesrv/pkg/waveenc"
)
@ -110,6 +111,7 @@ func (pipe *BufferedPipe) WriteTo(w io.Writer) (n int64, err error) {
// Close the pipe. This will cause any blocking WriteTo calls to return.
func (pipe *BufferedPipe) Close() error {
wlog.Logf("closing buffered pipe %s", pipe.Key)
defer pipe.bufferDataCond.Broadcast()
pipe.closed.Store(true)
return nil

View File

@ -1987,9 +1987,22 @@ func RunCommand(ctx context.Context, rcOpts RunCommandOpts, runPacket *packet.Ru
// Setting UsePty to false will ensure that the outputs get written to the correct file descriptors to extract stdout and stderr
runPacket.UsePty = rcOpts.EphemeralOpts.UsePty
// Ephemeral commands can override the cwd without persisting it to the DB
// Ephemeral commands can override the current working directory. We need to expand the home dir if it's relative.
if rcOpts.EphemeralOpts.OverrideCwd != "" {
currentState.Cwd = rcOpts.EphemeralOpts.OverrideCwd
overrideCwd := rcOpts.EphemeralOpts.OverrideCwd
if !strings.HasPrefix(overrideCwd, "/") {
expandedCwd, err := msh.GetRemoteRuntimeState().ExpandHomeDir(overrideCwd)
if err != nil {
return nil, nil, fmt.Errorf("cannot expand home dir for cwd: %w", err)
}
overrideCwd = expandedCwd
}
currentState.Cwd = overrideCwd
}
// Ephemeral commands can override the timeout
if rcOpts.EphemeralOpts.TimeoutMs > 0 {
runPacket.Timeout = time.Duration(rcOpts.EphemeralOpts.TimeoutMs) * time.Millisecond
}
// Ephemeral commands can override the env without persisting it to the DB
@ -2405,6 +2418,7 @@ func (msh *MShellProc) handleCmdStartError(rct *RunCmdType, startErr error) {
defer msh.RemoveRunningCmd(rct.CK)
if rct.EphemeralOpts != nil {
// nothing to do for ephemeral commands besides remove the running command
log.Printf("ephemeral command start error: %v\n", startErr)
return
}
ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
@ -2472,6 +2486,11 @@ func (msh *MShellProc) handleCmdDonePacket(rct *RunCmdType, donePk *packet.CmdDo
// Close the ephemeral response writer if it exists
if rct.EphemeralOpts != nil && rct.EphemeralOpts.ExpectsResponse {
if donePk.ExitCode != 0 {
// if the command failed, we need to write the error to the response writer
log.Printf("writing error to ephemeral response writer\n")
rct.EphemeralOpts.StderrWriter.Write([]byte(fmt.Sprintf("error: %d\n", donePk.ExitCode)))
}
log.Printf("closing ephemeral response writers\n")
defer rct.EphemeralOpts.StdoutWriter.Close()
defer rct.EphemeralOpts.StderrWriter.Close()
@ -2577,6 +2596,7 @@ func (msh *MShellProc) handleDataPacket(rct *RunCmdType, dataPk *packet.DataPack
return
}
if rct.EphemeralOpts != nil {
log.Printf("ephemeral data packet: %s\n", dataPk.CK)
// Write to the response writer if it's set
if len(realData) > 0 && rct.EphemeralOpts.ExpectsResponse {
switch dataPk.FdNum {
@ -2594,6 +2614,9 @@ func (msh *MShellProc) handleDataPacket(rct *RunCmdType, dataPk *packet.DataPack
log.Printf("error handling data packet: invalid fdnum %d\n", dataPk.FdNum)
}
}
if dataPk.Error != "" {
log.Printf("ephemeral data packet error: %s\n", dataPk.Error)
}
ack := makeDataAckPacket(dataPk.CK, dataPk.FdNum, len(realData), nil)
msh.ServerProc.Input.SendPacket(ack)
return