diff --git a/.gitattributes b/.gitattributes index fbd75d33c..212566614 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1 @@ -* text=lf \ No newline at end of file +* text=auto \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md deleted file mode 100644 index 3db62b1a9..000000000 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -name: Bug Report -about: Create a bug report to help us improve -title: '' -labels: '' -assignees: '' - ---- - -**Describe the bug** -A clear and concise description of what the bug is. - -**To Reproduce** -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Screenshots** -If applicable, add screenshots to help explain your problem. - -**Desktop (please complete the following information):** - - OS: [e.g. MacOS/Linux, x64 or arm64] - - Version [e.g. v0.5.0] - -**Additional context** -Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml new file mode 100644 index 000000000..3de761fb7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -0,0 +1,87 @@ +name: Bug Report +description: Create a bug report to help us improve. +title: "[Bug]: " +labels: ["bug", "triage"] +body: + - type: markdown + attributes: + value: | + ## Bug description + - type: textarea + attributes: + label: Current Behavior + description: A concise description of what you're experiencing. + validations: + required: true + - type: textarea + attributes: + label: Expected Behavior + description: A concise description of what you expected to happen. + validations: + required: true + - type: textarea + attributes: + label: Steps To Reproduce + description: Steps to reproduce the behavior. + placeholder: | + 1. In this environment... + 2. With this config... + 3. Run '...' + 4. See error... + validations: + required: true + + - type: markdown + attributes: + value: | + ## Environment details + + We require that you provide us the version of Wave you're running so we can track issues across versions. To find the Wave version, go to the app menu (this always visible on macOS, for Windows and Linux, click the `...` button) and navigate to `Wave -> About Wave Terminal`. This will bring up the About modal. Copy the client version and paste it below. + - type: input + attributes: + label: Wave Version + description: The version of Wave you are running + placeholder: v0.8.8 + validations: + required: true + - type: input + attributes: + label: OS + description: The name and version of the operating system of the computer where you are running Wave + placeholder: macOS 15.0 + validations: + required: false + - type: dropdown + attributes: + label: Architecture + description: The architecture of the computer where you are running Wave + options: + - arm64 + - x64 + validations: + required: false + + - type: markdown + attributes: + value: | + ## Extra details + - type: textarea + attributes: + label: Anything else? + description: | + Links? References? Anything that will give us more context about the issue you are encountering! + + Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in. + validations: + required: false + - type: checkboxes + attributes: + label: Questionnaire + description: "If you feel up to the challenge, please check one of the boxes below:" + options: + - label: I'm interested in fixing this myself but don't know where to start + required: false + - label: I would like to fix and I have a solution + required: false + - label: I don't have time to fix this right now, but maybe later + required: false diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md deleted file mode 100644 index 9eb042ae7..000000000 --- a/.github/ISSUE_TEMPLATE/feature-request.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -name: Feature Request -about: Suggest a new idea for this project -title: '' -labels: '' -assignees: '' - ---- - -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -**Describe the solution you'd like** -A clear and concise description of what you want to happen. diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml new file mode 100644 index 000000000..75f0b4652 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -0,0 +1,26 @@ +name: Feature Request +description: Suggest a new idea for this project. +title: "[Feature]: " +labels: ["enhancement", "triage"] +body: + - type: textarea + attributes: + label: Feature description + description: Describe the issue in detail and why we should add it. To help us out, please poke through our issue tracker and make sure it's not a duplicate issue. Ex. As a user, I can do [...] + validations: + required: true + - type: textarea + attributes: + label: Implementation Suggestion + description: If you have any suggestions on how to design this feature, list them here. + validations: + required: false + - type: textarea + attributes: + label: Anything else? + description: | + Links? References? Anything that will give us more context about how to deliver your feature! + + Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in. + validations: + required: false diff --git a/electron-builder.config.cjs b/electron-builder.config.cjs index ca953adda..c5cfdc26b 100644 --- a/electron-builder.config.cjs +++ b/electron-builder.config.cjs @@ -66,6 +66,7 @@ const config = { Keywords: "developer;terminal;emulator;", category: "Development;Utility;", }, + executableArgs: ["--enable-features", "UseOzonePlatform", "--ozone-platform-hint", "auto"], // Hint Electron to use Ozone abstraction layer for native Wayland support }, deb: { afterInstall: "build/deb-postinstall.tpl", diff --git a/frontend/app/store/keymodel.ts b/frontend/app/store/keymodel.ts index 0aafbe174..048786f2e 100644 --- a/frontend/app/store/keymodel.ts +++ b/frontend/app/store/keymodel.ts @@ -16,6 +16,13 @@ import * as jotai from "jotai"; const simpleControlShiftAtom = jotai.atom(false); const globalKeyMap = new Map boolean>(); +function getFocusedBlockInActiveTab() { + const activeTabId = globalStore.get(atoms.activeTabId); + const layoutModel = getLayoutModelForTabById(activeTabId); + const focusedNode = globalStore.get(layoutModel.focusedNode); + return focusedNode.data?.blockId; +} + function getSimpleControlShiftAtom() { return simpleControlShiftAtom; } @@ -161,12 +168,6 @@ function appHandleKeyDown(waveEvent: WaveKeyboardEvent): boolean { const blockId = focusedNode?.data?.blockId; if (blockId != null && shouldDispatchToBlock(waveEvent)) { const bcm = getBlockComponentModel(blockId); - if (bcm.openSwitchConnection != null) { - if (keyutil.checkKeyPressed(waveEvent, "Cmd:g")) { - bcm.openSwitchConnection(); - return true; - } - } const viewModel = bcm?.viewModel; if (viewModel?.keyDownHandler) { const handledByBlock = viewModel.keyDownHandler(waveEvent); @@ -262,6 +263,13 @@ function registerGlobalKeys() { switchBlockInDirection(tabId, NavigateDirection.Right); return true; }); + globalKeyMap.set("Cmd:g", () => { + const bcm = getBlockComponentModel(getFocusedBlockInActiveTab()); + if (bcm.openSwitchConnection != null) { + bcm.openSwitchConnection(); + return true; + } + }); for (let idx = 1; idx <= 9; idx++) { globalKeyMap.set(`Cmd:${idx}`, () => { switchTabAbs(idx); @@ -282,6 +290,11 @@ function registerGlobalKeys() { getApi().registerGlobalWebviewKeys(allKeys); } +function getAllGlobalKeyBindings(): string[] { + const allKeys = Array.from(globalKeyMap.keys()); + return allKeys; +} + // these keyboard events happen *anywhere*, even if you have focus in an input or somewhere else. function handleGlobalWaveKeyboardEvents(waveEvent: WaveKeyboardEvent): boolean { for (const key of globalKeyMap.keys()) { @@ -297,6 +310,7 @@ function handleGlobalWaveKeyboardEvents(waveEvent: WaveKeyboardEvent): boolean { export { appHandleKeyDown, + getAllGlobalKeyBindings, getSimpleControlShiftAtom, registerControlShiftStateUpdateHandler, registerElectronReinjectKeyHandler, diff --git a/frontend/app/view/preview/directorypreview.less b/frontend/app/view/preview/directorypreview.less index 9cb19651b..506c4778c 100644 --- a/frontend/app/view/preview/directorypreview.less +++ b/frontend/app/view/preview/directorypreview.less @@ -7,18 +7,38 @@ display: flex; flex-direction: column; height: 100%; + --min-row-width: 35rem; .dir-table { height: 100%; - min-width: 600px; + width: 100%; --col-size-size: 0.2rem; - border-radius: 3px; display: flex; flex-direction: column; - font: var(--base-font); + + &:not([data-scroll-height="0"]) .dir-table-head::after { + background: rgb(from var(--block-bg-color) r g b / 0.2); + } + + .dir-table-head::after { + content: ""; + z-index: -1; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + backdrop-filter: blur(4px); + } .dir-table-head { + position: sticky; + top: 0; + z-index: 10; + width: 100%; + border-bottom: 1px solid var(--border-color); + .dir-table-head-row { display: flex; - border-bottom: 1px solid var(--border-color); + min-width: var(--min-row-width); padding: 4px 6px; font-size: 0.75rem; @@ -68,10 +88,8 @@ } .dir-table-body { - flex: 1 1 auto; display: flex; flex-direction: column; - overflow: hidden; .dir-table-body-search-display { display: flex; border-radius: 3px; @@ -94,6 +112,7 @@ align-items: center; border-radius: 5px; padding: 0 6px; + min-width: var(--min-row-width); &.focused { background-color: rgb(from var(--accent-color) r g b / 0.5); diff --git a/frontend/app/view/preview/directorypreview.tsx b/frontend/app/view/preview/directorypreview.tsx index 73c8a098b..89552b31d 100644 --- a/frontend/app/view/preview/directorypreview.tsx +++ b/frontend/app/view/preview/directorypreview.tsx @@ -3,10 +3,10 @@ import { ContextMenuModel } from "@/app/store/contextmenu"; import { atoms, createBlock, getApi } from "@/app/store/global"; +import { FileService } from "@/app/store/services"; import type { PreviewModel } from "@/app/view/preview/preview"; -import * as services from "@/store/services"; -import * as keyutil from "@/util/keyutil"; -import * as util from "@/util/util"; +import { checkKeyPressed, isCharacterKeyEvent } from "@/util/keyutil"; +import { base64ToString, isBlank } from "@/util/util"; import { Column, Row, @@ -19,14 +19,11 @@ import { } from "@tanstack/react-table"; import clsx from "clsx"; import dayjs from "dayjs"; -import * as jotai from "jotai"; -import { OverlayScrollbarsComponent } from "overlayscrollbars-react"; +import { useAtom, useAtomValue } from "jotai"; +import { OverlayScrollbarsComponent, OverlayScrollbarsComponentRef } from "overlayscrollbars-react"; import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { quote as shellQuote } from "shell-quote"; - -import { OverlayScrollbars } from "overlayscrollbars"; - -import { useDimensionsWithExistingRef } from "@/app/hook/useDimensions"; +import { debounce } from "throttle-debounce"; import "./directorypreview.less"; interface DirectoryTableProps { @@ -95,7 +92,7 @@ function getLastModifiedTime(unixMillis: number, column: Column { while (mimeType.length > 0) { - let icon = fullConfig.mimetypes?.[mimeType]?.icon ?? null; + const icon = fullConfig.mimetypes?.[mimeType]?.icon ?? null; if (isIconValid(icon)) { return `fa fa-solid fa-${icon} fa-fw`; } @@ -149,10 +146,7 @@ function DirectoryTable({ [fullConfig.mimetypes] ); const getIconColor = useCallback( - (mimeType: string): string => { - let iconColor = fullConfig.mimetypes?.[mimeType]?.color ?? "inherit"; - return iconColor; - }, + (mimeType: string): string => fullConfig.mimetypes?.[mimeType]?.color ?? "inherit", [fullConfig.mimetypes] ); const columns = useMemo( @@ -261,8 +255,25 @@ function DirectoryTable({ return colSizes; }, [table.getState().columnSizingInfo]); + const osRef = useRef(); + const bodyRef = useRef(); + const [scrollHeight, setScrollHeight] = useState(0); + + const onScroll = useCallback( + debounce(2, () => { + setScrollHeight(osRef.current.osInstance().elements().viewport.scrollTop); + }), + [] + ); return ( -
+
{table.getHeaderGroups().map((headerGroup) => (
@@ -295,6 +306,7 @@ function DirectoryTable({
{table.getState().columnSizingInfo.isResizingColumn ? ( ) : ( )} -
+
); } interface TableBodyProps { + bodyRef: React.RefObject; model: PreviewModel; data: Array; table: Table; @@ -332,48 +348,32 @@ interface TableBodyProps { setSearch: (_: string) => void; setSelectedPath: (_: string) => void; setRefreshVersion: React.Dispatch>; + osRef: OverlayScrollbarsComponentRef; } function TableBody({ + bodyRef, model, - data, table, search, focusIndex, setFocusIndex, setSearch, - setSelectedPath, setRefreshVersion, + osRef, }: TableBodyProps) { - const [bodyHeight, setBodyHeight] = useState(0); - - const dummyLineRef = useRef(null); - const parentRef = useRef(null); - const warningBoxRef = useRef(null); - const osInstanceRef = useRef(null); + const dummyLineRef = useRef(); + const warningBoxRef = useRef(); const rowRefs = useRef([]); - const domRect = useDimensionsWithExistingRef(parentRef, 30); - const parentHeight = domRect?.height ?? 0; - const conn = jotai.useAtomValue(model.connection); + const conn = useAtomValue(model.connection); useEffect(() => { - if (dummyLineRef.current && data && parentRef.current) { - const rowHeight = dummyLineRef.current.offsetHeight; - const fullTBodyHeight = rowHeight * data.length; - const warningBoxHeight = warningBoxRef.current?.offsetHeight ?? 0; - const maxHeightLessHeader = parentHeight - warningBoxHeight; - const tbodyHeight = Math.min(maxHeightLessHeader, fullTBodyHeight); - setBodyHeight(tbodyHeight); - } - }, [data, parentHeight]); - - useEffect(() => { - if (focusIndex !== null && rowRefs.current[focusIndex] && parentRef.current) { - const viewport = osInstanceRef.current.elements().viewport; + if (focusIndex !== null && rowRefs.current[focusIndex] && bodyRef.current && osRef) { + const viewport = osRef.osInstance().elements().viewport; const viewportHeight = viewport.offsetHeight; const rowElement = rowRefs.current[focusIndex]; const rowRect = rowElement.getBoundingClientRect(); - const parentRect = parentRef.current.getBoundingClientRect(); + const parentRect = bodyRef.current.getBoundingClientRect(); const viewportScrollTop = viewport.scrollTop; const rowTopRelativeToViewport = rowRect.top - parentRect.top + viewportScrollTop; @@ -387,7 +387,7 @@ function TableBody({ viewport.scrollTo({ top: rowBottomRelativeToViewport - viewportHeight }); } } - }, [focusIndex, parentHeight]); + }, [focusIndex]); const handleFileContextMenu = useCallback( (e: any, path: string, mimetype: string) => { @@ -455,7 +455,7 @@ function TableBody({ menu.push({ label: "Delete File", click: async () => { - await services.FileService.DeleteFile(conn, path).catch((e) => console.log(e)); + await FileService.DeleteFile(conn, path).catch((e) => console.log(e)); setRefreshVersion((current) => current + 1); }, }); @@ -492,12 +492,8 @@ function TableBody({ [setSearch, handleFileContextMenu, setFocusIndex, focusIndex] ); - const handleScrollbarInitialized = (instance) => { - osInstanceRef.current = instance; - }; - return ( -
+
{search !== "" && (
Searching for "{search}" @@ -507,18 +503,13 @@ function TableBody({
)} - -
-
-
dummy-data
-
- {table.getTopRows().map(displayRow)} - {table.getCenterRows().map((row, idx) => displayRow(row, idx + table.getTopRows().length))} +
+
+
dummy-data
- + {table.getTopRows().map(displayRow)} + {table.getCenterRows().map((row, idx) => displayRow(row, idx + table.getTopRows().length))} +
); } @@ -537,11 +528,11 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) { const [focusIndex, setFocusIndex] = useState(0); const [unfilteredData, setUnfilteredData] = useState([]); const [filteredData, setFilteredData] = useState([]); - const fileName = jotai.useAtomValue(model.metaFilePath); - const showHiddenFiles = jotai.useAtomValue(model.showHiddenFiles); + const fileName = useAtomValue(model.metaFilePath); + const showHiddenFiles = useAtomValue(model.showHiddenFiles); const [selectedPath, setSelectedPath] = useState(""); - const [refreshVersion, setRefreshVersion] = jotai.useAtom(model.refreshVersion); - const conn = jotai.useAtomValue(model.connection); + const [refreshVersion, setRefreshVersion] = useAtom(model.refreshVersion); + const conn = useAtomValue(model.connection); useEffect(() => { model.refreshCallback = () => { @@ -554,8 +545,8 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) { useEffect(() => { const getContent = async () => { - const file = await services.FileService.ReadFile(conn, fileName); - const serializedContent = util.base64ToString(file?.data64); + const file = await FileService.ReadFile(conn, fileName); + const serializedContent = base64ToString(file?.data64); const content: FileInfo[] = JSON.parse(serializedContent); setUnfilteredData(content); }; @@ -574,19 +565,19 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) { useEffect(() => { model.directoryKeyDownHandler = (waveEvent: WaveKeyboardEvent): boolean => { - if (keyutil.checkKeyPressed(waveEvent, "Escape")) { + if (checkKeyPressed(waveEvent, "Escape")) { setSearchText(""); return; } - if (keyutil.checkKeyPressed(waveEvent, "ArrowUp")) { + if (checkKeyPressed(waveEvent, "ArrowUp")) { setFocusIndex((idx) => Math.max(idx - 1, 0)); return true; } - if (keyutil.checkKeyPressed(waveEvent, "ArrowDown")) { + if (checkKeyPressed(waveEvent, "ArrowDown")) { setFocusIndex((idx) => Math.min(idx + 1, filteredData.length - 1)); return true; } - if (keyutil.checkKeyPressed(waveEvent, "Enter")) { + if (checkKeyPressed(waveEvent, "Enter")) { if (filteredData.length == 0) { return; } @@ -594,14 +585,14 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) { setSearchText(""); return true; } - if (keyutil.checkKeyPressed(waveEvent, "Backspace")) { + if (checkKeyPressed(waveEvent, "Backspace")) { if (searchText.length == 0) { return true; } setSearchText((current) => current.slice(0, -1)); return true; } - if (keyutil.isCharacterKeyEvent(waveEvent)) { + if (isCharacterKeyEvent(waveEvent)) { setSearchText((current) => current + waveEvent.key); return true; } diff --git a/frontend/app/view/preview/preview.less b/frontend/app/view/preview/preview.less index 58f924827..0d29f58e5 100644 --- a/frontend/app/view/preview/preview.less +++ b/frontend/app/view/preview/preview.less @@ -79,5 +79,5 @@ .full-preview-content { flex-grow: 1; - overflow-y: hidden; + overflow: hidden; } diff --git a/frontend/app/view/term/term.tsx b/frontend/app/view/term/term.tsx index 3647afdce..5a4ff85b6 100644 --- a/frontend/app/view/term/term.tsx +++ b/frontend/app/view/term/term.tsx @@ -1,6 +1,7 @@ // Copyright 2024, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 +import { getAllGlobalKeyBindings } from "@/app/store/keymodel"; import { waveEventSubscribe } from "@/app/store/wps"; import { RpcApi } from "@/app/store/wshclientapi"; import { WindowRpcClient } from "@/app/store/wshrpcutil"; @@ -265,6 +266,7 @@ const TerminalView = ({ blockId, model }: TerminalViewProps) => { if (waveEvent.type != "keydown") { return true; } + // deal with terminal specific keybindings if (keyutil.checkKeyPressed(waveEvent, "Cmd:Escape")) { event.preventDefault(); event.stopPropagation(); @@ -274,37 +276,20 @@ const TerminalView = ({ blockId, model }: TerminalViewProps) => { }); return false; } - if ( - keyutil.checkKeyPressed(waveEvent, "Ctrl:Shift:ArrowLeft") || - keyutil.checkKeyPressed(waveEvent, "Ctrl:Shift:ArrowRight") || - keyutil.checkKeyPressed(waveEvent, "Ctrl:Shift:ArrowUp") || - keyutil.checkKeyPressed(waveEvent, "Ctrl:Shift:ArrowDown") - ) { - return false; - } - for (let i = 1; i <= 9; i++) { - if ( - keyutil.checkKeyPressed(waveEvent, `Ctrl:Shift:c{Digit${i}}`) || - keyutil.checkKeyPressed(waveEvent, `Ctrl:Shift:c{Numpad${i}}`) - ) { - return false; - } - } if (keyutil.checkKeyPressed(waveEvent, "Ctrl:Shift:v")) { const p = navigator.clipboard.readText(); p.then((text) => { termRef.current?.terminal.paste(text); - // termRef.current?.handleTermData(text); }); event.preventDefault(); event.stopPropagation(); - return true; + return false; } else if (keyutil.checkKeyPressed(waveEvent, "Ctrl:Shift:c")) { const sel = termRef.current?.terminal.getSelection(); navigator.clipboard.writeText(sel); event.preventDefault(); event.stopPropagation(); - return true; + return false; } if (shellProcStatusRef.current != "running" && keyutil.checkKeyPressed(waveEvent, "Enter")) { // restart @@ -313,6 +298,12 @@ const TerminalView = ({ blockId, model }: TerminalViewProps) => { prtn.catch((e) => console.log("error controller resync (enter)", blockId, e)); return false; } + const globalKeys = getAllGlobalKeyBindings(); + for (const key of globalKeys) { + if (keyutil.checkKeyPressed(waveEvent, key)) { + return false; + } + } return true; } const fullConfig = globalStore.get(atoms.fullConfigAtom); diff --git a/frontend/app/view/waveai/waveai.tsx b/frontend/app/view/waveai/waveai.tsx index bfff55358..5f282f24b 100644 --- a/frontend/app/view/waveai/waveai.tsx +++ b/frontend/app/view/waveai/waveai.tsx @@ -41,8 +41,6 @@ function promptToMsg(prompt: OpenAIPromptMessageType): ChatMessageType { }; } -const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); - export class WaveAiModel implements ViewModel { viewType: string; blockId: string; @@ -102,14 +100,12 @@ export class WaveAiModel implements ViewModel { // Add a typing indicator set(this.addMessageAtom, typingMessage); - await sleep(1500); const parts = userMessage.text.split(" "); let currentPart = 0; while (currentPart < parts.length) { const part = parts[currentPart] + " "; set(this.updateLastMessageAtom, part, true); currentPart++; - await sleep(100); } set(this.updateLastMessageAtom, "", false); }); @@ -209,7 +205,6 @@ export class WaveAiModel implements ViewModel { } break; } - await sleep(100); } globalStore.set(this.updateLastMessageAtom, "", false); if (fullMsg != "") { diff --git a/frontend/app/workspace/workspace.tsx b/frontend/app/workspace/workspace.tsx index 3aecbf094..bf392ce30 100644 --- a/frontend/app/workspace/workspace.tsx +++ b/frontend/app/workspace/workspace.tsx @@ -80,20 +80,6 @@ async function handleWidgetSelect(blockDef: BlockDef) { createBlock(blockDef); } -function isIconValid(icon: string): boolean { - if (util.isBlank(icon)) { - return false; - } - return icon.match(iconRegex) != null; -} - -function getIconClass(icon: string): string { - if (!isIconValid(icon)) { - return "fa fa-regular fa-browser fa-fw"; - } - return `fa fa-solid fa-${icon} fa-fw`; -} - const Widget = React.memo(({ widget }: { widget: WidgetConfigType }) => { return (
{ title={widget.description || widget.label} >
- +
{!util.isBlank(widget.label) ?
{widget.label}
: null}
diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts index 84a57f941..2ca9cc0ae 100644 --- a/frontend/types/gotypes.d.ts +++ b/frontend/types/gotypes.d.ts @@ -302,6 +302,7 @@ declare global { "term:mode"?: string; "term:theme"?: string; "term:localshellpath"?: string; + "term:localshellopts"?: string[]; count?: number; }; @@ -419,6 +420,7 @@ declare global { "term:fontfamily"?: string; "term:disablewebgl"?: boolean; "term:localshellpath"?: string; + "term:localshellopts"?: string[]; "editor:minimapenabled"?: boolean; "editor:stickyscrollenabled"?: boolean; "web:*"?: boolean; diff --git a/frontend/util/keyutil.ts b/frontend/util/keyutil.ts index 253a35451..b1fb5ccdf 100644 --- a/frontend/util/keyutil.ts +++ b/frontend/util/keyutil.ts @@ -47,16 +47,36 @@ function parseKeyDescription(keyDescription: string): KeyPressDecl { let keys = keyDescription.replace(/[()]/g, "").split(":"); for (let key of keys) { if (key == "Cmd") { + if (PLATFORM == PlatformMacOS) { + rtn.mods.Meta = true; + } else { + rtn.mods.Alt = true; + } rtn.mods.Cmd = true; } else if (key == "Shift") { rtn.mods.Shift = true; } else if (key == "Ctrl") { rtn.mods.Ctrl = true; } else if (key == "Option") { + if (PLATFORM == PlatformMacOS) { + rtn.mods.Alt = true; + } else { + rtn.mods.Meta = true; + } rtn.mods.Option = true; } else if (key == "Alt") { + if (PLATFORM == PlatformMacOS) { + rtn.mods.Option = true; + } else { + rtn.mods.Cmd = true; + } rtn.mods.Alt = true; } else if (key == "Meta") { + if (PLATFORM == PlatformMacOS) { + rtn.mods.Cmd = true; + } else { + rtn.mods.Option = true; + } rtn.mods.Meta = true; } else { let { key: parsedKey, type: keyType } = parseKey(key); @@ -138,10 +158,10 @@ function isInputEvent(event: WaveKeyboardEvent): boolean { function checkKeyPressed(event: WaveKeyboardEvent, keyDescription: string): boolean { let keyPress = parseKeyDescription(keyDescription); - if (!keyPress.mods.Alt && notMod(keyPress.mods.Option, event.option)) { + if (notMod(keyPress.mods.Option, event.option)) { return false; } - if (!keyPress.mods.Meta && notMod(keyPress.mods.Cmd, event.cmd)) { + if (notMod(keyPress.mods.Cmd, event.cmd)) { return false; } if (notMod(keyPress.mods.Shift, event.shift)) { @@ -150,10 +170,10 @@ function checkKeyPressed(event: WaveKeyboardEvent, keyDescription: string): bool if (notMod(keyPress.mods.Ctrl, event.control)) { return false; } - if (keyPress.mods.Alt && !event.alt) { + if (notMod(keyPress.mods.Alt, event.alt)) { return false; } - if (keyPress.mods.Meta && !event.meta) { + if (notMod(keyPress.mods.Meta, event.meta)) { return false; } let eventKey = ""; diff --git a/frontend/util/util.ts b/frontend/util/util.ts index 72729e855..665762494 100644 --- a/frontend/util/util.ts +++ b/frontend/util/util.ts @@ -81,8 +81,11 @@ function jsonDeepEqual(v1: any, v2: any): boolean { return false; } -function makeIconClass(icon: string, fw: boolean, opts?: { spin: boolean }): string { - if (icon == null) { +function makeIconClass(icon: string, fw: boolean, opts?: { spin?: boolean; defaultIcon?: string }): string { + if (isBlank(icon)) { + if (opts?.defaultIcon != null) { + return makeIconClass(opts.defaultIcon, fw, { spin: opts?.spin }); + } return null; } if (icon.match(/^(solid@)?[a-z0-9-]+$/)) { @@ -95,6 +98,14 @@ function makeIconClass(icon: string, fw: boolean, opts?: { spin: boolean }): str icon = icon.replace(/^regular@/, ""); return clsx(`fa fa-sharp fa-regular fa-${icon}`, fw ? "fa-fw" : null, opts?.spin ? "fa-spin" : null); } + if (icon.match(/^brands@[a-z0-9-]+$/)) { + // strip off the "brands@" prefix if it exists + icon = icon.replace(/^brands@/, ""); + return clsx(`fa fa-brands fa-${icon}`, fw ? "fa-fw" : null, opts?.spin ? "fa-spin" : null); + } + if (opts?.defaultIcon != null) { + return makeIconClass(opts.defaultIcon, fw, { spin: opts?.spin }); + } return null; } diff --git a/package.json b/package.json index d88b83a8d..7ecafacd8 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "productName": "Wave", "description": "Open-Source AI-Native Terminal Built for Seamless Workflows", "license": "Apache-2.0", - "version": "0.8.7-beta.1", + "version": "0.8.9-beta.0", "homepage": "https://waveterm.dev", "build": { "appId": "dev.commandline.waveterm" diff --git a/pkg/blockcontroller/blockcontroller.go b/pkg/blockcontroller/blockcontroller.go index e17d04cac..bd55082a3 100644 --- a/pkg/blockcontroller/blockcontroller.go +++ b/pkg/blockcontroller/blockcontroller.go @@ -302,6 +302,12 @@ func (bc *BlockController) DoRunShellCommand(rc *RunShellOpts, blockMeta waveobj if blockMeta.GetString(waveobj.MetaKey_TermLocalShellPath, "") != "" { cmdOpts.ShellPath = blockMeta.GetString(waveobj.MetaKey_TermLocalShellPath, "") } + if len(settings.TermLocalShellOpts) > 0 { + cmdOpts.ShellOpts = append([]string{}, settings.TermLocalShellOpts...) + } + if len(blockMeta.GetStringList(waveobj.MetaKey_TermLocalShellOpts)) > 0 { + cmdOpts.ShellOpts = append([]string{}, blockMeta.GetStringList(waveobj.MetaKey_TermLocalShellOpts)...) + } shellProc, err = shellexec.StartShellProc(rc.TermSize, cmdStr, cmdOpts) if err != nil { return err diff --git a/pkg/shellexec/shellexec.go b/pkg/shellexec/shellexec.go index cc419f283..a58433ded 100644 --- a/pkg/shellexec/shellexec.go +++ b/pkg/shellexec/shellexec.go @@ -35,6 +35,7 @@ type CommandOptsType struct { Cwd string `json:"cwd,omitempty"` Env map[string]string `json:"env,omitempty"` ShellPath string `json:"shellPath,omitempty"` + ShellOpts []string `json:"shellOpts,omitempty"` } type ShellProc struct { @@ -159,6 +160,7 @@ func StartRemoteShellProc(termSize waveobj.TermSize, cmdStr string, cmdOpts Comm log.Printf("error installing rc files: %v", err) return nil, err } + shellOpts = append(shellOpts, cmdOpts.ShellOpts...) homeDir := remote.GetHomeDir(client) @@ -280,6 +282,7 @@ func StartShellProc(termSize waveobj.TermSize, cmdStr string, cmdOpts CommandOpt if shellPath == "" { shellPath = shellutil.DetectLocalShellPath() } + shellOpts = append(shellOpts, cmdOpts.ShellOpts...) if cmdStr == "" { if isBashShell(shellPath) { // add --rcfile diff --git a/pkg/waveobj/metaconsts.go b/pkg/waveobj/metaconsts.go index 40de3c165..5de9fc3b4 100644 --- a/pkg/waveobj/metaconsts.go +++ b/pkg/waveobj/metaconsts.go @@ -60,6 +60,7 @@ const ( MetaKey_TermMode = "term:mode" MetaKey_TermTheme = "term:theme" MetaKey_TermLocalShellPath = "term:localshellpath" + MetaKey_TermLocalShellOpts = "term:localshellopts" MetaKey_Count = "count" ) diff --git a/pkg/waveobj/metamap.go b/pkg/waveobj/metamap.go index 57a881ea5..4412683b5 100644 --- a/pkg/waveobj/metamap.go +++ b/pkg/waveobj/metamap.go @@ -14,6 +14,24 @@ func (m MetaMapType) GetString(key string, def string) string { return def } +func (m MetaMapType) GetStringList(key string) []string { + v, ok := m[key] + if !ok { + return nil + } + varr, ok := v.([]any) + if !ok { + return nil + } + rtn := make([]string, 0) + for _, varrVal := range varr { + if s, ok := varrVal.(string); ok { + rtn = append(rtn, s) + } + } + return rtn +} + func (m MetaMapType) GetBool(key string, def bool) bool { if v, ok := m[key]; ok { if b, ok := v.(bool); ok { diff --git a/pkg/waveobj/wtypemeta.go b/pkg/waveobj/wtypemeta.go index e9d4e95d7..6bedb5004 100644 --- a/pkg/waveobj/wtypemeta.go +++ b/pkg/waveobj/wtypemeta.go @@ -54,12 +54,13 @@ type MetaTSType struct { BgOpacity float64 `json:"bg:opacity,omitempty"` BgBlendMode string `json:"bg:blendmode,omitempty"` - TermClear bool `json:"term:*,omitempty"` - TermFontSize int `json:"term:fontsize,omitempty"` - TermFontFamily string `json:"term:fontfamily,omitempty"` - TermMode string `json:"term:mode,omitempty"` - TermTheme string `json:"term:theme,omitempty"` - TermLocalShellPath string `json:"term:localshellpath,omitempty"` // matches settings + TermClear bool `json:"term:*,omitempty"` + TermFontSize int `json:"term:fontsize,omitempty"` + TermFontFamily string `json:"term:fontfamily,omitempty"` + TermMode string `json:"term:mode,omitempty"` + TermTheme string `json:"term:theme,omitempty"` + TermLocalShellPath string `json:"term:localshellpath,omitempty"` // matches settings + TermLocalShellOpts []string `json:"term:localshellopts,omitempty"` // matches settings Count int `json:"count,omitempty"` // temp for cpu plot. will remove later } diff --git a/pkg/wconfig/defaultconfig/presets.json b/pkg/wconfig/defaultconfig/presets.json index 9e0ee9b92..65884eaf3 100644 --- a/pkg/wconfig/defaultconfig/presets.json +++ b/pkg/wconfig/defaultconfig/presets.json @@ -6,27 +6,85 @@ }, "bg@rainbow": { "display:name": "Rainbow", - "display:order": 1, + "display:order": 2.1, "bg:*": true, "bg": "linear-gradient( 226.4deg, rgba(255,26,1,1) 28.9%, rgba(254,155,1,1) 33%, rgba(255,241,0,1) 48.6%, rgba(34,218,1,1) 65.3%, rgba(0,141,254,1) 80.6%, rgba(113,63,254,1) 100.1% )", "bg:opacity": 0.3 }, "bg@green": { "display:name": "Green", + "display:order": 1.2, "bg:*": true, "bg": "green", "bg:opacity": 0.3 }, "bg@blue": { "display:name": "Blue", + "display:order": 1.1, "bg:*": true, "bg": "blue", "bg:opacity": 0.3 }, "bg@red": { "display:name": "Red", + "display:order": 1.3, "bg:*": true, "bg": "red", "bg:opacity": 0.3 + }, + "bg@ocean-depths": { + "display:name": "Ocean Depths", + "display:order": 2.2, + "bg:*": true, + "bg": "linear-gradient(135deg, purple, blue, teal)", + "bg:opacity": 0.7 + }, + "bg@aqua-horizon": { + "display:name": "Aqua Horizon", + "display:order": 2.3, + "bg:*": true, + "bg": "linear-gradient(135deg, rgba(15, 30, 50, 1) 0%, rgba(40, 90, 130, 0.85) 30%, rgba(20, 100, 150, 0.75) 60%, rgba(0, 120, 160, 0.65) 80%, rgba(0, 140, 180, 0.55) 100%), linear-gradient(135deg, rgba(100, 80, 255, 0.4), rgba(0, 180, 220, 0.4)), radial-gradient(circle at 70% 70%, rgba(255, 255, 255, 0.05), transparent 70%)", + "bg:opacity": 0.85, + "bg:blendmode": "overlay" + }, + "bg@sunset": { + "display:name": "Sunset", + "display:order": 2.4, + "bg:*": true, + "bg": "linear-gradient(135deg, rgba(128, 0, 0, 1), rgba(255, 69, 0, 0.8), rgba(75, 0, 130, 1))", + "bg:opacity": 0.8, + "bg:blendmode": "normal" + }, + "bg@enchantedforest": { + "display:name": "Enchanted Forest", + "display:order": 2.7, + "bg:*": true, + "bg": "linear-gradient(145deg, rgba(0,50,0,1), rgba(34,139,34,0.7) 20%, rgba(0,100,0,0.5) 40%, rgba(0,200,100,0.3) 60%, rgba(34,139,34,0.8) 80%, rgba(0,50,0,1)), radial-gradient(circle at 30% 30%, rgba(255,255,255,0.1), transparent 80%), radial-gradient(circle at 70% 70%, rgba(255,255,255,0.1), transparent 80%)", + "bg:opacity": 0.8, + "bg:blendmode": "soft-light" + }, + "bg@twilight-mist": { + "display:name": "Twilight Mist", + "display:order": 2.9, + "bg:*": true, + "bg": "linear-gradient(180deg, rgba(60,60,90,1) 0%, rgba(90,110,140,0.8) 40%, rgba(120,140,160,0.6) 70%, rgba(60,60,90,1) 100%), radial-gradient(circle at 30% 40%, rgba(255,255,255,0.15), transparent 60%), radial-gradient(circle at 70% 70%, rgba(255,255,255,0.1), transparent 70%)", + "bg:opacity": 0.9, + "bg:blendmode": "soft-light" + }, + "bg@duskhorizon": { + "display:name": "Dusk Horizon", + "display:order": 3.1, + "bg:*": true, + "bg": "linear-gradient(0deg, rgba(128,0,0,1) 0%, rgba(204,85,0,0.7) 20%, rgba(255,140,0,0.6) 45%, rgba(160,90,160,0.5) 65%, rgba(60,60,120,1) 100%), radial-gradient(circle at 30% 30%, rgba(255,255,255,0.1), transparent 60%), radial-gradient(circle at 70% 70%, rgba(255,255,255,0.05), transparent 70%)", + "bg:opacity": 0.9, + "bg:blendmode": "overlay" + }, + "bg@tropical-radiance": { + "display:name": "Tropical Radiance", + "display:order": 3.3, + "bg:*": true, + "bg": "linear-gradient(135deg, rgba(204, 51, 255, 0.9) 0%, rgba(255, 85, 153, 0.75) 30%, rgba(255, 51, 153, 0.65) 60%, rgba(204, 51, 255, 0.6) 80%, rgba(51, 102, 255, 0.5) 100%), radial-gradient(circle at 30% 40%, rgba(255,255,255,0.1), transparent 60%), radial-gradient(circle at 70% 70%, rgba(255,255,255,0.05), transparent 70%)", + "bg:opacity": 0.9, + "bg:blendmode": "overlay" } } diff --git a/pkg/wconfig/metaconsts.go b/pkg/wconfig/metaconsts.go index 0ea0fe6ec..89d408cb8 100644 --- a/pkg/wconfig/metaconsts.go +++ b/pkg/wconfig/metaconsts.go @@ -19,6 +19,7 @@ const ( ConfigKey_TermFontFamily = "term:fontfamily" ConfigKey_TermDisableWebGl = "term:disablewebgl" ConfigKey_TermLocalShellPath = "term:localshellpath" + ConfigKey_TermLocalShellOpts = "term:localshellopts" ConfigKey_EditorMinimapEnabled = "editor:minimapenabled" ConfigKey_EditorStickyScrollEnabled = "editor:stickyscrollenabled" diff --git a/pkg/wconfig/settingsconfig.go b/pkg/wconfig/settingsconfig.go index 2eeba23cb..7f41cc8e7 100644 --- a/pkg/wconfig/settingsconfig.go +++ b/pkg/wconfig/settingsconfig.go @@ -48,11 +48,12 @@ type SettingsType struct { AiMaxTokens float64 `json:"ai:maxtokens,omitempty"` AiTimeoutMs float64 `json:"ai:timeoutms,omitempty"` - TermClear bool `json:"term:*,omitempty"` - TermFontSize float64 `json:"term:fontsize,omitempty"` - TermFontFamily string `json:"term:fontfamily,omitempty"` - TermDisableWebGl bool `json:"term:disablewebgl,omitempty"` - TermLocalShellPath string `json:"term:localshellpath,omitempty"` + TermClear bool `json:"term:*,omitempty"` + TermFontSize float64 `json:"term:fontsize,omitempty"` + TermFontFamily string `json:"term:fontfamily,omitempty"` + TermDisableWebGl bool `json:"term:disablewebgl,omitempty"` + TermLocalShellPath string `json:"term:localshellpath,omitempty"` + TermLocalShellOpts []string `json:"term:localshellopts,omitempty"` EditorMinimapEnabled bool `json:"editor:minimapenabled,omitempty"` EditorStickyScrollEnabled bool `json:"editor:stickyscrollenabled,omitempty"` diff --git a/prettier.config.cjs b/prettier.config.cjs index 3cfb47d5f..5f4fd84dd 100644 --- a/prettier.config.cjs +++ b/prettier.config.cjs @@ -3,7 +3,7 @@ module.exports = { plugins: ["prettier-plugin-jsdoc", "prettier-plugin-organize-imports"], printWidth: 120, trailingComma: "es5", - useTabs: false, + useTabs: false, jsdocVerticalAlignment: true, jsdocSeparateReturnsFromParam: true, jsdocSeparateTagGroups: true,