diff --git a/public/themes/default.css b/public/themes/default.css index c015dbbf..9233a226 100644 --- a/public/themes/default.css +++ b/public/themes/default.css @@ -42,16 +42,19 @@ --app-bg-color: black; --app-accent-color: rgb(88, 193, 66); --app-accent-bg-color: rgba(88, 193, 66, 0.25); + --app-accent-bg-darker-color: rgba(88, 193, 66, 0.5); --app-text-color: rgb(211, 215, 207); --app-text-primary-color: rgb(255, 255, 255); --app-text-secondary-color: rgb(195, 200, 194); --app-text-disabled-color: rgb(173, 173, 173); + --app-text-bg-color: rgb(23, 23, 23); + --app-text-bg-disabled-color: rgb(51, 51, 51); --app-border-color: rgb(51, 51, 51); - --app-maincontent-bg-color: #333; + --app-maincontent-bg-color: rgb(51, 51, 51); --app-panel-bg-color: rgba(21, 23, 21, 1); --app-panel-bg-color-dev: rgb(21, 23, 48); --app-icon-color: rgb(139, 145, 138); - --app-icon-hover-color: #fff; + --app-icon-hover-color: rgb(255, 255, 255); --app-selected-mask-color: rgba(255, 255, 255, 0.06); /* global status colors */ @@ -122,18 +125,18 @@ /* line colors */ --line-sidebar-message-color: rgb(196, 160, 0); - --line-background: rgba(21, 23, 21, 1); - --line-avatar-color: #eceeec; + --line-background: var(--app-panel-bg-color); + --line-avatar-color: rgb(236, 238, 236); --line-text-color: rgb(211, 215, 207); --line-svg-fill-color: rgb(150, 152, 150); - --line-svg-hover-fill-color: #eceeec; + --line-svg-hover-fill-color: rgb(236, 238, 236); --line-separator-color: rgb(126, 126, 126); --line-error-color: var(--app-error-color); --line-warning-color: var(--app-warning-color); - --line-base-soft-blue-color: #729fcf; + --line-base-soft-blue-color: rgb(114, 159, 207); --line-active-border-color: var(--app-accent-color); --line-selected-bg-color: rgba(255, 255, 255, 0.05); - --line-selected-border-left-color: #777777; + --line-selected-border-left-color: rgb(119, 119, 119); --line-selected-error-border-color: rgba(204, 0, 0, 0.8); --line-selected-error-bg-color: rgb(19, 4, 3); --line-error-bg-color: rgba(200, 0, 0, 0.1); @@ -152,22 +155,21 @@ /* table colors */ --table-border-color: rgba(241, 246, 243, 0.15); --table-thead-border-top-color: rgba(250, 250, 250, 0.1); - --table-thead-bright-border-color: #ccc; + --table-thead-bright-border-color: rgb(204, 204, 204); --table-thead-bg-color: rgba(250, 250, 250, 0.02); --table-tr-border-bottom-color: rgba(241, 246, 243, 0.15); --table-tr-hover-bg-color: rgba(255, 255, 255, 0.06); - --table-tr-selected-bg-color: #222; - --table-tr-selected-hover-bg-color: #333; + --table-tr-selected-bg-color: rgb(34, 34, 34); + --table-tr-selected-hover-bg-color: var(--app-maincontent-bg-color); /* cmdinput colors */ - --cmdinput-textarea-bg-color: #171717; + --cmdinput-bg-color: var(--app-text-bg-color); --cmdinput-text-error-color: var(--term-red); --cmdinput-history-item-error-color: var(--term-bright-red); --cmdinput-history-item-selected-error-color: var(--term-bright-red); - --cmdinput-button-bg-color: rgb(88, 193, 66); - --cmdinput-comment-button-bg-color: rgb(57, 113, 255); - --cmdinput-disabled-icon-color: rgb(76, 81, 75, 1); - --cmdinput-history-bg-color: rgb(21, 23, 21, 1); + --cmdinput-button-bg-color: var(--tab-green); + --cmdinput-disabled-bg-color: var(--app-text-bg-disabled-color); + --cmdinput-history-bg-color: var(--app-bg-color); /* screen view color */ --screen-view-text-caption-color: rgb(139, 145, 138); diff --git a/public/themes/light.css b/public/themes/light.css index 7431c68a..30a9194e 100644 --- a/public/themes/light.css +++ b/public/themes/light.css @@ -5,20 +5,20 @@ @import url("./term-light.css"); :root { - --app-bg-color: #fefefe; + --app-bg-color: rgb(254, 254, 254); --app-accent-color: rgb(75, 166, 57); --app-accent-bg-color: rgba(75, 166, 57, 0.2); - --app-text-color: #000; + --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-border-color: rgb(139 145 138); - --app-panel-bg-color: #e0e0e0; - --app-panel-bg-color-dev: #e0e0e0; - --app-icon-color: rgb(80, 80, 80); - --app-icon-hover-color: rgb(100, 100, 100); + --app-panel-bg-color: rgb(224, 224, 224); + --app-panel-bg-color-dev: rgb(224, 224, 224); + --app-icon-color: rgb(110, 110, 110); + --app-icon-hover-color: rgb(80, 80, 80); --app-selected-mask-color: rgba(0, 0, 0, 0.06); - --input-bg-color: #eeeeee; + --input-bg-color: rgb(238, 238, 238); /* tab color */ --tab-white: rgb(0, 0, 0, 0.6); @@ -30,8 +30,8 @@ --table-thead-bg-color: rgba(250, 250, 250, 0.15); --table-tr-border-bottom-color: rgba(0, 0, 0, 0.15); --table-tr-hover-bg-color: rgba(0, 0, 0, 0.15); - --table-tr-selected-bg-color: #dddddd; - --table-tr-selected-hover-bg-color: #cccccc; + --table-tr-selected-bg-color: rgb(221, 221, 221); + --table-tr-selected-hover-bg-color: rgb(204, 204, 204); /* form colors */ --form-element-border-color: rgba(0, 0, 0, 0.3); @@ -41,8 +41,8 @@ --form-element-label-color: rgba(0, 0, 0, 0.6); --form-element-secondary-color: rgba(0, 0, 0, 0.09); --form-element-icon-color: rgb(0, 0, 0, 0.6); - --form-element-disabled-text-color: #b7b7b7; - --form-element-placeholder-color: #b7b7b7; + --form-element-disabled-text-color: rgb(183, 183, 183); + --form-element-placeholder-color: rgb(183, 183, 183); --markdown-bg-color: rgb(0, 0, 0, 0.1); @@ -50,8 +50,6 @@ --modal-header-bottom-border-color: rgba(0, 0, 0, 0.3); /* cmd input */ - --cmdinput-textarea-bg-color: rgba(0, 0, 0, 0.1); - --cmdinput-textarea-border-color: var(--form-element-border-color); /* scroll colors */ --scrollbar-background-color: var(--app-bg-color); @@ -64,15 +62,15 @@ --line-actions-inactive-color: rgba(0, 0, 0, 0.3); --line-actions-active-color: rgba(0, 0, 0, 1); - --logo-button-hover-bg-color: #f0f0f0; + --logo-button-hover-bg-color: rgb(240, 240, 240); --xterm-viewport-border-color: rgba(0, 0, 0, 0.3); - --datepicker-cell-hover-bg-color: #f0f0f0; + --datepicker-cell-hover-bg-color: rgb(240, 240, 240); --datepicker-cell-other-text-color: rgba(0, 0, 0, 0.3); --datepicker-header-fade-color: rgba(0, 0, 0, 0.4); - --datepicker-year-header-bg-color: #f5f5f5; - --datepicker-year-header-border-color: #dcdcdc; + --datepicker-year-header-bg-color: rgb(245, 245, 245); + --datepicker-year-header-border-color: rgb(220, 220, 220); /* toggle colors */ --toggle-thumb-color: var(--app-bg-color); diff --git a/src/app/line/line.less b/src/app/line/line.less index 532ac349..c05603ac 100644 --- a/src/app/line/line.less +++ b/src/app/line/line.less @@ -106,11 +106,6 @@ } } - &.top-border { - border-top: 1px solid #777; - padding: 10px; - } - &:hover .meta .termopts { display: block; } diff --git a/src/app/sidebar/main.tsx b/src/app/sidebar/main.tsx index 1ed22ce6..80828fe2 100644 --- a/src/app/sidebar/main.tsx +++ b/src/app/sidebar/main.tsx @@ -242,7 +242,7 @@ class MainSideBar extends React.Component { } render() { - let mainView = GlobalModel.activeMainView.get(); + const mainView = GlobalModel.activeMainView.get(); const historyActive = mainView == "history"; const connectionsActive = mainView == "connections"; const settingsActive = mainView == "clientsettings"; diff --git a/src/app/workspace/cmdinput/aichat.less b/src/app/workspace/cmdinput/aichat.less new file mode 100644 index 00000000..fe42fba7 --- /dev/null +++ b/src/app/workspace/cmdinput/aichat.less @@ -0,0 +1,70 @@ +.cmd-aichat { + padding-bottom: 0 !important; + .auxview-content { + flex-flow: column nowrap; + height: 100%; + + .chat-window { + overflow-y: auto; + margin-bottom: 5px; + flex: 1 1 auto; + flex-direction: column-reverse; + } + + .chat-input { + padding: 0.5em 0.5em 0.5em 0.5em; + flex: 1 0 auto; + + .chat-textarea { + color: var(--app-text-primary-color); + background-color: var(--cmdinput-textarea-bg); + resize: none; + width: 100%; + border: transparent; + outline: none; + overflow: auto; + overflow-wrap: anywhere; + font-family: var(--termfontfamily); + font-weight: normal; + line-height: var(--termlineheight); + } + } + + .chat-msg { + margin-top: calc(var(--termpad) * 2); + margin-bottom: calc(var(--termpad) * 2); + + .chat-msg-header { + display: flex; + margin-bottom: 2px; + + i { + margin-right: 0.5em; + } + + .chat-username { + font-weight: bold; + margin-right: 5px; + } + } + } + + .chat-msg-assistant { + color: var(--app-text-color); + } + + .chat-msg-user { + .msg-text { + font-family: var(--markdown-font); + font-size: 14px; + white-space: pre-wrap; + } + } + + .chat-msg-error { + color: var(--cmdinput-text-error); + font-family: var(--markdown-font); + font-size: 14px; + } + } +} diff --git a/src/app/workspace/cmdinput/aichat.tsx b/src/app/workspace/cmdinput/aichat.tsx index 7fef6c78..5adfbf56 100644 --- a/src/app/workspace/cmdinput/aichat.tsx +++ b/src/app/workspace/cmdinput/aichat.tsx @@ -5,18 +5,18 @@ import * as React from "react"; import * as mobxReact from "mobx-react"; import * as mobx from "mobx"; import { GlobalModel } from "@/models"; -import { isBlank } from "@/util/util"; import { boundMethod } from "autobind-decorator"; -import cn from "classnames"; import { If, For } from "tsx-control-statements/components"; import { Markdown } from "@/elements"; -import { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "@/util/keyutil"; +import { AuxiliaryCmdView } from "./auxview"; + +import "./aichat.less"; class AIChatKeybindings extends React.Component<{ AIChatObject: AIChat }, {}> { componentDidMount(): void { - let AIChatObject = this.props.AIChatObject; - let keybindManager = GlobalModel.keybindManager; - let inputModel = GlobalModel.inputModel; + const AIChatObject = this.props.AIChatObject; + const keybindManager = GlobalModel.keybindManager; + const inputModel = GlobalModel.inputModel; keybindManager.registerKeybinding("pane", "aichat", "generic:confirm", (waveEvent) => { AIChatObject.onEnterKeyPressed(); @@ -54,10 +54,10 @@ class AIChatKeybindings extends React.Component<{ AIChatObject: AIChat }, {}> { @mobxReact.observer class AIChat extends React.Component<{}, {}> { chatListKeyCount: number = 0; - textAreaNumLines: mobx.IObservableValue = mobx.observable.box(1, { name: "textAreaNumLines" }); chatWindowScrollRef: React.RefObject; textAreaRef: React.RefObject; isFocused: OV; + termFontSize: number = 14; constructor(props: any) { super(props); @@ -69,8 +69,7 @@ class AIChat extends React.Component<{}, {}> { } componentDidMount() { - let model = GlobalModel; - let inputModel = model.inputModel; + const inputModel = GlobalModel.inputModel; if (this.chatWindowScrollRef != null && this.chatWindowScrollRef.current != null) { this.chatWindowScrollRef.current.scrollTop = this.chatWindowScrollRef.current.scrollHeight; } @@ -79,6 +78,7 @@ class AIChat extends React.Component<{}, {}> { inputModel.setCmdInfoChatRefs(this.textAreaRef, this.chatWindowScrollRef); } this.requestChatUpdate(); + this.onTextAreaChange(null); } componentDidUpdate() { @@ -92,20 +92,18 @@ class AIChat extends React.Component<{}, {}> { } submitChatMessage(messageStr: string) { - let model = GlobalModel; - let inputModel = model.inputModel; - let curLine = inputModel.getCurLine(); - let prtn = GlobalModel.submitChatInfoCommand(messageStr, curLine, false); + const curLine = GlobalModel.inputModel.getCurLine(); + const prtn = GlobalModel.submitChatInfoCommand(messageStr, curLine, false); prtn.then((rtn) => { if (!rtn.success) { console.log("submit chat command error: " + rtn.error); } - }).catch((error) => {}); + }).catch((_) => {}); } getLinePos(elem: any): { numLines: number; linePos: number } { - let numLines = elem.value.split("\n").length; - let linePos = elem.value.substr(0, elem.selectionStart).split("\n").length; + const numLines = elem.value.split("\n").length; + const linePos = elem.value.substr(0, elem.selectionStart).split("\n").length; return { numLines, linePos }; } @@ -121,22 +119,32 @@ class AIChat extends React.Component<{}, {}> { })(); } + // Adjust the height of the textarea to fit the text onTextAreaChange(e: any) { - // set height of textarea based on number of newlines - mobx.action(() => { - this.textAreaNumLines.set(e.target.value.split(/\n/).length); - GlobalModel.inputModel.codeSelectDeselectAll(); - })(); + // Calculate the bounding height of the text area + const textAreaMaxLines = 4; + const textAreaLineHeight = this.termFontSize * 1.5; + const textAreaMinHeight = textAreaLineHeight; + const textAreaMaxHeight = textAreaLineHeight * textAreaMaxLines; + + // Get the height of the wrapped text area content. Courtesy of https://stackoverflow.com/questions/995168/textarea-to-resize-based-on-content-length + this.textAreaRef.current.style.height = "1px"; + const scrollHeight: number = this.textAreaRef.current.scrollHeight; + + // Set the new height of the text area, bounded by the min and max height. + const newHeight = Math.min(Math.max(scrollHeight, textAreaMinHeight), textAreaMaxHeight); + this.textAreaRef.current.style.height = newHeight + "px"; + GlobalModel.inputModel.codeSelectDeselectAll(); } onEnterKeyPressed() { - let inputModel = GlobalModel.inputModel; - let currentRef = this.textAreaRef.current; + const inputModel = GlobalModel.inputModel; + const currentRef = this.textAreaRef.current; if (currentRef == null) { return; } if (inputModel.getCodeSelectSelectedIndex() == -1) { - let messageStr = currentRef.value; + const messageStr = currentRef.value; this.submitChatMessage(messageStr); currentRef.value = ""; } else { @@ -145,7 +153,7 @@ class AIChat extends React.Component<{}, {}> { } onExpandInputPressed() { - let currentRef = this.textAreaRef.current; + const currentRef = this.textAreaRef.current; if (currentRef == null) { return; } @@ -154,7 +162,7 @@ class AIChat extends React.Component<{}, {}> { } onArrowUpPressed(): boolean { - let currentRef = this.textAreaRef.current; + const currentRef = this.textAreaRef.current; if (currentRef == null) { return false; } @@ -168,8 +176,8 @@ class AIChat extends React.Component<{}, {}> { } onArrowDownPressed(): boolean { - let currentRef = this.textAreaRef.current; - let inputModel = GlobalModel.inputModel; + const currentRef = this.textAreaRef.current; + const inputModel = GlobalModel.inputModel; if (currentRef == null) { return false; } @@ -190,10 +198,10 @@ class AIChat extends React.Component<{}, {}> { } renderChatMessage(chatItem: OpenAICmdInfoChatMessageType): any { - let curKey = "chatmsg-" + this.chatListKeyCount; + const curKey = "chatmsg-" + this.chatListKeyCount; this.chatListKeyCount++; - let senderClassName = chatItem.isassistantresponse ? "chat-msg-assistant" : "chat-msg-user"; - let msgClassName = "chat-msg " + senderClassName; + const senderClassName = chatItem.isassistantresponse ? "chat-msg-assistant" : "chat-msg-user"; + const msgClassName = "chat-msg " + senderClassName; let innerHTML: React.JSX.Element = (
@@ -226,31 +234,10 @@ class AIChat extends React.Component<{}, {}> { ); } - renderChatWindow(): any { - let model = GlobalModel; - let inputModel = model.inputModel; - let chatMessageItems = inputModel.AICmdInfoChatItems.slice(); - let chitem: OpenAICmdInfoChatMessageType = null; - return ( -
- - {this.renderChatMessage(chitem)} - -
- ); - } - render() { - let model = GlobalModel; - let inputModel = model.inputModel; - - const termFontSize = 14; - const textAreaMaxLines = 4; - const textAreaLineHeight = termFontSize * 1.5; - const textAreaPadding = 2 * 0.5 * termFontSize; - let textAreaMaxHeight = textAreaLineHeight * textAreaMaxLines + textAreaPadding; - let textAreaInnerHeight = this.textAreaNumLines.get() * textAreaLineHeight + textAreaPadding; - let renderKeybindings = mobx + const chatMessageItems = GlobalModel.inputModel.AICmdInfoChatItems.slice(); + const chitem: OpenAICmdInfoChatMessageType = null; + const renderKeybindings = mobx .computed(() => { return ( this.isFocused.get() || @@ -260,42 +247,39 @@ class AIChat extends React.Component<{}, {}> { ); }) .get(); + return ( -
+ GlobalModel.inputModel.closeAIAssistantChat(true)} + iconClass="fa-sharp fa-solid fa-sparkles" + > -
-
- -
-
Wave AI
-
-
inputModel.closeAIAssistantChat(true)} - > - -
+
+ + {this.renderChatMessage(chitem)} +
-
- {this.renderChatWindow()} - -
+
+ +
+ ); } } diff --git a/src/app/workspace/cmdinput/auxview.less b/src/app/workspace/cmdinput/auxview.less new file mode 100644 index 00000000..374d9940 --- /dev/null +++ b/src/app/workspace/cmdinput/auxview.less @@ -0,0 +1,51 @@ +// For the additonal views, we want less padding on the top and bottom than we want for the base-cmdinput div +.auxview { + padding: var(--termpad) calc(var(--termpad) * 2) var(--termpad) calc(var(--termpad) * 3 - 1px); + overflow: auto; + flex-shrink: 1; + width: 100%; + + --auxview-titlebar-height: 18px; + + .auxview-titlebar { + position: absolute; + z-index: 22; + top: 0; + left: 0; + background-color: var(--app-panel-bg-color); + color: var(--term-blue); + padding: 6px 10px 6px 10px; + display: flex; + flex-direction: row; + width: 100%; + border-bottom: 1px solid var(--app-border-color); + font: var(--base-font); + user-select: none; + cursor: default; + line-height: var(--auxview-titlebar-height); + + .title-string { + font-weight: bold; + } + + .close-button { + cursor: pointer; + i { + color: var(--app-icon-color); + + &:hover { + color: var(--app-icon-hover-color); + } + } + } + + div:not(.close-button, .flex-spacer) { + margin-right: 10px; + } + } + + .auxview-content { + display: flex; + padding-top: calc(var(--auxview-titlebar-height) + 6px); + } +} diff --git a/src/app/workspace/cmdinput/auxview.tsx b/src/app/workspace/cmdinput/auxview.tsx new file mode 100644 index 00000000..281d43de --- /dev/null +++ b/src/app/workspace/cmdinput/auxview.tsx @@ -0,0 +1,50 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import * as React from "react"; +import cn from "classnames"; +import { If } from "tsx-control-statements/components"; + +import "./auxview.less"; + +export class AuxiliaryCmdView extends React.Component< + { + title: string; + className?: string; + iconClass?: string; + titleBarContents?: React.ReactElement[]; + children?: React.ReactNode; + onClose?: React.MouseEventHandler; + }, + {} +> { + render() { + const { title, className, iconClass, titleBarContents, children, onClose } = this.props; + + return ( +
+
+ +
+ +
+
+
{title}
+ + {titleBarContents} + +
+ + +
+ +
+
+
+ +
{children}
+
+
+ ); + } +} diff --git a/src/app/workspace/cmdinput/cmdinput.less b/src/app/workspace/cmdinput/cmdinput.less index 973159f7..97d02351 100644 --- a/src/app/workspace/cmdinput/cmdinput.less +++ b/src/app/workspace/cmdinput/cmdinput.less @@ -7,127 +7,17 @@ position: absolute; bottom: 0; width: 100%; - padding: calc(var(--termpad) * 2) calc(var(--termpad) * 2) calc(var(--termpad) * 2) calc(var(--termpad) * 3 - 1px); z-index: 100; border-top: 2px solid var(--app-border-color); background-color: var(--app-bg-color); - &.has-info, + // Apply a border between the base cmdinput and any views shown above it + // TODO: use a generic selector for this &.has-aichat, - &.has-history { - .cmdinput-actions { - display: none; - } - } - - .titlebar-spacer { - height: 31px; - } - - .cmdinput-conn { - position: absolute; - top: 0; - left: 0; - border-radius: 0 0 4px 0; - background-color: rgba(88, 193, 66, 0.3); - padding: 2px 10px 4px 10px; - font-size: calc(var(--termfontsize)); - cursor: pointer; - - i { - margin-left: 5px; - } - - &:hover { - background-color: rgba(88, 193, 66, 0.5); - } - } - - .cmdinput-actions { - position: absolute; - border-radius: 4px; - padding-left: 4px; - padding-right: 4px; - font-size: calc(var(--termfontsize) + 2px); - line-height: 1.2; - - // we want to align to 2nd line of meta. that's 2xPad + 1xLineHightSm - // height of actions is 1xLineHeight + 8px (2x2px padding on icons) - top: calc(var(--termpad)); - right: calc(var(--termpad) * 2); - - display: flex; - flex-direction: row; - align-items: center; - - .cmdinput-icon { - display: inline-flex; - color: var(--app-icon-hover-color); - opacity: 0.5; - - .centered-icon { - .positional-icon-visible; - } - - &.running-cmds { - .rotate { - fill: var(--app-warning-color); - } - } - - &.active { - opacity: 1; - } - - &:hover { - opacity: 1; - } - - padding: 4px 6px; - cursor: pointer; - } - - .line-icon + .line-icon:not(.line-icon-shrink-left) { - margin-left: 3px; - } - } - - .cmdinput-titlebar { - position: absolute; - z-index: 22; - top: 0; - left: 0; - background-color: var(--app-panel-bg-color); - color: var(--term-blue); - padding: 6px 10px 6px 10px; - display: flex; - flex-direction: row; - width: 100%; - overflow-x: auto; - border-bottom: 1px solid var(--app-border-color); - font: var(--base-font); - - .title-icon { - margin-right: 10px; - } - - .title-string { - font-weight: bold; - } - - .close-button { - cursor: pointer; - i { - color: var(--app-icon-color); - - &:hover { - color: var(--app-icon-hover-color); - } - } - } - - .spacer { - flex: 0 0 10px; + &.has-history, + &.has-info { + .base-cmdinput { + border-top: 1px solid var(--app-border-color); } } @@ -135,22 +25,12 @@ padding-top: var(--termpad); } - .focus-indicator { - height: 90%; - top: 5%; - left: 4px; - } - &.has-history, &.has-aichat { padding-top: var(--termpad); height: max(300px, 70%); } - &.has-remote { - max-height: max(300px, 70%); - } - .remote-status-warning { display: flex; flex-direction: row; @@ -164,394 +44,153 @@ } } - .input-minmax-control { - position: absolute; - top: 5px; - right: 5px; - color: var(--term-foreground); - - padding: 5px; - cursor: pointer; - } - .cmd-input-grow-spacer { flex-grow: 1; } - .base-cmdinput:not(:first-child) { - margin-top: var(--termpad); - } - - .cmd-input-context { - color: var(--term-bright-white); - white-space: nowrap; - display: flex; - justify-content: space-between; - align-items: center; - font-family: var(--termfontfamily); - font-size: var(--termfontsize); - line-height: var(--termlineheight); - margin-left: 2px; - } - - .cmd-input-filter { - opacity: 0.5; - &:hover { - opacity: 1; - } - - .avatar { - display: inline-block; - width: 1em; - height: 1em; - margin: 0 0.5em; - vertical-align: text-top; - fill: var(--term-foreground); - } - - .warning { - fill: var(--term-yellow); - } - } - - .cmd-input-field { + .base-cmdinput { position: relative; - padding-right: var(--termpad); - font-family: var(--termfontfamily); - font-weight: normal; - line-height: var(--termlineheight); - font-size: var(--termfontsize); + // Rather than apply the padding to the whole container, we will apply it to the inner contents directly. + // This is more fragile, but allows us to capture a larger target area for the individual components. + --padding-top: var(--termpad); + --padding-sides: calc(var(--termpad) * 2); - .cmd-hints { - position: absolute; - bottom: -14px; - right: 0px; - } - .control { - padding: 1em 2px; - } - - .textareainput-div { - position: relative; - - &.control { - padding: var(--termpad) 0; - } - - .shelltag { - position: absolute; - // 13px = 10px height + 3px padding. subtract termpad to account for textareainput-div padding (2px not sure?) - bottom: calc(-13px + var(--termpad)); - right: 0px; - font-size: 10px; - color: var(--app-text-secondary-color); - line-height: 1; - padding: 1px 8px 3px 8px; - background-color: var(--cmdinput-textarea-bg-color); - border-radius: 0 0 5px 5px; - } - } - - textarea { - color: var(--app-text-primary-color); - background-color: var(--cmdinput-textarea-bg-color); - padding: var(--termpad); - resize: none; - overflow: auto; - overflow-wrap: anywhere; - border-color: transparent; - border: none; + .cmd-input-context { + color: var(--term-bright-white); + white-space: nowrap; + display: flex; + justify-content: space-between; + align-items: center; font-family: var(--termfontfamily); + font-size: var(--termfontsize); + line-height: var(--termlineheight); + + // We don't want to pad the bottom or it will push the input field down. + padding: var(--padding-top) var(--padding-sides) 0 var(--padding-sides); + margin-left: 2px; + } + + .cmd-input-field { + position: relative; + font-family: var(--termfontfamily); + font-weight: normal; line-height: var(--termlineheight); font-size: var(--termfontsize); - border-radius: 4px 4px 0 4px; // 0 is for shelltag + border: none; + cursor: text; - &.display-disabled { - background-color: #444; + // We don't want to pad the top or it will push the input field down. + padding: 0 var(--padding-sides) var(--padding-top) var(--padding-sides); + + .cmd-hints { + position: absolute; + bottom: -14px; + right: 0px; + } + .control { + padding: 1em 2px; } - &:focus { + .textareainput-div { + position: relative; + + &.control { + padding: var(--termpad) 0; + } + + .shelltag { + position: absolute; + // 13px = 10px height + 3px padding. subtract termpad to account for textareainput-div padding (2px not sure?) + bottom: calc(-13px + var(--termpad)); + right: 0; + font-size: 10px; + color: var(--app-text-secondary-color); + line-height: 1; + user-select: none; + } + } + + textarea { + color: var(--app-text-primary-color); + background-color: var(--app-bg-color); + padding: var(--termpad) 0; + resize: none; + overflow: auto; + overflow-wrap: anywhere; + font-family: var(--termfontfamily); + line-height: var(--termlineheight); + font-size: var(--termfontsize); border: none; + box-shadow: none; + } + + input.history-input { + border: 0; + padding: 0; + height: 0; + } + + .cmd-quick-context .button { + background-color: var(--app-bg-color) !important; + color: var(--app-text-color); + } + + &.inputmode-global .cmd-quick-context .button { + color: var(--app-bg-color); + background-color: var(--cmdinput-button-bg-color) !important; + } + + &.inputmode-comment .cmd-quick-context .button { + color: var(--app-bg-color); + background-color: var(--cmdinput-comment-button-bg-color) !important; } } - input.history-input { - border: 0; - padding: 0; - height: 0; - } - - .cmd-quick-context .button { - background-color: #000 !important; - color: var(--app-text-color); - } - - &.inputmode-global .cmd-quick-context .button { - color: var(--app-bg-color); - background-color: var(--cmdinput-button-bg-color) !important; - } - - &.inputmode-comment .cmd-quick-context .button { - color: var(--app-bg-color); - background-color: var(--cmdinput-comment-button-bg-color) !important; - } - - .cmd-exec { + .cmdinput-actions { position: absolute; - right: 0; - .icon { - vertical-align: bottom; - margin-right: 1em; - width: 2.5em; - height: 2.5em; - cursor: pointer; - border-radius: 50%; - fill: var(--app-accent-color); - padding: 0.25em; - } - .icon.disabled { - fill: var(--cmdinput-disabled-icon-color); - cursor: default; - } - .cmd-btn { - display: inline-block; - margin-right: 0; - padding: 0.2em 0.7rem; - border-radius: 4px; - vertical-align: super; + font-size: calc(var(--termfontsize) + 2px); + line-height: 1.2; - .hint-elem { - cursor: pointer; - opacity: 0.5; + // Align to the same bounds as the input field + top: var(--padding-top); + right: var(--padding-sides); - &:hover { - opacity: 1; + display: flex; + flex-direction: row; + align-items: center; + + .cmdinput-icon { + display: inline-flex; + color: var(--app-icon-hover-color); + opacity: 0.5; + + .centered-icon { + .positional-icon-visible; + } + + &.running-cmds { + .rotate { + fill: var(--app-warning-color); } } - } - } - } - .cmd-aichat { - display: flex; - justify-content: flex-end; - flex-flow: column nowrap; - margin-bottom: 10px; - flex-shrink: 1; - overflow-y: auto; - - .chat-window { - overflow-y: auto; - margin-bottom: 5px; - flex-shrink: 1; - flex-direction: column-reverse; - } - - .chat-textarea { - color: var(--app-text-primary-color); - background-color: var(--cmdinput-textarea-bg); - padding: 0.5em; - resize: none; - overflow: auto; - overflow-wrap: anywhere; - border-color: transparent; - border: none; - font-family: var(--termfontfamily); - font-weight: normal; - flex-shrink: 0; - flex-grow: 1; - border-radius: 4px; - - &:focus { - border: none; - outline: none; - } - } - - .chat-msg { - margin-top: calc(var(--termpad) * 2); - margin-bottom: calc(var(--termpad) * 2); - - .chat-msg-header { - display: flex; - margin-bottom: 2px; - - i { - margin-right: 0.5em; + &.active { + opacity: 1; } - .chat-username { - font-weight: bold; - margin-right: 5px; - } - } - } - - .chat-msg-assistant { - color: var(--app-text-color); - } - - .chat-msg-user { - .msg-text { - font-family: var(--markdown-font); - font-size: 14px; - white-space: pre-wrap; - } - } - - .chat-msg-error { - color: var(--cmdinput-text-error); - font-family: var(--markdown-font); - font-size: 14px; - } - - .grow-spacer { - flex: 1 0 10px; - } - } - - .cmd-history { - color: var(--app-text-color); - font-family: var(--termfontfamily); - font-size: var(--termfontsize); - overflow: auto; - flex-shrink: 1; - - .history-title { - div:first-child { - margin-left: var(--termpad); - } - - .history-opt { - white-space: nowrap; - } - - .history-clickable-opt { - cursor: pointer; - &:hover { - color: var(--app-text-primary-color); + opacity: 1; } - } - .history-clickable-opt { - white-space: nowrap; + // 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; cursor: pointer; } - } - .history-items { - color: var(--app-text-color); - - padding-bottom: 6px; - - .history-line { - white-space: pre; + .line-icon + .line-icon:not(.line-icon-shrink-left) { + margin-left: 3px; } - - .history-item.history-haderror { - color: var(--cmdinput-history-item-error-color); - } - - .history-line:first-child { - margin-left: 0 !important; - } - - .history-item { - padding-left: 5px; - cursor: pointer; - - &:hover { - background-color: #222; - } - } - - .history-item.is-selected { - font-weight: bold; - color: var(--app-text-primary-color); - background-color: #444; - } - - .history-item.is-selected.history-haderror { - color: var(--cmdinput-history-item-selected-error-color); - } - } - } - - .cmd-input-info { - flex-shrink: 1; - overflow-y: auto; - margin-bottom: var(--termpad); - font-family: var(--termfontfamily); - font-size: var(--termfontsize); - line-height: var(--termlineheight); - - .info-title { - position: absolute; - z-index: 102; - top: 5px; - left: 0; - font-size: calc(var(--termfontsize) + 2px); - background-color: var(--app-bg-color); - color: var(--term-blue); - padding-bottom: 4px; - padding-left: calc(var(--termpad) * 2); - display: flex; - flex-direction: row; - width: 100%; - overflow-x: auto; - border-bottom: 1px solid var(--app-border-color); - } - - .info-title + .info-msg, - .info-title + .info-lines, - .info-title + .info-comps, - .info-title + .info-error { - margin-top: 26px; - } - - .info-msg { - color: var(--term-blue); - padding-bottom: 2px; - - a { - color: var(--term-blue); - } - } - - .info-lines { - color: var(--app-text-color); - white-space: pre; - padding-bottom: 6px; - } - - .info-comps { - display: flex; - flex-direction: row; - flex-wrap: wrap; - padding-bottom: 5px; - font-weight: normal; - font-family: var(--termfontfamily); - font-size: var(--termfontsize); - - .info-comp { - min-width: 200px; - color: var(--term-foreground); - margin-right: 10px; - - &.has-space { - text-decoration: underline dotted #777; - } - } - - .metacmd-comp { - color: var(--term-bright-green); - } - } - - .info-error { - color: var(--cmdinput-text-error-color); - padding-bottom: 2px; } } } diff --git a/src/app/workspace/cmdinput/cmdinput.tsx b/src/app/workspace/cmdinput/cmdinput.tsx index 5588206e..6410f56a 100644 --- a/src/app/workspace/cmdinput/cmdinput.tsx +++ b/src/app/workspace/cmdinput/cmdinput.tsx @@ -61,12 +61,17 @@ class CmdInput extends React.Component<{}, {}> { } @boundMethod - cmdInputClick(e: any): void { + baseCmdInputClick(e: React.MouseEvent): void { if (this.promptRef.current != null) { if (this.promptRef.current.contains(e.target)) { return; } } + if ((e.target as HTMLDivElement).classList.contains("cmd-input-context")) { + e.stopPropagation(); + return; + } + GlobalModel.inputModel.setHistoryFocus(false); GlobalModel.inputModel.giveFocus(); } @@ -74,8 +79,12 @@ class CmdInput extends React.Component<{}, {}> { clickAIAction(e: any): void { e.preventDefault(); e.stopPropagation(); - let inputModel = GlobalModel.inputModel; - inputModel.openAIAssistantChat(); + const inputModel = GlobalModel.inputModel; + if (inputModel.aIChatShow.get()) { + inputModel.closeAIAssistantChat(true); + } else { + inputModel.openAIAssistantChat(); + } } @boundMethod @@ -160,6 +169,9 @@ class CmdInput extends React.Component<{}, {}> { } let shellInitMsg: string = null; let hidePrompt = false; + + const openView = inputModel.getOpenView(); + const hasOpenView = openView ? `has-${openView}` : null; if (ri == null) { let shellStr = "shell"; if (!util.isBlank(remote?.defaultshelltype)) { @@ -172,42 +184,7 @@ class CmdInput extends React.Component<{}, {}> { } } return ( -
-
- 0}> -
this.toggleFilter(screen)} - > - {numRunningLines}{" "} - - - -
-
-
- -
-
- -
-
+
@@ -252,7 +229,38 @@ class CmdInput extends React.Component<{}, {}> {
-
+
+
+ 0}> +
this.toggleFilter(screen)} + > + {numRunningLines}{" "} + + + +
+
+
+ +
+
+ +
+
diff --git a/src/app/workspace/cmdinput/historyinfo.less b/src/app/workspace/cmdinput/historyinfo.less new file mode 100644 index 00000000..88252045 --- /dev/null +++ b/src/app/workspace/cmdinput/historyinfo.less @@ -0,0 +1,60 @@ +.cmd-history { + color: var(--app-text-color); + font-family: var(--termfontfamily); + font-size: var(--termfontsize); + + .auxview-titlebar { + .history-opt { + white-space: nowrap; + } + + .history-clickable-opt { + cursor: pointer; + white-space: nowrap; + cursor: pointer; + + &:hover { + color: var(--app-text-primary-color); + } + } + } + + .history-items { + color: var(--app-text-color); + width: 100%; + + .history-item { + cursor: pointer; + border-radius: 5px; + + .history-line { + white-space: pre; + + &:first-child { + margin-left: 0 !important; + } + } + + &:hover { + background-color: var(--table-tr-hover-bg-color); + } + + &.history-haderror { + color: var(--cmdinput-history-item-error-color); + } + + &.is-selected { + font-weight: bold; + color: var(--app-text-primary-color); + background-color: var(--table-tr-selected-bg-color); + &:hover { + background-color: var(--table-tr-selected-hover-bg-color); + } + } + + &.is-selected.history-haderror { + color: var(--cmdinput-history-item-selected-error-color); + } + } + } +} diff --git a/src/app/workspace/cmdinput/historyinfo.tsx b/src/app/workspace/cmdinput/historyinfo.tsx index 9481f322..e188139b 100644 --- a/src/app/workspace/cmdinput/historyinfo.tsx +++ b/src/app/workspace/cmdinput/historyinfo.tsx @@ -13,6 +13,9 @@ import localizedFormat from "dayjs/plugin/localizedFormat"; import { GlobalModel } from "@/models"; import { isBlank } from "@/util/util"; +import "./historyinfo.less"; +import { AuxiliaryCmdView } from "./auxview"; + dayjs.extend(localizedFormat); const TDots = "⋮"; @@ -43,7 +46,7 @@ class HItem extends React.Component< if (hitem.remote == null || isBlank(hitem.remote.remoteid)) { return sprintf("%-15s ", ""); } - let r = GlobalModel.getRemote(hitem.remote.remoteid); + const r = GlobalModel.getRemote(hitem.remote.remoteid); if (r == null) { return sprintf("%-15s ", "???"); } @@ -71,15 +74,15 @@ class HItem extends React.Component< if (!opts.limitRemote) { remoteStr = this.renderRemote(hitem); } - let selectedStr = isSelected ? "*" : " "; - let lineNumStr = hitem.linenum > 0 ? "(" + hitem.linenum + ")" : ""; + const selectedStr = isSelected ? "*" : " "; + const lineNumStr = hitem.linenum > 0 ? "(" + hitem.linenum + ")" : ""; if (isBlank(opts.queryType) || opts.queryType == "screen") { return selectedStr + sprintf("%7s", lineNumStr) + " " + remoteStr; } if (opts.queryType == "session") { let screenStr = ""; if (!isBlank(hitem.screenid)) { - let scrName = scrNames[hitem.screenid]; + const scrName = scrNames[hitem.screenid]; if (scrName != null) { screenStr = "[" + truncateWithTDots(scrName, 15) + "]"; } @@ -89,19 +92,18 @@ class HItem extends React.Component< if (opts.queryType == "global") { let sessionStr = ""; if (!isBlank(hitem.sessionid)) { - let sessionName = snames[hitem.sessionid]; + const sessionName = snames[hitem.sessionid]; if (sessionName != null) { sessionStr = "#" + truncateWithTDots(sessionName, 15); } } let screenStr = ""; if (!isBlank(hitem.screenid)) { - let scrName = scrNames[hitem.screenid]; + const scrName = scrNames[hitem.screenid]; if (scrName != null) { screenStr = "[" + truncateWithTDots(scrName, 13) + "]"; } } - let ssStr = sessionStr + screenStr; return ( selectedStr + sprintf("%15s ", sessionStr) + @@ -116,12 +118,12 @@ class HItem extends React.Component< } render() { - let { hitem, isSelected, opts, snames, scrNames } = this.props; - let lines = hitem.cmdstr.split("\n"); + const { hitem, isSelected, opts, snames, scrNames } = this.props; + const lines = hitem.cmdstr.split("\n"); let line: string = ""; let idx = 0; - let infoText = this.renderHInfoText(hitem, opts, isSelected, snames, scrNames); - let infoTextSpacer = sprintf("%" + infoText.length + "s", ""); + const infoText = this.renderHInfoText(hitem, opts, isSelected, snames, scrNames); + const infoTextSpacer = sprintf("%" + infoText.length + "s", ""); return (
{ containingText: mobx.IObservableValue = mobx.observable.box(""); componentDidMount() { - let inputModel = GlobalModel.inputModel; + const inputModel = GlobalModel.inputModel; let hitem = inputModel.getHistorySelectedItem(); if (hitem == null) { hitem = inputModel.getFirstHistoryItem(); @@ -170,15 +172,15 @@ class HistoryInfo extends React.Component<{}, {}> { @boundMethod handleItemClick(hitem: HistoryItem) { - let inputModel = GlobalModel.inputModel; - let selItem = inputModel.getHistorySelectedItem(); + const inputModel = GlobalModel.inputModel; + const selItem = inputModel.getHistorySelectedItem(); + inputModel.setHistoryFocus(true); if (this.lastClickHNum == hitem.historynum && selItem != null && selItem.historynum == hitem.historynum) { inputModel.grabSelectedHistoryItem(); return; } - inputModel.giveFocus(); inputModel.setHistorySelectionNum(hitem.historynum); - let now = Date.now(); + const now = Date.now(); this.lastClickHNum = hitem.historynum; this.lastClickTs = now; setTimeout(() => { @@ -191,24 +193,41 @@ class HistoryInfo extends React.Component<{}, {}> { @boundMethod handleClickType() { - let inputModel = GlobalModel.inputModel; + const inputModel = GlobalModel.inputModel; + inputModel.setHistoryFocus(true); inputModel.toggleHistoryType(); } @boundMethod handleClickRemote() { - let inputModel = GlobalModel.inputModel; + const inputModel = GlobalModel.inputModel; + inputModel.setHistoryFocus(true); inputModel.toggleRemoteType(); } + @boundMethod + getTitleBarContents(): React.ReactElement[] { + const opts = GlobalModel.inputModel.historyQueryOpts.get(); + + return [ +
+ [for {opts.queryType} ⌘S] +
, +
+ [containing '{opts.queryStr}'] +
, +
+ [{opts.limitRemote ? "this" : "any"} remote ⌘R] +
, + ]; + } + render() { - let inputModel = GlobalModel.inputModel; - let idx: number = 0; - let selItem = inputModel.getHistorySelectedItem(); - let hitems = inputModel.getFilteredHistoryItems(); - hitems = hitems.slice().reverse(); + const inputModel = GlobalModel.inputModel; + const selItem = inputModel.getHistorySelectedItem(); + const hitems = inputModel.getFilteredHistoryItems().slice().reverse(); + const opts = inputModel.historyQueryOpts.get(); let hitem: HistoryItem = null; - let opts = inputModel.historyQueryOpts.get(); let snames: Record = {}; let scrNames: Record = {}; if (opts.queryType == "global") { @@ -218,29 +237,13 @@ class HistoryInfo extends React.Component<{}, {}> { scrNames = GlobalModel.getScreenNames(); } return ( -
-
-
- -
-
History
-
-
- [for {opts.queryType} ⌘S] -
-
-
- [containing '{opts.queryStr}'] -
-
-
- [{opts.limitRemote ? "this" : "any"} remote ⌘R] -
-
-
- -
-
+
{ { "show-sessions": opts.queryType == "global" } )} > -
[no history] 0}> @@ -264,7 +266,7 @@ class HistoryInfo extends React.Component<{}, {}> {
-
+
); } } diff --git a/src/app/workspace/cmdinput/infomsg.less b/src/app/workspace/cmdinput/infomsg.less new file mode 100644 index 00000000..4e08f5b9 --- /dev/null +++ b/src/app/workspace/cmdinput/infomsg.less @@ -0,0 +1,49 @@ +.cmd-input-info { + font-family: var(--termfontfamily); + font-size: var(--termfontsize); + line-height: var(--termlineheight); + + .info-msg { + color: var(--term-blue); + padding-bottom: 2px; + + a { + color: var(--term-blue); + } + } + + .info-lines { + color: var(--app-text-color); + white-space: pre; + padding-bottom: 6px; + } + + .info-comps { + display: flex; + flex-direction: row; + flex-wrap: wrap; + padding-bottom: 5px; + font-weight: normal; + font-family: var(--termfontfamily); + font-size: var(--termfontsize); + + .info-comp { + min-width: 200px; + color: var(--term-foreground); + margin-right: 10px; + + &.has-space { + text-decoration: underline dotted #777; + } + } + + .metacmd-comp { + color: var(--term-bright-green); + } + } + + .info-error { + color: var(--cmdinput-text-error-color); + padding-bottom: 2px; + } +} diff --git a/src/app/workspace/cmdinput/infomsg.tsx b/src/app/workspace/cmdinput/infomsg.tsx index 95b217a7..6c21864e 100644 --- a/src/app/workspace/cmdinput/infomsg.tsx +++ b/src/app/workspace/cmdinput/infomsg.tsx @@ -9,6 +9,9 @@ import dayjs from "dayjs"; import localizedFormat from "dayjs/plugin/localizedFormat"; import { GlobalModel } from "@/models"; import * as appconst from "@/app/appconst"; +import { AuxiliaryCmdView } from "./auxview"; + +import "./infomsg.less"; dayjs.extend(localizedFormat); @@ -40,29 +43,22 @@ class InfoMsg extends React.Component<{}, {}> { } render() { - let model = GlobalModel; - let inputModel = model.inputModel; - let infoMsg = inputModel.infoMsg.get(); - let infoShow = inputModel.infoShow.get(); + const inputModel = GlobalModel.inputModel; + const infoMsg: InfoType = inputModel.infoMsg.get(); + const infoShow: boolean = inputModel.infoShow.get(); let line: string = null; let istr: string = null; let idx: number = 0; let titleStr = null; - let remoteEditKey = "inforemoteedit"; if (infoMsg != null) { titleStr = infoMsg.infotitle; } - let activeScreen = model.getActiveScreen(); if (!infoShow) { return null; } + return ( -
- -
- {titleStr} -
-
+
@@ -108,7 +104,7 @@ class InfoMsg extends React.Component<{}, {}> {
to reset, run: /reset:cwd
-
+
); } } diff --git a/src/app/workspace/cmdinput/textareainput.tsx b/src/app/workspace/cmdinput/textareainput.tsx index 4b1dd820..db1575d6 100644 --- a/src/app/workspace/cmdinput/textareainput.tsx +++ b/src/app/workspace/cmdinput/textareainput.tsx @@ -287,7 +287,7 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: () setFocus(): void { const inputModel = GlobalModel.inputModel; - if (inputModel.historyShow.get()) { + if (inputModel.historyFocus.get()) { this.historyInputRef.current.focus(); } else { this.mainInputRef.current.focus(); @@ -551,7 +551,7 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: () @boundMethod handleMainFocus(e: any) { const inputModel = GlobalModel.inputModel; - if (inputModel.historyShow.get()) { + if (inputModel.historyFocus.get()) { e.preventDefault(); if (this.historyInputRef.current != null) { this.historyInputRef.current.focus(); @@ -578,7 +578,7 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: () @boundMethod handleHistoryFocus(e: any) { const inputModel = GlobalModel.inputModel; - if (!inputModel.historyShow.get()) { + if (!inputModel.historyFocus.get()) { e.preventDefault(); if (this.mainInputRef.current != null) { this.mainInputRef.current.focus(); @@ -616,7 +616,9 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: () if (numLines > 1 || longLine || inputModel.inputExpanded.get()) { displayLines = 5; } - const disabled = inputModel.historyShow.get(); + + // TODO: invert logic here. We should track focus on the main textarea and assume aux view is focused if not. + const disabled = inputModel.historyFocus.get(); if (disabled) { displayLines = 1; } @@ -661,7 +663,7 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: () - +
{shellType}
= mobx.observable.box(false); + historyFocus: OV = mobx.observable.box(false); infoShow: OV = mobx.observable.box(false); aIChatShow: OV = mobx.observable.box(false); cmdInputHeight: OV = mobx.observable.box(0); @@ -92,7 +93,7 @@ class InputModel { } toggleHistoryType(): void { - let opts = mobx.toJS(this.historyQueryOpts.get()); + const opts = mobx.toJS(this.historyQueryOpts.get()); let htype = opts.queryType; if (htype == "screen") { htype = "session"; @@ -105,7 +106,7 @@ class InputModel { } toggleRemoteType(): void { - let opts = mobx.toJS(this.historyQueryOpts.get()); + const opts = mobx.toJS(this.historyQueryOpts.get()); if (opts.limitRemote) { opts.limitRemote = false; opts.limitRemoteInstance = false; @@ -139,21 +140,21 @@ class InputModel { } _focusCmdInput(): void { - let elem = document.getElementById("main-cmd-input"); + const elem = document.getElementById("main-cmd-input"); if (elem != null) { elem.focus(); } } _focusHistoryInput(): void { - let elem: HTMLElement = document.querySelector(".cmd-input input.history-input"); + const elem: HTMLElement = document.querySelector(".cmd-input input.history-input"); if (elem != null) { elem.focus(); } } giveFocus(): void { - if (this.historyShow.get()) { + if (this.historyFocus.get()) { this._focusHistoryInput(); } else { this._focusCmdInput(); @@ -165,7 +166,7 @@ class InputModel { this.physicalInputFocused.set(isFocused); })(); if (isFocused) { - let screen = this.globalModel.getActiveScreen(); + const screen = this.globalModel.getActiveScreen(); if (screen != null) { if (screen.focusType.get() != "input") { GlobalCommandRunner.screenSetFocus("input"); @@ -175,11 +176,11 @@ class InputModel { } hasFocus(): boolean { - let mainInputElem = document.getElementById("main-cmd-input"); + const mainInputElem = document.getElementById("main-cmd-input"); if (document.activeElement == mainInputElem) { return true; } - let historyInputElem = document.querySelector(".cmd-input input.history-input"); + const historyInputElem = document.querySelector(".cmd-input input.history-input"); if (document.activeElement == historyInputElem) { return true; } @@ -190,6 +191,19 @@ class InputModel { return false; } + getOpenView(): string { + if (this.historyShow.get()) { + return "history"; + } + if (this.aIChatShow.get()) { + return "aichat"; + } + if (this.infoShow.get()) { + return "info"; + } + return null; + } + setHistoryType(htype: HistoryTypeStrs): void { if (this.historyQueryOpts.get().queryType == htype) { return; @@ -201,20 +215,19 @@ class InputModel { if (oldItem == null) { return 0; } - let newItems = this.getFilteredHistoryItems(); + const newItems = this.getFilteredHistoryItems(); if (newItems.length == 0) { return 0; } let bestIdx = 0; - for (let i = 0; i < newItems.length; i++) { + for (const [i, item] of newItems.entries()) { // still start at i=0 to catch the historynum equality case - let item = newItems[i]; if (item.historynum == oldItem.historynum) { bestIdx = i; break; } - let bestTsDiff = Math.abs(item.ts - newItems[bestIdx].ts); - let curTsDiff = Math.abs(item.ts - oldItem.ts); + const bestTsDiff = Math.abs(item.ts - newItems[bestIdx].ts); + const curTsDiff = Math.abs(item.ts - oldItem.ts); if (curTsDiff < bestTsDiff) { bestIdx = i; } @@ -224,9 +237,9 @@ class InputModel { setHistoryQueryOpts(opts: HistoryQueryOpts): void { mobx.action(() => { - let oldItem = this.getHistorySelectedItem(); + const oldItem = this.getHistorySelectedItem(); this.historyQueryOpts.set(opts); - let bestIndex = this.findBestNewIndex(oldItem); + const bestIndex = this.findBestNewIndex(oldItem); setTimeout(() => this.setHistoryIndex(bestIndex, true), 10); })(); } @@ -253,17 +266,28 @@ class InputModel { this.setInputPopUpType("none"); } this.historyShow.set(show); + this.historyFocus.set(show); if (this.hasFocus()) { this.giveFocus(); } })(); } + setHistoryFocus(focus: boolean): void { + if (this.historyFocus.get() == focus) { + return; + } + mobx.action(() => { + this.historyFocus.set(focus); + this.giveFocus(); + })(); + } + isHistoryLoaded(): boolean { if (this.historyLoading.get()) { return false; } - let hitems = this.historyItems.get(); + const hitems = this.historyItems.get(); return hitems != null; } @@ -294,6 +318,7 @@ class InputModel { if (!this.historyShow.get()) { mobx.action(() => { this.setHistoryShow(true); + this.aIChatShow.set(false); this.infoShow.set(false); this.dropModHistory(true); this.giveFocus(); @@ -311,11 +336,11 @@ class InputModel { } getHistorySelectedItem(): HistoryItem { - let hidx = this.historyIndex.get(); + const hidx = this.historyIndex.get(); if (hidx == 0) { return null; } - let hitems = this.getFilteredHistoryItems(); + const hitems = this.getFilteredHistoryItems(); if (hidx > hitems.length) { return null; } @@ -323,7 +348,7 @@ class InputModel { } getFirstHistoryItem(): HistoryItem { - let hitems = this.getFilteredHistoryItems(); + const hitems = this.getFilteredHistoryItems(); if (hitems.length == 0) { return null; } @@ -331,9 +356,9 @@ class InputModel { } setHistorySelectionNum(hnum: string): void { - let hitems = this.getFilteredHistoryItems(); - for (let i = 0; i < hitems.length; i++) { - if (hitems[i].historynum == hnum) { + const hitems = this.getFilteredHistoryItems(); + for (const [i, hitem] of hitems.entries()) { + if (hitem.historynum == hnum) { this.setHistoryIndex(i + 1); return; } @@ -342,8 +367,8 @@ class InputModel { setHistoryInfo(hinfo: HistoryInfoType): void { mobx.action(() => { - let oldItem = this.getHistorySelectedItem(); - let hitems: HistoryItem[] = hinfo.items ?? []; + const oldItem = this.getHistorySelectedItem(); + const hitems: HistoryItem[] = hinfo.items ?? []; this.historyItems.set(hitems); this.historyLoading.set(false); this.historyQueryOpts.get().queryType = hinfo.historytype; @@ -352,7 +377,7 @@ class InputModel { this.historyQueryOpts.get().limitRemoteInstance = false; } if (this.historyAfterLoadIndex == -1) { - let bestIndex = this.findBestNewIndex(oldItem); + const bestIndex = this.findBestNewIndex(oldItem); setTimeout(() => this.setHistoryIndex(bestIndex, true), 100); } else if (this.historyAfterLoadIndex) { if (hitems.length >= this.historyAfterLoadIndex) { @@ -371,10 +396,10 @@ class InputModel { } _getFilteredHistoryItems(): HistoryItem[] { - let hitems: HistoryItem[] = this.historyItems.get() ?? []; - let rtn: HistoryItem[] = []; - let opts = mobx.toJS(this.historyQueryOpts.get()); - let ctx = this.globalModel.getUIContext(); + const hitems: HistoryItem[] = this.historyItems.get() ?? []; + const rtn: HistoryItem[] = []; + const opts: HistoryQueryOpts = mobx.toJS(this.historyQueryOpts.get()); + const ctx = this.globalModel.getUIContext(); let curRemote: RemotePtrType = ctx.remote; if (curRemote == null) { curRemote = { ownerid: "", name: "", remoteid: "" }; @@ -411,7 +436,7 @@ class InputModel { if (isBlank(hitem.cmdstr)) { continue; } - let idx = hitem.cmdstr.indexOf(opts.queryStr); + const idx = hitem.cmdstr.indexOf(opts.queryStr); if (idx == -1) { continue; } @@ -423,24 +448,24 @@ class InputModel { } scrollHistoryItemIntoView(hnum: string): void { - let elem: HTMLElement = document.querySelector(".cmd-history .hnum-" + hnum); + const elem: HTMLElement = document.querySelector(".cmd-history .hnum-" + hnum); if (elem == null) { return; } - let historyDiv = elem.closest(".cmd-history"); + const historyDiv = elem.closest(".cmd-history"); if (historyDiv == null) { return; } - let buffer = 15; + const buffer = 15; let titleHeight = 24; - let titleDiv: HTMLElement = document.querySelector(".cmd-history .history-title"); + const titleDiv: HTMLElement = document.querySelector(".cmd-history .history-title"); if (titleDiv != null) { titleHeight = titleDiv.offsetHeight + 2; } - let elemOffset = elem.offsetTop; - let elemHeight = elem.clientHeight; - let topPos = historyDiv.scrollTop; - let endPos = topPos + historyDiv.clientHeight; + const elemOffset = elem.offsetTop; + const elemHeight = elem.clientHeight; + const topPos = historyDiv.scrollTop; + const endPos = topPos + historyDiv.clientHeight; if (elemOffset + elemHeight + buffer > endPos) { if (elemHeight + buffer > historyDiv.clientHeight - titleHeight) { historyDiv.scrollTop = elemOffset - titleHeight; @@ -459,7 +484,7 @@ class InputModel { } grabSelectedHistoryItem(): void { - let hitem = this.getHistorySelectedItem(); + const hitem = this.getHistorySelectedItem(); if (hitem == null) { this.resetHistory(); return; @@ -498,9 +523,8 @@ class InputModel { if (!this.isHistoryLoaded()) { return; } - let hitems = this.getFilteredHistoryItems(); - let idx = this.historyIndex.get(); - idx += amt; + const hitems = this.getFilteredHistoryItems(); + let idx = this.historyIndex.get() + amt; if (idx < 0) { idx = 0; } @@ -540,7 +564,6 @@ class InputModel { } setAIChatFocus() { - console.log("setting ai chat focus"); if (this.aiChatTextAreaRef?.current != null) { this.aiChatTextAreaRef.current.focus(); } @@ -551,9 +574,8 @@ class InputModel { this.codeSelectSelectedIndex.get() >= 0 && this.codeSelectSelectedIndex.get() < this.codeSelectBlockRefArray.length ) { - let curBlockRef = this.codeSelectBlockRefArray[this.codeSelectSelectedIndex.get()]; - let codeText = curBlockRef.current.innerText; - codeText = codeText.replace(/\n$/, ""); // remove trailing newline + const curBlockRef = this.codeSelectBlockRefArray[this.codeSelectSelectedIndex.get()]; + const codeText = curBlockRef.current.innerText.replace(/\n$/, ""); // remove trailing newline this.setCurLine(codeText); this.giveFocus(); } @@ -574,23 +596,21 @@ class InputModel { mobx.action(() => { if (blockIndex >= 0 && blockIndex < this.codeSelectBlockRefArray.length) { this.codeSelectSelectedIndex.set(blockIndex); - let currentRef = this.codeSelectBlockRefArray[blockIndex].current; - if (currentRef != null) { - if (this.aiChatWindowRef?.current != null) { - let chatWindowTop = this.aiChatWindowRef.current.scrollTop; - let chatWindowBottom = chatWindowTop + this.aiChatWindowRef.current.clientHeight - 100; - let elemTop = currentRef.offsetTop; - let elemBottom = elemTop - currentRef.offsetHeight; - let elementIsInView = elemBottom < chatWindowBottom && elemTop > chatWindowTop; - if (!elementIsInView) { - this.aiChatWindowRef.current.scrollTop = - elemBottom - this.aiChatWindowRef.current.clientHeight / 3; - } + const currentRef = this.codeSelectBlockRefArray[blockIndex].current; + if (currentRef != null && this.aiChatWindowRef?.current != null) { + const chatWindowTop = this.aiChatWindowRef.current.scrollTop; + const chatWindowBottom = chatWindowTop + this.aiChatWindowRef.current.clientHeight - 100; + const elemTop = currentRef.offsetTop; + let elemBottom = elemTop - currentRef.offsetHeight; + const elementIsInView = elemBottom < chatWindowBottom && elemTop > chatWindowTop; + if (!elementIsInView) { + this.aiChatWindowRef.current.scrollTop = + elemBottom - this.aiChatWindowRef.current.clientHeight / 3; } } - this.codeSelectBlockRefArray = []; - this.setAIChatFocus(); } + this.codeSelectBlockRefArray = []; + this.setAIChatFocus(); })(); } @@ -603,7 +623,7 @@ class InputModel { } else if (this.codeSelectSelectedIndex.get() == this.codeSelectBottom) { return; } - let incBlockIndex = this.codeSelectSelectedIndex.get() + 1; + const incBlockIndex = this.codeSelectSelectedIndex.get() + 1; if (this.codeSelectSelectedIndex.get() == this.codeSelectBlockRefArray.length - 1) { this.codeSelectDeselectAll(); if (this.aiChatWindowRef?.current != null) { @@ -627,7 +647,7 @@ class InputModel { } else if (this.codeSelectSelectedIndex.get() == this.codeSelectTop) { return; } - let decBlockIndex = this.codeSelectSelectedIndex.get() - 1; + const decBlockIndex = this.codeSelectSelectedIndex.get() - 1; if (decBlockIndex < 0) { this.codeSelectDeselectAll(this.codeSelectTop); if (this.aiChatWindowRef?.current != null) { @@ -666,6 +686,8 @@ class InputModel { mobx.action(() => { this.setInputPopUpType("aichat"); this.aIChatShow.set(true); + this.historyShow.set(false); + this.infoShow.set(false); this.setAIChatFocus(); })(); } @@ -686,7 +708,7 @@ class InputModel { } clearAIAssistantChat(): void { - let prtn = this.globalModel.submitChatInfoCommand("", "", true); + const prtn = this.globalModel.submitChatInfoCommand("", "", true); prtn.then((rtn) => { if (!rtn.success) { console.log("submit chat command error: " + rtn.error); @@ -700,11 +722,11 @@ class InputModel { if (!this.infoShow.get()) { return false; } - let info = this.infoMsg.get(); + const info = this.infoMsg.get(); if (info == null) { return false; } - let div = document.querySelector(".cmd-input-info"); + const div = document.querySelector(".cmd-input-info"); if (div == null) { return false; } @@ -736,7 +758,7 @@ class InputModel { this.setHistoryShow(false); return; } - let isShowing = this.infoShow.get(); + const isShowing = this.infoShow.get(); if (isShowing) { this.infoShow.set(false); } else { @@ -750,7 +772,7 @@ class InputModel { @boundMethod uiSubmitCommand(): void { mobx.action(() => { - let commandStr = this.getCurLine(); + const commandStr = this.getCurLine(); if (commandStr.trim() == "") { return; } @@ -771,7 +793,7 @@ class InputModel { } setCurLine(val: string): void { - let hidx = this.historyIndex.get(); + const hidx = this.historyIndex.get(); mobx.action(() => { if (this.modHistory.length <= hidx) { this.modHistory.length = hidx + 1; @@ -803,15 +825,15 @@ class InputModel { } getCurLine(): string { - let hidx = this.historyIndex.get(); + const hidx = this.historyIndex.get(); if (hidx < this.modHistory.length && this.modHistory[hidx] != null) { return this.modHistory[hidx]; } - let hitems = this.getFilteredHistoryItems(); + const hitems = this.getFilteredHistoryItems(); if (hidx == 0 || hitems == null || hidx > hitems.length) { return ""; } - let hitem = hitems[hidx - 1]; + const hitem = hitems[hidx - 1]; if (hitem == null) { return ""; } diff --git a/src/util/textmeasure.ts b/src/util/textmeasure.ts index 0d7c24c2..5254dc2f 100644 --- a/src/util/textmeasure.ts +++ b/src/util/textmeasure.ts @@ -23,7 +23,7 @@ function getMonoFontSize(fontSize: number): MonoFontSize { if (MonoFontSizes[fontSize] != null) { return MonoFontSizes[fontSize]; } - let size = measureText("W", { pre: true, mono: true, fontSize: fontSize }); + const size = measureText("W", { pre: true, mono: true, fontSize: fontSize }); if (size.height != 0 && size.width != 0) { MonoFontSizes[fontSize] = size; } @@ -38,7 +38,7 @@ function measureText(text: string, textOpts: { pre?: boolean; mono?: boolean; fo if (textOpts == null) { throw new Error("invalid textOpts passed to measureText (null)"); } - let textElem = document.createElement("span"); + const textElem = document.createElement("span"); if (textOpts.pre) { textElem.classList.add("pre"); } @@ -53,19 +53,19 @@ function measureText(text: string, textOpts: { pre?: boolean; mono?: boolean; fo } } textElem.innerText = text; - let measureDiv = document.getElementById("measure"); + const measureDiv = document.getElementById("measure"); if (measureDiv == null) { throw new Error("cannot measure text, no #measure div"); } measureDiv.replaceChildren(textElem); - let height = Math.ceil(textElem.offsetHeight); - let width = textElem.offsetWidth; - let pad = Math.floor(height / 2); + const height = Math.ceil(textElem.offsetHeight); + const width = textElem.offsetWidth; + const pad = Math.floor(height / 2); return { width, height, pad, fontSize: textOpts.fontSize }; } function windowWidthToCols(width: number, fontSize: number): number { - let dr = getMonoFontSize(fontSize); + const dr = getMonoFontSize(fontSize); let cols = Math.trunc((width - MagicLayout.ScreenMaxContentWidthBuffer) / dr.width) - 1; cols = boundInt(cols, MinTermCols, MaxTermCols); return cols; @@ -80,7 +80,7 @@ function windowHeightToRows(lhe: LineHeightEnv, height: number): number { } function termWidthFromCols(cols: number, fontSize: number): number { - let dr = getMonoFontSize(fontSize); + const dr = getMonoFontSize(fontSize); return Math.ceil(dr.width * cols) + MagicLayout.TermWidthBuffer; } @@ -89,12 +89,12 @@ function termWidthFromCols(cols: number, fontSize: number): number { // works out to `realHeight = round(ceil(height * dpr) * rows / dpr) / rows` // their calculation is based off the "totalRows" (so that argument has been added) function termHeightFromRows(rows: number, fontSize: number, totalRows: number): number { - let dr = getMonoFontSize(fontSize); + const dr = getMonoFontSize(fontSize); const dpr = window.devicePixelRatio; if (totalRows == null || totalRows == 0) { totalRows = rows > 25 ? rows : 25; } - let realHeight = Math.round((Math.ceil(dr.height * dpr) * totalRows) / dpr) / totalRows; + const realHeight = Math.round((Math.ceil(dr.height * dpr) * totalRows) / dpr) / totalRows; return Math.ceil(realHeight * rows); }