Merge branch 'main' into evan/global-hotkey

This commit is contained in:
Evan Simkowitz 2024-10-03 11:39:06 -07:00
commit 4273b4e2a7
No known key found for this signature in database
26 changed files with 378 additions and 191 deletions

2
.gitattributes vendored
View File

@ -1 +1 @@
* text=lf
* text=auto

View File

@ -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.

87
.github/ISSUE_TEMPLATE/bug-report.yml vendored Normal file
View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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",

View File

@ -16,6 +16,13 @@ import * as jotai from "jotai";
const simpleControlShiftAtom = jotai.atom(false);
const globalKeyMap = new Map<string, (waveEvent: WaveKeyboardEvent) => 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,

View File

@ -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);

View File

@ -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<FileInfo, number
const iconRegex = /^[a-z0-9- ]+$/;
function isIconValid(icon: string): boolean {
if (util.isBlank(icon)) {
if (isBlank(icon)) {
return false;
}
return icon.match(iconRegex) != null;
@ -134,11 +131,11 @@ function DirectoryTable({
setSelectedPath,
setRefreshVersion,
}: DirectoryTableProps) {
const fullConfig = jotai.useAtomValue(atoms.fullConfigAtom);
const fullConfig = useAtomValue(atoms.fullConfigAtom);
const getIconFromMimeType = useCallback(
(mimeType: string): string => {
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<OverlayScrollbarsComponentRef>();
const bodyRef = useRef<HTMLDivElement>();
const [scrollHeight, setScrollHeight] = useState(0);
const onScroll = useCallback(
debounce(2, () => {
setScrollHeight(osRef.current.osInstance().elements().viewport.scrollTop);
}),
[]
);
return (
<div className="dir-table" style={{ ...columnSizeVars }}>
<OverlayScrollbarsComponent
options={{ scrollbars: { autoHide: "leave" } }}
events={{ scroll: onScroll }}
className="dir-table"
style={{ ...columnSizeVars }}
ref={osRef}
data-scroll-height={scrollHeight}
>
<div className="dir-table-head">
{table.getHeaderGroups().map((headerGroup) => (
<div className="dir-table-head-row" key={headerGroup.id}>
@ -295,6 +306,7 @@ function DirectoryTable({
</div>
{table.getState().columnSizingInfo.isResizingColumn ? (
<MemoizedTableBody
bodyRef={bodyRef}
model={model}
data={data}
table={table}
@ -304,9 +316,11 @@ function DirectoryTable({
setSearch={setSearch}
setSelectedPath={setSelectedPath}
setRefreshVersion={setRefreshVersion}
osRef={osRef.current}
/>
) : (
<TableBody
bodyRef={bodyRef}
model={model}
data={data}
table={table}
@ -316,13 +330,15 @@ function DirectoryTable({
setSearch={setSearch}
setSelectedPath={setSelectedPath}
setRefreshVersion={setRefreshVersion}
osRef={osRef.current}
/>
)}
</div>
</OverlayScrollbarsComponent>
);
}
interface TableBodyProps {
bodyRef: React.RefObject<HTMLDivElement>;
model: PreviewModel;
data: Array<FileInfo>;
table: Table<FileInfo>;
@ -332,48 +348,32 @@ interface TableBodyProps {
setSearch: (_: string) => void;
setSelectedPath: (_: string) => void;
setRefreshVersion: React.Dispatch<React.SetStateAction<number>>;
osRef: OverlayScrollbarsComponentRef;
}
function TableBody({
bodyRef,
model,
data,
table,
search,
focusIndex,
setFocusIndex,
setSearch,
setSelectedPath,
setRefreshVersion,
osRef,
}: TableBodyProps) {
const [bodyHeight, setBodyHeight] = useState(0);
const dummyLineRef = useRef<HTMLDivElement>(null);
const parentRef = useRef<HTMLDivElement>(null);
const warningBoxRef = useRef<HTMLDivElement>(null);
const osInstanceRef = useRef<OverlayScrollbars>(null);
const dummyLineRef = useRef<HTMLDivElement>();
const warningBoxRef = useRef<HTMLDivElement>();
const rowRefs = useRef<HTMLDivElement[]>([]);
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 (
<div className="dir-table-body" ref={parentRef}>
<div className="dir-table-body" ref={bodyRef}>
{search !== "" && (
<div className="dir-table-body-search-display" ref={warningBoxRef}>
<span>Searching for "{search}"</span>
@ -507,18 +503,13 @@ function TableBody({
</div>
</div>
)}
<OverlayScrollbarsComponent
options={{ scrollbars: { autoHide: "leave" } }}
events={{ initialized: handleScrollbarInitialized }}
>
<div className="dir-table-body-scroll-box" style={{ height: bodyHeight }}>
<div className="dir-table-body-scroll-box">
<div className="dummy dir-table-body-row" ref={dummyLineRef}>
<div className="dir-table-body-cell">dummy-data</div>
</div>
{table.getTopRows().map(displayRow)}
{table.getCenterRows().map((row, idx) => displayRow(row, idx + table.getTopRows().length))}
</div>
</OverlayScrollbarsComponent>
</div>
);
}
@ -537,11 +528,11 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) {
const [focusIndex, setFocusIndex] = useState(0);
const [unfilteredData, setUnfilteredData] = useState<FileInfo[]>([]);
const [filteredData, setFilteredData] = useState<FileInfo[]>([]);
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;
}

View File

@ -79,5 +79,5 @@
.full-preview-content {
flex-grow: 1;
overflow-y: hidden;
overflow: hidden;
}

View File

@ -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);

View File

@ -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 != "") {

View File

@ -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 (
<div
@ -102,7 +88,7 @@ const Widget = React.memo(({ widget }: { widget: WidgetConfigType }) => {
title={widget.description || widget.label}
>
<div className="widget-icon" style={{ color: widget.color }}>
<i className={getIconClass(widget.icon)}></i>
<i className={util.makeIconClass(widget.icon, true, { defaultIcon: "browser" })}></i>
</div>
{!util.isBlank(widget.label) ? <div className="widget-label">{widget.label}</div> : null}
</div>

View File

@ -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;

View File

@ -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 = "";

View File

@ -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;
}

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -60,6 +60,7 @@ const (
MetaKey_TermMode = "term:mode"
MetaKey_TermTheme = "term:theme"
MetaKey_TermLocalShellPath = "term:localshellpath"
MetaKey_TermLocalShellOpts = "term:localshellopts"
MetaKey_Count = "count"
)

View File

@ -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 {

View File

@ -60,6 +60,7 @@ type MetaTSType struct {
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
}

View File

@ -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"
}
}

View File

@ -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"

View File

@ -53,6 +53,7 @@ type SettingsType struct {
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"`