Compare commits

..

4 Commits

Author SHA1 Message Date
Evan Simkowitz 669762a9c9 save work 2024-03-26 12:44:29 -07:00
Evan Simkowitz f975ecce48 Moving screen and session into workspaces package 2024-03-26 12:27:34 -07:00
Evan Simkowitz a121bd4bb5 Move bookmarks, history, playbook, and telemetry code out of sstore (#493)
* break out telemetry and playbook

* break out bookmarks

* add license disclaimers
2024-03-25 20:20:52 -07:00
dependabot[bot] dcc7b2943e Bump webpack-dev-middleware from 5.3.3 to 5.3.4 (#490)
Bumps [webpack-dev-middleware](https://github.com/webpack/webpack-dev-middleware) from 5.3.3 to 5.3.4.
- [Release notes](https://github.com/webpack/webpack-dev-middleware/releases)
- [Changelog](https://github.com/webpack/webpack-dev-middleware/blob/v5.3.4/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-dev-middleware/compare/v5.3.3...v5.3.4)

---
updated-dependencies:
- dependency-name: webpack-dev-middleware
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-25 09:55:15 -07:00
33 changed files with 2220 additions and 2618 deletions
+1 -1
View File
@@ -26,7 +26,7 @@ import * as appconst from "@/app/appconst";
import * as textmeasure from "@/util/textmeasure";
import "./screenview.less";
// import "./tabs.less";
import "./tabs.less";
import { MagicLayout } from "../../magiclayout";
dayjs.extend(localizedFormat);
-100
View File
@@ -1,100 +0,0 @@
.screen-tab {
font: var(--base-font);
font-size: var(--screentabs-font-size);
line-height: var(--screentabs-line-height);
border-top: 2px solid transparent;
background: var(--app-bg-color);
.background {
// This applies a transparency mask to the background color, as set above, so that it will blend with whatever the theme's background color is.
z-index: 1;
width: var(--screen-tab-width);
mask-image: linear-gradient(rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0) 100%);
}
&.is-active {
opacity: 1;
font-weight: var(--screentabs-selected-font-weight);
border-top: 2px solid var(--tab-color);
}
&.is-archived {
.fa.fa-archive {
margin-right: 4px;
}
}
svg.svg-icon-inner path {
fill: var(--tab-color);
}
.tabicon i {
color: var(--tab-color);
}
&.color-green,
&.color-default {
--tab-color: var(--tab-green);
}
&.color-orange {
--tab-color: var(--tab-orange);
}
&.color-red {
--tab-color: var(--tab-red);
}
&.color-yellow {
--tab-color: var(--tab-yellow);
}
&.color-blue {
--tab-color: var(--tab-blue);
}
&.color-mint {
--tab-color: var(--tab-mint);
}
&.color-cyan {
--tab-color: var(--tab-cyan);
}
&.color-white {
--tab-color: var(--tab-white);
}
&.color-violet {
--tab-color: var(--tab-violet);
}
&.color-pink {
--tab-color: var(--tab-pink);
}
.screen-tab-inner {
display: flex;
flex-direction: row;
position: absolute;
z-index: 2;
min-width: var(--screen-tab-width);
max-width: var(--screen-tab-width);
align-items: center;
cursor: pointer;
padding: 8px 8px 4px 8px; // extra 4px of tab padding to account for horizontal scrollbar (to make tab text look centered)
.front-icon {
.positional-icon-visible;
}
.tab-name {
flex-grow: 1;
}
// Only one of these will be visible at a time
.end-icons {
// This adjusts the position of the icon to account for the default 8px margin on the parent. We want the positional calculations for this icon to assume it is flush with the edge of the screen tab.
margin: 0 -5px 0 0;
line-height: normal;
.tab-index {
font-size: 12.5px;
}
}
}
.vertical-line {
border-left: 1px solid var(--app-border-color);
margin: 10px 0 8px 0;
}
&:not(:hover) .status-indicator {
.status-indicator-visible;
}
&:hover {
.actions {
.positional-icon-visible;
}
}
}
-71
View File
@@ -1,71 +0,0 @@
import React, { useRef } from "react";
import cn from "classnames";
import { ActionsIcon, StatusIndicator, CenteredIcon } from "@/common/icons/icons";
import { TabIcon } from "@/elements/tabicon";
import { GlobalModel, Screen } from "@/models";
import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import * as constants from "@/app/appconst";
import "./tab2.less";
type ScreenTabProps = {
screen: Screen;
activeScreenId: string;
onDragStart: (name: string, ref: React.RefObject<HTMLDivElement>) => void;
onSwitchScreen: (screenId: string) => void;
};
const ScreenTab: React.FC<ScreenTabProps> = mobxReact.observer(
({ screen, activeScreenId, onSwitchScreen, onDragStart }) => {
const ref = useRef<HTMLDivElement>(null);
const openScreenSettings = (e: any, screen: Screen): void => {
e.preventDefault();
e.stopPropagation();
mobx.action(() => {
GlobalModel.screenSettingsModal.set({ sessionId: screen.sessionId, screenId: screen.screenId });
})();
GlobalModel.modalsModel.pushModal(constants.SCREEN_SETTINGS);
};
const archived = screen.archived.get() ? (
<i title="archived" className="fa-sharp fa-solid fa-box-archive" />
) : null;
const statusIndicatorLevel = screen.statusIndicator.get();
const runningCommands = screen.numRunningCmds.get() > 0;
return (
<div
ref={ref}
data-screenid={screen.screenId}
className={cn(
"screen-tab",
{ "is-active": activeScreenId == screen.screenId, "is-archived": screen.archived.get() },
"color-" + screen.getTabColor()
)}
onMouseDown={() => onDragStart(screen.name.get(), ref)}
onClick={() => onSwitchScreen(screen.screenId)}
data-screentab-name={screen.name.get()}
>
<div className="background"></div>
<div className="screen-tab-inner">
<CenteredIcon className="front-icon">
<TabIcon icon={screen.getTabIcon()} color={screen.getTabColor()} />
</CenteredIcon>
<div className="tab-name truncate">
{archived}
{screen.name.get()}
</div>
<div className="end-icons">
<StatusIndicator level={statusIndicatorLevel} runningCommands={runningCommands} />
<ActionsIcon onClick={(e) => openScreenSettings(e, screen)} />
</div>
</div>
<div className="vertical-line"></div>
</div>
);
}
);
export { ScreenTab };
-45
View File
@@ -1,45 +0,0 @@
.screen-tabs-container {
position: relative;
height: var(--screentabs-height);
.screen-tabs-container-inner {
position: relative; // Needed for absolute positioning of child tabs
white-space: nowrap;
height: 100%;
margin-right: 42px;
overflow: hidden;
}
.new-screen {
width: 42px;
height: 40px;
background-color: var(--app-bg-color);
cursor: pointer;
position: absolute;
top: 50%;
transform: translateY(-50%);
font-size: 20px;
text-align: center;
line-height: 38px;
user-select: none;
}
}
// This ensures the tab bar does not collide with the floating logo. The floating logo sits above the sidebar when it is not collapsed, so no additional margin is needed in that case.
// More margin is given on macOS to account for the traffic light buttons
#main.platform-darwin.mainsidebar-collapsed .screen-tabs-container {
margin-left: var(--floating-logo-width-darwin);
}
#main:not(.platform-darwin).mainsidebar-collapsed .screen-tabs-container {
margin-left: var(--floating-logo-width);
}
// This ensures the tab bar does not collide with the right sidebar triggers.
#main.platform-darwin.rightsidebar-collapsed .screen-tabs-container {
margin-right: var(--floating-right-sidebar-triggers-width-darwin);
}
#main:not(.platform-darwin).rightsidebar-collapsed .screen-tabs-container {
margin-left: var(--floating-right-sidebar-triggers-width);
}
-335
View File
@@ -1,335 +0,0 @@
import React, { useState, useCallback, useRef, useEffect } from "react";
import { reaction } from "mobx";
import { ScreenTab } from "./tab2";
import { observer, useLocalObservable } from "mobx-react";
import { For } from "tsx-control-statements/components";
import { GlobalModel, GlobalCommandRunner, Session, Screen } from "@/models";
import AddIcon from "@/assets/icons/add.svg";
import "./tabs2.less";
const DEFAULT_TAB_WIDTH = 170;
type ScreenTabsProps = {
session: Session;
};
const ScreenTabs: React.FC<ScreenTabsProps> = observer(({ session }) => {
const [screens, setScreens] = useState<Screen[]>([]);
const [tabWidth, setTabWidth] = useState(DEFAULT_TAB_WIDTH);
const [_, setDraggedTab] = useState<string | null>(null);
const [dragStartPositions, setDragStartPositions] = useState<number[]>([]);
const tabContainerRef = useRef<HTMLDivElement>(null);
const addBtnRef = useRef<HTMLDivElement>(null);
const mainSidebarWidth = GlobalModel.mainSidebarModel.getWidth();
const rightSidebarWidth = GlobalModel.rightSidebarModel.getWidth();
let prevDelta: number;
let prevDragDirection: string;
let draggedRemoved: boolean;
let shrunk: boolean;
const store = useLocalObservable(() => ({
get activeScreenId() {
return session?.activeScreenId.get();
},
get screens() {
let activeScreenId = store.activeScreenId;
if (!activeScreenId) {
return [];
}
let screens = GlobalModel.getSessionScreens(session.sessionId);
let filteredScreens = screens.filter(
(screen) => !screen.archived.get() || activeScreenId === screen.screenId
);
filteredScreens.sort((a, b) => a.screenIdx.get() - b.screenIdx.get());
return filteredScreens;
},
}));
useEffect(() => {
// Update tabs when screens change
const dispose = reaction(
() => store.screens,
(screens) => {
setScreens(screens);
}
);
// Clean up
return () => {
if (dispose) dispose();
};
}, [screens.length]);
const getActiveScreenId = (): string | null => {
if (session) {
return session.activeScreenId.get();
}
return null;
};
const updateTabPositions = useCallback(() => {
if (tabContainerRef.current) {
const tabElements = Array.from(tabContainerRef.current.querySelectorAll(".screen-tab"));
let newStartPositions = [];
let cumulativeLeft = 0; // Start from the left edge
tabElements.forEach((tab) => {
newStartPositions.push(cumulativeLeft);
cumulativeLeft += tab.getBoundingClientRect().width; // Add each tab's actual width to the cumulative position
});
setDragStartPositions(newStartPositions);
}
}, [screens]);
useEffect(() => {
updateTabPositions();
}, [screens, updateTabPositions]);
const resizeTabs = useCallback(() => {
if (tabContainerRef.current) {
const containerWidth = tabContainerRef.current.getBoundingClientRect().width;
const numberOfTabs = screens.length;
const totalDefaultTabWidth = numberOfTabs * DEFAULT_TAB_WIDTH;
if (totalDefaultTabWidth > containerWidth) {
// Case where resizing is needed due to limited container width
shrunk = true;
const newTabWidth = containerWidth / numberOfTabs;
setTabWidth(newTabWidth);
screens.forEach((screen, index) => {
const tabElement = tabContainerRef.current.querySelector(
`[data-screentab-name="${screen.name.get()}"]`
) as HTMLElement;
tabElement.style.width = `${newTabWidth}px`;
tabElement.style.left = `${index * newTabWidth}px`;
});
} else if (shrunk || totalDefaultTabWidth < containerWidth) {
// Case where tabs were previously shrunk or there is enough space for default width tabs
shrunk = false;
setTabWidth(DEFAULT_TAB_WIDTH);
screens.forEach((screen, index) => {
const tabElement = tabContainerRef.current.querySelector(
`[data-screentab-name="${screen.name.get()}"]`
) as HTMLElement;
tabElement.style.width = `${DEFAULT_TAB_WIDTH}px`;
tabElement.style.left = `${index * DEFAULT_TAB_WIDTH}px`;
});
}
// Update the position of the Add Tab button
const addButtonElement = addBtnRef.current;
if (addButtonElement && tabContainerRef.current) {
const tabElements = Array.from(tabContainerRef.current.querySelectorAll(".screen-tab"));
const lastTab = tabElements[tabElements.length - 1];
if (lastTab) {
const lastTabRect = lastTab.getBoundingClientRect();
const containerRect = tabContainerRef.current.getBoundingClientRect();
// Calculate the left position relative to the tab container
addButtonElement.style.left = `${lastTabRect.right - containerRect.left}px`;
}
}
updateTabPositions();
}
}, [screens.length, updateTabPositions]);
// Resize tabs when the number of tabs or the window size changes
useEffect(() => {
resizeTabs();
window.addEventListener("resize", resizeTabs);
return () => {
window.removeEventListener("resize", resizeTabs);
};
}, [resizeTabs]);
useEffect(() => {
resizeTabs();
}, [mainSidebarWidth, rightSidebarWidth]);
const onDragStart = useCallback(
(screenId: string, ref: React.RefObject<HTMLDivElement>) => {
setDraggedTab(screenId);
let tabIndex = screens.findIndex((screen) => screen.screenId === screenId);
const tabStartX = dragStartPositions[tabIndex]; // Starting X position of the tab
const containerWidth = tabContainerRef.current.getBoundingClientRect().width;
if (ref.current) {
let initialOffsetX: number | null = null;
const handleMouseMove = (event: MouseEvent) => {
if (initialOffsetX === null) {
initialOffsetX = event.clientX - tabStartX;
}
let currentX = event.clientX - initialOffsetX;
// Constrain movement within the container bounds
if (tabContainerRef.current) {
const numberOfTabs = screens.length;
const totalDefaultTabWidth = numberOfTabs * DEFAULT_TAB_WIDTH;
const containerRect = tabContainerRef.current.getBoundingClientRect();
let containerRectWidth = containerRect.width;
// Set to the total default tab width if there's vacant space
if (totalDefaultTabWidth < containerRectWidth) {
containerRectWidth = totalDefaultTabWidth;
}
const minLeft = 0;
const maxRight = containerRectWidth - tabWidth;
// Adjust currentX to stay within bounds
currentX = Math.min(Math.max(currentX, minLeft), maxRight);
}
ref.current.style.transform = `translateX(${currentX - tabStartX}px)`;
ref.current.style.zIndex = "100";
let dragDirection;
if (currentX - prevDelta > 0) {
dragDirection = "+";
} else if (currentX - prevDelta === 0) {
dragDirection = prevDragDirection;
} else {
dragDirection = "-";
}
prevDelta = currentX;
prevDragDirection = dragDirection;
let newTabIndex = tabIndex;
if (dragDirection === "+") {
// Dragging to the right
for (let i = tabIndex + 1; i < screens.length; i++) {
const otherTabStart = dragStartPositions[i];
if (currentX + tabWidth > otherTabStart + tabWidth / 2) {
newTabIndex = i;
}
}
} else {
// Dragging to the left
for (let i = tabIndex - 1; i >= 0; i--) {
const otherTabEnd = dragStartPositions[i] + tabWidth;
if (currentX < otherTabEnd - tabWidth / 2) {
newTabIndex = i;
}
}
}
// Rearrange the tabs temporarily
if (newTabIndex !== tabIndex) {
const tempTabs = Array.from(screens);
// Remove the dragged tab if not already done
if (!draggedRemoved) {
screens.splice(tabIndex, 1);
draggedRemoved = true;
}
// Find current index of the dragged tab in tempTabs
const currentIndexOfDraggedTab = tabs.indexOf(name);
// Move the dragged tab to its new position
if (currentIndexOfDraggedTab !== -1) {
tabs.splice(currentIndexOfDraggedTab, 1);
}
tabs.splice(newTabIndex, 0, name);
// Update visual positions of the tabs
tabs.forEach((tempTab, index) => {
const tabElement = tabContainerRef.current.querySelector(
`[data-screentab-name="${tempTab}"]`
) as HTMLElement;
if (tempTab !== name) {
tabElement.style.left = `${index * tabWidth}px`;
}
});
tabIndex = newTabIndex;
}
};
document.addEventListener("mousemove", handleMouseMove);
const handleMouseUp = (event: MouseEvent) => {
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
if (ref.current) {
// Reset transform for all tabs
const tabElements = tabContainerRef.current.querySelectorAll(".screen-tab");
tabElements.forEach((tab) => {
const htmlTab = tab as HTMLElement;
htmlTab.style.transform = "";
htmlTab.style.zIndex = "0";
});
// Update the final position of the dragged tab
const draggedTab = screens[tabIndex];
const finalLeftPosition = tabIndex * tabWidth;
const draggedTabElement = tabContainerRef.current.querySelector(
`[data-screentab-name="${draggedTab.name.get()}"]`
) as HTMLElement;
if (draggedTabElement) {
draggedTabElement.style.left = `${finalLeftPosition}px`;
}
}
setDraggedTab(null);
draggedRemoved = false;
};
document.addEventListener("mouseup", handleMouseUp);
}
},
[screens, dragStartPositions]
);
const onSwitchScreen = (screenId: string) => {
if (session == null) {
return;
}
if (session.activeScreenId.get() == screenId) {
return;
}
let screen = session.getScreenById(screenId);
if (screen == null) {
return;
}
GlobalCommandRunner.switchScreen(screenId);
};
const handleNewScreen = () => {
GlobalCommandRunner.createNewScreen();
};
if (session == null) {
return null;
}
const screen: Screen | null = null;
const activeScreenId = getActiveScreenId();
return (
<div className="screen-tabs-container">
<div className="screen-tabs-container-inner" ref={tabContainerRef}>
<For each="screen" of={tabs}>
<ScreenTab
key={screen.screenId}
screen={screen}
activeScreenId={activeScreenId}
onSwitchScreen={onSwitchScreen}
onDragStart={onDragStart}
/>
</For>
</div>
<div ref={addBtnRef} className="new-screen" onClick={handleNewScreen} style={{ left: DEFAULT_TAB_WIDTH }}>
<AddIcon className="icon hoverEffect" />
</div>
</div>
);
});
export { ScreenTabs };
+1 -1
View File
@@ -11,7 +11,7 @@ import { If } from "tsx-control-statements/components";
import { GlobalModel } from "@/models";
import { CmdInput } from "./cmdinput/cmdinput";
import { ScreenView } from "./screen/screenview";
import { ScreenTabs } from "./screen/tabs2";
import { ScreenTabs } from "./screen/tabs";
import { ErrorBoundary } from "@/common/error/errorboundary";
import * as textmeasure from "@/util/textmeasure";
import "./workspace.less";
+4 -3
View File
@@ -45,6 +45,7 @@ import (
"github.com/wavetermdev/waveterm/wavesrv/pkg/scpacket"
"github.com/wavetermdev/waveterm/wavesrv/pkg/scws"
"github.com/wavetermdev/waveterm/wavesrv/pkg/sstore"
"github.com/wavetermdev/waveterm/wavesrv/pkg/telemetry"
"github.com/wavetermdev/waveterm/wavesrv/pkg/wsshell"
)
@@ -211,7 +212,7 @@ func HandleLogActiveState(w http.ResponseWriter, r *http.Request) {
WriteJsonError(w, fmt.Errorf(ErrorDecodingJson, err))
return
}
activity := sstore.ActivityUpdate{}
activity := telemetry.ActivityUpdate{}
if activeState.Fg {
activity.FgMinutes = 1
}
@@ -222,7 +223,7 @@ func HandleLogActiveState(w http.ResponseWriter, r *http.Request) {
activity.OpenMinutes = 1
}
activity.NumConns = remote.NumRemotes()
err = sstore.UpdateCurrentActivity(r.Context(), activity)
err = telemetry.UpdateCurrentActivity(r.Context(), activity)
if err != nil {
WriteJsonError(w, fmt.Errorf("error updating activity: %w", err))
return
@@ -998,7 +999,7 @@ func main() {
}
log.Printf("PCLOUD_ENDPOINT=%s\n", pcloud.GetEndpoint())
sstore.UpdateActivityWrap(context.Background(), sstore.ActivityUpdate{NumConns: remote.NumRemotes()}, "numconns") // set at least one record into activity
telemetry.UpdateActivityWrap(context.Background(), telemetry.ActivityUpdate{NumConns: remote.NumRemotes()}, "numconns") // set at least one record into activity
installSignalHandlers()
go telemetryLoop()
go stdinReadWatch()
+204
View File
@@ -0,0 +1,204 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package bookmarks
import (
"context"
"fmt"
"github.com/wavetermdev/waveterm/wavesrv/pkg/dbutil"
"github.com/wavetermdev/waveterm/wavesrv/pkg/sstore"
)
type BookmarkType struct {
BookmarkId string `json:"bookmarkid"`
CreatedTs int64 `json:"createdts"`
CmdStr string `json:"cmdstr"`
Alias string `json:"alias,omitempty"`
Tags []string `json:"tags"`
Description string `json:"description"`
OrderIdx int64 `json:"orderidx"`
Remove bool `json:"remove,omitempty"`
}
func (bm *BookmarkType) GetSimpleKey() string {
return bm.BookmarkId
}
func (bm *BookmarkType) ToMap() map[string]interface{} {
rtn := make(map[string]interface{})
rtn["bookmarkid"] = bm.BookmarkId
rtn["createdts"] = bm.CreatedTs
rtn["cmdstr"] = bm.CmdStr
rtn["alias"] = bm.Alias
rtn["description"] = bm.Description
rtn["tags"] = dbutil.QuickJsonArr(bm.Tags)
return rtn
}
func (bm *BookmarkType) FromMap(m map[string]interface{}) bool {
dbutil.QuickSetStr(&bm.BookmarkId, m, "bookmarkid")
dbutil.QuickSetInt64(&bm.CreatedTs, m, "createdts")
dbutil.QuickSetStr(&bm.Alias, m, "alias")
dbutil.QuickSetStr(&bm.CmdStr, m, "cmdstr")
dbutil.QuickSetStr(&bm.Description, m, "description")
dbutil.QuickSetJsonArr(&bm.Tags, m, "tags")
return true
}
type bookmarkOrderType struct {
BookmarkId string
OrderIdx int64
}
func GetBookmarks(ctx context.Context, tag string) ([]*BookmarkType, error) {
var bms []*BookmarkType
txErr := sstore.WithTx(ctx, func(tx *sstore.TxWrap) error {
var query string
if tag == "" {
query = `SELECT * FROM bookmark`
bms = dbutil.SelectMapsGen[*BookmarkType](tx, query)
} else {
query = `SELECT * FROM bookmark WHERE EXISTS (SELECT 1 FROM json_each(tags) WHERE value = ?)`
bms = dbutil.SelectMapsGen[*BookmarkType](tx, query, tag)
}
bmMap := dbutil.MakeGenMap(bms)
var orders []bookmarkOrderType
query = `SELECT bookmarkid, orderidx FROM bookmark_order WHERE tag = ?`
tx.Select(&orders, query, tag)
for _, bmOrder := range orders {
bm := bmMap[bmOrder.BookmarkId]
if bm != nil {
bm.OrderIdx = bmOrder.OrderIdx
}
}
return nil
})
if txErr != nil {
return nil, txErr
}
return bms, nil
}
func GetBookmarkById(ctx context.Context, bookmarkId string, tag string) (*BookmarkType, error) {
var rtn *BookmarkType
txErr := sstore.WithTx(ctx, func(tx *sstore.TxWrap) error {
query := `SELECT * FROM bookmark WHERE bookmarkid = ?`
rtn = dbutil.GetMapGen[*BookmarkType](tx, query, bookmarkId)
if rtn == nil {
return nil
}
query = `SELECT orderidx FROM bookmark_order WHERE bookmarkid = ? AND tag = ?`
orderIdx := tx.GetInt(query, bookmarkId, tag)
rtn.OrderIdx = int64(orderIdx)
return nil
})
if txErr != nil {
return nil, txErr
}
return rtn, nil
}
func GetBookmarkIdByArg(ctx context.Context, bookmarkArg string) (string, error) {
var rtnId string
txErr := sstore.WithTx(ctx, func(tx *sstore.TxWrap) error {
if len(bookmarkArg) == 8 {
query := `SELECT bookmarkid FROM bookmark WHERE bookmarkid LIKE (? || '%')`
rtnId = tx.GetString(query, bookmarkArg)
return nil
}
query := `SELECT bookmarkid FROM bookmark WHERE bookmarkid = ?`
rtnId = tx.GetString(query, bookmarkArg)
return nil
})
if txErr != nil {
return "", txErr
}
return rtnId, nil
}
func GetBookmarkIdsByCmdStr(ctx context.Context, cmdStr string) ([]string, error) {
return sstore.WithTxRtn(ctx, func(tx *sstore.TxWrap) ([]string, error) {
query := `SELECT bookmarkid FROM bookmark WHERE cmdstr = ?`
bmIds := tx.SelectStrings(query, cmdStr)
return bmIds, nil
})
}
// ignores OrderIdx field
func InsertBookmark(ctx context.Context, bm *BookmarkType) error {
if bm == nil || bm.BookmarkId == "" {
return fmt.Errorf("invalid empty bookmark id")
}
txErr := sstore.WithTx(ctx, func(tx *sstore.TxWrap) error {
query := `SELECT bookmarkid FROM bookmark WHERE bookmarkid = ?`
if tx.Exists(query, bm.BookmarkId) {
return fmt.Errorf("bookmarkid already exists")
}
query = `INSERT INTO bookmark ( bookmarkid, createdts, cmdstr, alias, tags, description)
VALUES (:bookmarkid,:createdts,:cmdstr,:alias,:tags,:description)`
tx.NamedExec(query, bm.ToMap())
for _, tag := range append(bm.Tags, "") {
query = `SELECT COALESCE(max(orderidx), 0) FROM bookmark_order WHERE tag = ?`
maxOrder := tx.GetInt(query, tag)
query = `INSERT INTO bookmark_order (tag, bookmarkid, orderidx) VALUES (?, ?, ?)`
tx.Exec(query, tag, bm.BookmarkId, maxOrder+1)
}
return nil
})
return txErr
}
const (
BookmarkField_Desc = "desc"
BookmarkField_CmdStr = "cmdstr"
)
func EditBookmark(ctx context.Context, bookmarkId string, editMap map[string]interface{}) error {
txErr := sstore.WithTx(ctx, func(tx *sstore.TxWrap) error {
query := `SELECT bookmarkid FROM bookmark WHERE bookmarkid = ?`
if !tx.Exists(query, bookmarkId) {
return fmt.Errorf("bookmark not found")
}
if desc, found := editMap[BookmarkField_Desc]; found {
query = `UPDATE bookmark SET description = ? WHERE bookmarkid = ?`
tx.Exec(query, desc, bookmarkId)
}
if cmdStr, found := editMap[BookmarkField_CmdStr]; found {
query = `UPDATE bookmark SET cmdstr = ? WHERE bookmarkid = ?`
tx.Exec(query, cmdStr, bookmarkId)
}
return nil
})
return txErr
}
func fixupBookmarkOrder(tx *sstore.TxWrap) {
query := `
WITH new_order AS (
SELECT tag, bookmarkid, row_number() OVER (PARTITION BY tag ORDER BY orderidx) AS newidx FROM bookmark_order
)
UPDATE bookmark_order
SET orderidx = new_order.newidx
FROM new_order
WHERE bookmark_order.tag = new_order.tag AND bookmark_order.bookmarkid = new_order.bookmarkid
`
tx.Exec(query)
}
func DeleteBookmark(ctx context.Context, bookmarkId string) error {
txErr := sstore.WithTx(ctx, func(tx *sstore.TxWrap) error {
query := `SELECT bookmarkid FROM bookmark WHERE bookmarkid = ?`
if !tx.Exists(query, bookmarkId) {
return fmt.Errorf("bookmark not found")
}
query = `DELETE FROM bookmark WHERE bookmarkid = ?`
tx.Exec(query, bookmarkId)
query = `DELETE FROM bookmark_order WHERE bookmarkid = ?`
tx.Exec(query, bookmarkId)
fixupBookmarkOrder(tx)
return nil
})
return txErr
}
+23
View File
@@ -0,0 +1,23 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package bookmarks
import "github.com/wavetermdev/waveterm/wavesrv/pkg/scbus"
type BookmarksUpdate struct {
Bookmarks []*BookmarkType `json:"bookmarks"`
SelectedBookmark string `json:"selectedbookmark,omitempty"`
}
func (BookmarksUpdate) GetType() string {
return "bookmarks"
}
func AddBookmarksUpdate(update *scbus.ModelUpdatePacketType, bookmarks []*BookmarkType, selectedBookmark *string) {
if selectedBookmark == nil {
update.AddUpdate(BookmarksUpdate{Bookmarks: bookmarks})
} else {
update.AddUpdate(BookmarksUpdate{Bookmarks: bookmarks, SelectedBookmark: *selectedBookmark})
}
}
+45 -42
View File
@@ -35,8 +35,10 @@ import (
"github.com/wavetermdev/waveterm/waveshell/pkg/shellutil"
"github.com/wavetermdev/waveterm/waveshell/pkg/shexec"
"github.com/wavetermdev/waveterm/waveshell/pkg/utilfn"
"github.com/wavetermdev/waveterm/wavesrv/pkg/bookmarks"
"github.com/wavetermdev/waveterm/wavesrv/pkg/comp"
"github.com/wavetermdev/waveterm/wavesrv/pkg/dbutil"
"github.com/wavetermdev/waveterm/wavesrv/pkg/history"
"github.com/wavetermdev/waveterm/wavesrv/pkg/pcloud"
"github.com/wavetermdev/waveterm/wavesrv/pkg/promptenc"
"github.com/wavetermdev/waveterm/wavesrv/pkg/releasechecker"
@@ -47,6 +49,7 @@ import (
"github.com/wavetermdev/waveterm/wavesrv/pkg/scbus"
"github.com/wavetermdev/waveterm/wavesrv/pkg/scpacket"
"github.com/wavetermdev/waveterm/wavesrv/pkg/sstore"
"github.com/wavetermdev/waveterm/wavesrv/pkg/telemetry"
"golang.org/x/mod/semver"
)
@@ -473,7 +476,7 @@ func doHistoryExpansion(ctx context.Context, ids resolvedIds, hnum int) (string,
foundHistoryNum := hnum
if hnum == -1 {
var err error
foundHistoryNum, err = sstore.GetLastHistoryLineNum(ctx, ids.ScreenId)
foundHistoryNum, err = history.GetLastHistoryLineNum(ctx, ids.ScreenId)
if err != nil {
return "", fmt.Errorf("cannot expand history, error finding last history item: %v", err)
}
@@ -481,7 +484,7 @@ func doHistoryExpansion(ctx context.Context, ids resolvedIds, hnum int) (string,
return "", fmt.Errorf("cannot expand history, no last history item")
}
}
hitem, err := sstore.GetHistoryItemByLineNum(ctx, ids.ScreenId, foundHistoryNum)
hitem, err := history.GetHistoryItemByLineNum(ctx, ids.ScreenId, foundHistoryNum)
if err != nil {
return "", fmt.Errorf("cannot get history item '%d': %v", foundHistoryNum, err)
}
@@ -666,7 +669,7 @@ func addToHistory(ctx context.Context, pk *scpacket.FeCommandPacketType, history
if err != nil {
return err
}
hitem := &sstore.HistoryItemType{
hitem := &history.HistoryItemType{
HistoryId: scbase.GenWaveUUID(),
Ts: time.Now().UnixMilli(),
UserId: DefaultUserId,
@@ -690,7 +693,7 @@ func addToHistory(ctx context.Context, pk *scpacket.FeCommandPacketType, history
if !isMetaCmd && historyContext.RemotePtr != nil {
hitem.Remote = *historyContext.RemotePtr
}
err = sstore.InsertHistoryItem(ctx, hitem)
err = history.InsertHistoryItem(ctx, hitem)
if err != nil {
return err
}
@@ -706,7 +709,7 @@ func EvalCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbus.U
}
evalDepth := getEvalDepth(ctx)
if pk.Interactive && evalDepth == 0 {
sstore.UpdateActivityWrap(ctx, sstore.ActivityUpdate{NumCommands: 1}, "numcommands")
telemetry.UpdateActivityWrap(ctx, telemetry.ActivityUpdate{NumCommands: 1}, "numcommands")
}
if evalDepth > MaxEvalDepth {
return nil, fmt.Errorf("alias/history expansion max-depth exceeded")
@@ -3352,8 +3355,8 @@ func validateRemoteColor(color string, typeStr string) error {
}
func SessionOpenSharedCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbus.UpdatePacket, error) {
activity := sstore.ActivityUpdate{ClickShared: 1}
sstore.UpdateActivityWrap(ctx, activity, "click-shared")
activity := telemetry.ActivityUpdate{ClickShared: 1}
telemetry.UpdateActivityWrap(ctx, activity, "click-shared")
return nil, fmt.Errorf("shared sessions are not available in this version of prompt (stay tuned)")
}
@@ -3615,11 +3618,11 @@ func MainViewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scb
update := scbus.MakeUpdatePacket()
mainViewArg := pk.Args[0]
if mainViewArg == sstore.MainViewSession {
update.AddUpdate(&sstore.MainViewUpdate{MainView: sstore.MainViewSession})
update.AddUpdate(&MainViewUpdate{MainView: sstore.MainViewSession})
} else if mainViewArg == sstore.MainViewConnections {
update.AddUpdate(&sstore.MainViewUpdate{MainView: sstore.MainViewConnections})
update.AddUpdate(&MainViewUpdate{MainView: sstore.MainViewConnections})
} else if mainViewArg == sstore.MainViewSettings {
update.AddUpdate(&sstore.MainViewUpdate{MainView: sstore.MainViewSettings})
update.AddUpdate(&MainViewUpdate{MainView: sstore.MainViewSettings})
} else if mainViewArg == sstore.MainViewHistory {
return nil, fmt.Errorf("use /history instead")
} else if mainViewArg == sstore.MainViewBookmarks {
@@ -3825,7 +3828,7 @@ func HistoryPurgeCommand(ctx context.Context, pk *scpacket.FeCommandPacketType)
}
historyIds = append(historyIds, historyArg)
}
err := sstore.PurgeHistoryByIds(ctx, historyIds)
err := history.PurgeHistoryByIds(ctx, historyIds)
if err != nil {
return nil, fmt.Errorf("/history:purge error purging items: %v", err)
}
@@ -3837,7 +3840,7 @@ const HistoryViewPageSize = 50
var cmdFilterLs = regexp.MustCompile(`^ls(\s|$)`)
var cmdFilterCd = regexp.MustCompile(`^cd(\s|$)`)
func historyCmdFilter(hitem *sstore.HistoryItemType) bool {
func historyCmdFilter(hitem *history.HistoryItemType) bool {
cmdStr := hitem.CmdStr
if cmdStr == "" || strings.Index(cmdStr, ";") != -1 || strings.Index(cmdStr, "\n") != -1 {
return true
@@ -3864,7 +3867,7 @@ func HistoryViewAllCommand(ctx context.Context, pk *scpacket.FeCommandPacketType
if err != nil {
return nil, err
}
opts := sstore.HistoryQueryOpts{MaxItems: HistoryViewPageSize, Offset: offset, RawOffset: rawOffset}
opts := history.HistoryQueryOpts{MaxItems: HistoryViewPageSize, Offset: offset, RawOffset: rawOffset}
if pk.Kwargs["text"] != "" {
opts.SearchText = pk.Kwargs["text"]
}
@@ -3903,25 +3906,25 @@ func HistoryViewAllCommand(ctx context.Context, pk *scpacket.FeCommandPacketType
if err != nil {
return nil, fmt.Errorf("invalid meta arg (must be boolean): %v", err)
}
hresult, err := sstore.GetHistoryItems(ctx, opts)
hresult, err := history.GetHistoryItems(ctx, opts)
if err != nil {
return nil, err
}
hvdata := &sstore.HistoryViewData{
hvdata := &history.HistoryViewData{
Items: hresult.Items,
Offset: hresult.Offset,
RawOffset: hresult.RawOffset,
NextRawOffset: hresult.NextRawOffset,
HasMore: hresult.HasMore,
}
lines, cmds, err := sstore.GetLineCmdsFromHistoryItems(ctx, hvdata.Items)
lines, cmds, err := history.GetLineCmdsFromHistoryItems(ctx, hvdata.Items)
if err != nil {
return nil, err
}
hvdata.Lines = lines
hvdata.Cmds = cmds
update := scbus.MakeUpdatePacket()
update.AddUpdate(&sstore.MainViewUpdate{MainView: sstore.MainViewHistory, HistoryView: hvdata})
update.AddUpdate(&MainViewUpdate{MainView: sstore.MainViewHistory, HistoryView: hvdata})
return update, nil
}
@@ -3957,17 +3960,17 @@ func HistoryCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbu
} else if htype == HistoryTypeSession {
hScreenId = ""
}
hopts := sstore.HistoryQueryOpts{MaxItems: maxItems, SessionId: hSessionId, ScreenId: hScreenId}
hresult, err := sstore.GetHistoryItems(ctx, hopts)
hopts := history.HistoryQueryOpts{MaxItems: maxItems, SessionId: hSessionId, ScreenId: hScreenId}
hresult, err := history.GetHistoryItems(ctx, hopts)
if err != nil {
return nil, err
}
show := !resolveBool(pk.Kwargs["noshow"], false)
if show {
sstore.UpdateActivityWrap(ctx, sstore.ActivityUpdate{HistoryView: 1}, "history")
telemetry.UpdateActivityWrap(ctx, telemetry.ActivityUpdate{HistoryView: 1}, "history")
}
update := scbus.MakeUpdatePacket()
update.AddUpdate(sstore.HistoryInfoType{
update.AddUpdate(history.HistoryInfoType{
HistoryType: htype,
SessionId: ids.SessionId,
ScreenId: ids.ScreenId,
@@ -4310,16 +4313,16 @@ func BookmarksShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType)
if len(pk.Args) > 0 {
tagName = pk.Args[0]
}
bms, err := sstore.GetBookmarks(ctx, tagName)
bms, err := bookmarks.GetBookmarks(ctx, tagName)
if err != nil {
return nil, fmt.Errorf("cannot retrieve bookmarks: %v", err)
}
sstore.UpdateActivityWrap(ctx, sstore.ActivityUpdate{BookmarksView: 1}, "bookmarks")
telemetry.UpdateActivityWrap(ctx, telemetry.ActivityUpdate{BookmarksView: 1}, "bookmarks")
update := scbus.MakeUpdatePacket()
update.AddUpdate(&sstore.MainViewUpdate{
update.AddUpdate(&MainViewUpdate{
MainView: sstore.MainViewBookmarks,
BookmarksView: &sstore.BookmarksUpdate{Bookmarks: bms},
BookmarksView: &bookmarks.BookmarksUpdate{Bookmarks: bms},
})
return update, nil
}
@@ -4329,7 +4332,7 @@ func BookmarkSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (
return nil, fmt.Errorf("/bookmark:set requires one argument (bookmark id)")
}
bookmarkArg := pk.Args[0]
bookmarkId, err := sstore.GetBookmarkIdByArg(ctx, bookmarkArg)
bookmarkId, err := bookmarks.GetBookmarkIdByArg(ctx, bookmarkArg)
if err != nil {
return nil, fmt.Errorf("error trying to resolve bookmark: %v", err)
}
@@ -4338,25 +4341,25 @@ func BookmarkSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (
}
editMap := make(map[string]interface{})
if descStr, found := pk.Kwargs["desc"]; found {
editMap[sstore.BookmarkField_Desc] = descStr
editMap[bookmarks.BookmarkField_Desc] = descStr
}
if cmdStr, found := pk.Kwargs["cmdstr"]; found {
editMap[sstore.BookmarkField_CmdStr] = cmdStr
editMap[bookmarks.BookmarkField_CmdStr] = cmdStr
}
if len(editMap) == 0 {
return nil, fmt.Errorf("no fields set, can set %s", formatStrs([]string{"desc", "cmdstr"}, "or", false))
}
err = sstore.EditBookmark(ctx, bookmarkId, editMap)
err = bookmarks.EditBookmark(ctx, bookmarkId, editMap)
if err != nil {
return nil, fmt.Errorf("error trying to edit bookmark: %v", err)
}
bm, err := sstore.GetBookmarkById(ctx, bookmarkId, "")
bm, err := bookmarks.GetBookmarkById(ctx, bookmarkId, "")
if err != nil {
return nil, fmt.Errorf("error retrieving edited bookmark: %v", err)
}
bms := []*sstore.BookmarkType{bm}
bms := []*bookmarks.BookmarkType{bm}
update := scbus.MakeUpdatePacket()
sstore.AddBookmarksUpdate(update, bms, nil)
bookmarks.AddBookmarksUpdate(update, bms, nil)
update.AddUpdate(sstore.InfoMsgUpdate("bookmark edited"))
return update, nil
}
@@ -4366,20 +4369,20 @@ func BookmarkDeleteCommand(ctx context.Context, pk *scpacket.FeCommandPacketType
return nil, fmt.Errorf("/bookmark:delete requires one argument (bookmark id)")
}
bookmarkArg := pk.Args[0]
bookmarkId, err := sstore.GetBookmarkIdByArg(ctx, bookmarkArg)
bookmarkId, err := bookmarks.GetBookmarkIdByArg(ctx, bookmarkArg)
if err != nil {
return nil, fmt.Errorf("error trying to resolve bookmark: %v", err)
}
if bookmarkId == "" {
return nil, fmt.Errorf("bookmark not found")
}
err = sstore.DeleteBookmark(ctx, bookmarkId)
err = bookmarks.DeleteBookmark(ctx, bookmarkId)
if err != nil {
return nil, fmt.Errorf("error deleting bookmark: %v", err)
}
update := scbus.MakeUpdatePacket()
bms := []*sstore.BookmarkType{{BookmarkId: bookmarkId, Remove: true}}
sstore.AddBookmarksUpdate(update, bms, nil)
bms := []*bookmarks.BookmarkType{{BookmarkId: bookmarkId, Remove: true}}
bookmarks.AddBookmarksUpdate(update, bms, nil)
update.AddUpdate(sstore.InfoMsgUpdate("bookmark deleted"))
return update, nil
}
@@ -4407,7 +4410,7 @@ func LineBookmarkCommand(ctx context.Context, pk *scpacket.FeCommandPacketType)
if cmdObj == nil {
return nil, fmt.Errorf("cannot bookmark non-cmd line")
}
existingBmIds, err := sstore.GetBookmarkIdsByCmdStr(ctx, cmdObj.CmdStr)
existingBmIds, err := bookmarks.GetBookmarkIdsByCmdStr(ctx, cmdObj.CmdStr)
if err != nil {
return nil, fmt.Errorf("error trying to retrieve current boookmarks: %v", err)
}
@@ -4415,7 +4418,7 @@ func LineBookmarkCommand(ctx context.Context, pk *scpacket.FeCommandPacketType)
if len(existingBmIds) > 0 {
newBmId = existingBmIds[0]
} else {
newBm := &sstore.BookmarkType{
newBm := &bookmarks.BookmarkType{
BookmarkId: uuid.New().String(),
CreatedTs: time.Now().UnixMilli(),
CmdStr: cmdObj.CmdStr,
@@ -4423,17 +4426,17 @@ func LineBookmarkCommand(ctx context.Context, pk *scpacket.FeCommandPacketType)
Tags: nil,
Description: "",
}
err = sstore.InsertBookmark(ctx, newBm)
err = bookmarks.InsertBookmark(ctx, newBm)
if err != nil {
return nil, fmt.Errorf("cannot insert bookmark: %v", err)
}
newBmId = newBm.BookmarkId
}
bms, err := sstore.GetBookmarks(ctx, "")
bms, err := bookmarks.GetBookmarks(ctx, "")
update := scbus.MakeUpdatePacket()
update.AddUpdate(&sstore.MainViewUpdate{
update.AddUpdate(&MainViewUpdate{
MainView: sstore.MainViewBookmarks,
BookmarksView: &sstore.BookmarksUpdate{Bookmarks: bms, SelectedBookmark: newBmId},
BookmarksView: &bookmarks.BookmarksUpdate{Bookmarks: bms, SelectedBookmark: newBmId},
})
return update, nil
}
+19
View File
@@ -0,0 +1,19 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package cmdrunner
import (
"github.com/wavetermdev/waveterm/wavesrv/pkg/bookmarks"
"github.com/wavetermdev/waveterm/wavesrv/pkg/history"
)
type MainViewUpdate struct {
MainView string `json:"mainview"`
HistoryView *history.HistoryViewData `json:"historyview,omitempty"`
BookmarksView *bookmarks.BookmarksUpdate `json:"bookmarksview,omitempty"`
}
func (MainViewUpdate) GetType() string {
return "mainview"
}
+327
View File
@@ -0,0 +1,327 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package history
import (
"context"
"fmt"
"strings"
"github.com/google/uuid"
"github.com/wavetermdev/waveterm/wavesrv/pkg/dbutil"
"github.com/wavetermdev/waveterm/wavesrv/pkg/sstore"
"github.com/wavetermdev/waveterm/wavesrv/pkg/workspaces/line"
)
type HistoryItemType struct {
HistoryId string `json:"historyid"`
Ts int64 `json:"ts"`
UserId string `json:"userid"`
SessionId string `json:"sessionid"`
ScreenId string `json:"screenid"`
LineId string `json:"lineid"`
HadError bool `json:"haderror"`
CmdStr string `json:"cmdstr"`
Remote sstore.RemotePtrType `json:"remote"`
IsMetaCmd bool `json:"ismetacmd"`
ExitCode *int64 `json:"exitcode,omitempty"`
DurationMs *int64 `json:"durationms,omitempty"`
FeState sstore.FeStateType `json:"festate,omitempty"`
Tags map[string]bool `json:"tags,omitempty"`
LineNum int64 `json:"linenum" dbmap:"-"`
Status string `json:"status"`
// only for updates
Remove bool `json:"remove" dbmap:"-"`
// transient (string because of different history orderings)
HistoryNum string `json:"historynum" dbmap:"-"`
}
func (h *HistoryItemType) ToMap() map[string]interface{} {
rtn := make(map[string]interface{})
rtn["historyid"] = h.HistoryId
rtn["ts"] = h.Ts
rtn["userid"] = h.UserId
rtn["sessionid"] = h.SessionId
rtn["screenid"] = h.ScreenId
rtn["lineid"] = h.LineId
rtn["linenum"] = h.LineNum
rtn["haderror"] = h.HadError
rtn["cmdstr"] = h.CmdStr
rtn["remoteownerid"] = h.Remote.OwnerId
rtn["remoteid"] = h.Remote.RemoteId
rtn["remotename"] = h.Remote.Name
rtn["ismetacmd"] = h.IsMetaCmd
rtn["exitcode"] = h.ExitCode
rtn["durationms"] = h.DurationMs
rtn["festate"] = dbutil.QuickJson(h.FeState)
rtn["tags"] = dbutil.QuickJson(h.Tags)
rtn["status"] = h.Status
return rtn
}
func (h *HistoryItemType) FromMap(m map[string]interface{}) bool {
dbutil.QuickSetStr(&h.HistoryId, m, "historyid")
dbutil.QuickSetInt64(&h.Ts, m, "ts")
dbutil.QuickSetStr(&h.UserId, m, "userid")
dbutil.QuickSetStr(&h.SessionId, m, "sessionid")
dbutil.QuickSetStr(&h.ScreenId, m, "screenid")
dbutil.QuickSetStr(&h.LineId, m, "lineid")
dbutil.QuickSetBool(&h.HadError, m, "haderror")
dbutil.QuickSetStr(&h.CmdStr, m, "cmdstr")
dbutil.QuickSetStr(&h.Remote.OwnerId, m, "remoteownerid")
dbutil.QuickSetStr(&h.Remote.RemoteId, m, "remoteid")
dbutil.QuickSetStr(&h.Remote.Name, m, "remotename")
dbutil.QuickSetBool(&h.IsMetaCmd, m, "ismetacmd")
dbutil.QuickSetStr(&h.HistoryNum, m, "historynum")
dbutil.QuickSetInt64(&h.LineNum, m, "linenum")
dbutil.QuickSetNullableInt64(&h.ExitCode, m, "exitcode")
dbutil.QuickSetNullableInt64(&h.DurationMs, m, "durationms")
dbutil.QuickSetJson(&h.FeState, m, "festate")
dbutil.QuickSetJson(&h.Tags, m, "tags")
dbutil.QuickSetStr(&h.Status, m, "status")
return true
}
type HistoryQueryOpts struct {
Offset int
MaxItems int
FromTs int64
SearchText string
SessionId string
RemoteId string
ScreenId string
NoMeta bool
RawOffset int
FilterFn func(*HistoryItemType) bool
}
type HistoryQueryResult struct {
MaxItems int
Items []*HistoryItemType
Offset int // the offset shown to user
RawOffset int // internal offset
HasMore bool
NextRawOffset int // internal offset used by pager for next query
prevItems int // holds number of items skipped by RawOffset
}
type HistoryViewData struct {
Items []*HistoryItemType `json:"items"`
Offset int `json:"offset"`
RawOffset int `json:"rawoffset"`
NextRawOffset int `json:"nextrawoffset"`
HasMore bool `json:"hasmore"`
Lines []*line.LineType `json:"lines"`
Cmds []*sstore.CmdType `json:"cmds"`
}
const HistoryCols = "h.historyid, h.ts, h.userid, h.sessionid, h.screenid, h.lineid, h.haderror, h.cmdstr, h.remoteownerid, h.remoteid, h.remotename, h.ismetacmd, h.linenum, h.exitcode, h.durationms, h.festate, h.tags, h.status"
const DefaultMaxHistoryItems = 1000
func InsertHistoryItem(ctx context.Context, hitem *HistoryItemType) error {
if hitem == nil {
return fmt.Errorf("cannot insert nil history item")
}
txErr := sstore.WithTx(ctx, func(tx *sstore.TxWrap) error {
query := `INSERT INTO history
( historyid, ts, userid, sessionid, screenid, lineid, haderror, cmdstr, remoteownerid, remoteid, remotename, ismetacmd, linenum, exitcode, durationms, festate, tags, status) VALUES
(:historyid,:ts,:userid,:sessionid,:screenid,:lineid,:haderror,:cmdstr,:remoteownerid,:remoteid,:remotename,:ismetacmd,:linenum,:exitcode,:durationms,:festate,:tags,:status)`
tx.NamedExec(query, hitem.ToMap())
return nil
})
return txErr
}
const HistoryQueryChunkSize = 1000
func _getNextHistoryItem(items []*HistoryItemType, index int, filterFn func(*HistoryItemType) bool) (*HistoryItemType, int) {
for ; index < len(items); index++ {
item := items[index]
if filterFn(item) {
return item, index
}
}
return nil, index
}
// returns true if done, false if we still need to process more items
func (result *HistoryQueryResult) processItem(item *HistoryItemType, rawOffset int) bool {
if result.prevItems < result.Offset {
result.prevItems++
return false
}
if len(result.Items) == result.MaxItems {
result.HasMore = true
result.NextRawOffset = rawOffset
return true
}
if len(result.Items) == 0 {
result.RawOffset = rawOffset
}
result.Items = append(result.Items, item)
return false
}
func runHistoryQueryWithFilter(tx *sstore.TxWrap, opts HistoryQueryOpts) (*HistoryQueryResult, error) {
if opts.MaxItems == 0 {
return nil, fmt.Errorf("invalid query, maxitems is 0")
}
rtn := &HistoryQueryResult{Offset: opts.Offset, MaxItems: opts.MaxItems}
var rawOffset int
if opts.RawOffset >= opts.Offset {
rtn.prevItems = opts.Offset
rawOffset = opts.RawOffset
} else {
rawOffset = 0
}
for {
resultItems, err := runHistoryQuery(tx, opts, rawOffset, HistoryQueryChunkSize)
if err != nil {
return nil, err
}
isDone := false
for resultIdx := 0; resultIdx < len(resultItems); resultIdx++ {
if opts.FilterFn != nil && !opts.FilterFn(resultItems[resultIdx]) {
continue
}
isDone = rtn.processItem(resultItems[resultIdx], rawOffset+resultIdx)
if isDone {
break
}
}
if isDone {
break
}
if len(resultItems) < HistoryQueryChunkSize {
break
}
rawOffset += HistoryQueryChunkSize
}
return rtn, nil
}
func runHistoryQuery(tx *sstore.TxWrap, opts HistoryQueryOpts, realOffset int, itemLimit int) ([]*HistoryItemType, error) {
// check sessionid/screenid format because we are directly inserting them into the SQL
if opts.SessionId != "" {
_, err := uuid.Parse(opts.SessionId)
if err != nil {
return nil, fmt.Errorf("malformed sessionid")
}
}
if opts.ScreenId != "" {
_, err := uuid.Parse(opts.ScreenId)
if err != nil {
return nil, fmt.Errorf("malformed screenid")
}
}
if opts.RemoteId != "" {
_, err := uuid.Parse(opts.RemoteId)
if err != nil {
return nil, fmt.Errorf("malformed remoteid")
}
}
whereClause := "WHERE 1"
var queryArgs []interface{}
hNumStr := ""
if opts.SessionId != "" && opts.ScreenId != "" {
whereClause += fmt.Sprintf(" AND h.sessionid = '%s' AND h.screenid = '%s'", opts.SessionId, opts.ScreenId)
hNumStr = ""
} else if opts.SessionId != "" {
whereClause += fmt.Sprintf(" AND h.sessionid = '%s'", opts.SessionId)
hNumStr = "s"
} else {
hNumStr = "g"
}
if opts.SearchText != "" {
whereClause += " AND h.cmdstr LIKE ? ESCAPE '\\'"
likeArg := opts.SearchText
likeArg = strings.ReplaceAll(likeArg, "%", "\\%")
likeArg = strings.ReplaceAll(likeArg, "_", "\\_")
queryArgs = append(queryArgs, "%"+likeArg+"%")
}
if opts.FromTs > 0 {
whereClause += fmt.Sprintf(" AND h.ts <= %d", opts.FromTs)
}
if opts.RemoteId != "" {
whereClause += fmt.Sprintf(" AND h.remoteid = '%s'", opts.RemoteId)
}
if opts.NoMeta {
whereClause += " AND NOT h.ismetacmd"
}
query := fmt.Sprintf("SELECT %s, ('%s' || CAST((row_number() OVER win) as text)) historynum FROM history h %s WINDOW win AS (ORDER BY h.ts, h.historyid) ORDER BY h.ts DESC, h.historyid DESC LIMIT %d OFFSET %d", HistoryCols, hNumStr, whereClause, itemLimit, realOffset)
marr := tx.SelectMaps(query, queryArgs...)
rtn := make([]*HistoryItemType, len(marr))
for idx, m := range marr {
hitem := dbutil.FromMap[*HistoryItemType](m)
rtn[idx] = hitem
}
return rtn, nil
}
func GetHistoryItems(ctx context.Context, opts HistoryQueryOpts) (*HistoryQueryResult, error) {
var rtn *HistoryQueryResult
txErr := sstore.WithTx(ctx, func(tx *sstore.TxWrap) error {
var err error
rtn, err = runHistoryQueryWithFilter(tx, opts)
if err != nil {
return err
}
return nil
})
if txErr != nil {
return nil, txErr
}
return rtn, nil
}
func GetHistoryItemByLineNum(ctx context.Context, screenId string, lineNum int) (*HistoryItemType, error) {
return sstore.WithTxRtn(ctx, func(tx *sstore.TxWrap) (*HistoryItemType, error) {
query := `SELECT * FROM history WHERE screenid = ? AND linenum = ?`
hitem := dbutil.GetMapGen[*HistoryItemType](tx, query, screenId, lineNum)
return hitem, nil
})
}
func GetLastHistoryLineNum(ctx context.Context, screenId string) (int, error) {
return sstore.WithTxRtn(ctx, func(tx *sstore.TxWrap) (int, error) {
query := `SELECT COALESCE(max(linenum), 0) FROM history WHERE screenid = ?`
maxLineNum := tx.GetInt(query, screenId)
return maxLineNum, nil
})
}
func getLineIdsFromHistoryItems(historyItems []*HistoryItemType) []string {
var rtn []string
for _, hitem := range historyItems {
if hitem.LineId != "" {
rtn = append(rtn, hitem.LineId)
}
}
return rtn
}
func GetLineCmdsFromHistoryItems(ctx context.Context, historyItems []*HistoryItemType) ([]*line.LineType, []*sstore.CmdType, error) {
if len(historyItems) == 0 {
return nil, nil, nil
}
return sstore.WithTxRtn3(ctx, func(tx *sstore.TxWrap) ([]*line.LineType, []*sstore.CmdType, error) {
lineIdsJsonArr := dbutil.QuickJsonArr(getLineIdsFromHistoryItems(historyItems))
query := `SELECT * FROM line WHERE lineid IN (SELECT value FROM json_each(?))`
lineArr := dbutil.SelectMappable[*line.LineType](tx, query, lineIdsJsonArr)
query = `SELECT * FROM cmd WHERE lineid IN (SELECT value FROM json_each(?))`
cmdArr := dbutil.SelectMapsGen[*sstore.CmdType](tx, query, lineIdsJsonArr)
return lineArr, cmdArr, nil
})
}
func PurgeHistoryByIds(ctx context.Context, historyIds []string) error {
return sstore.WithTx(ctx, func(tx *sstore.TxWrap) error {
query := `DELETE FROM history WHERE historyid IN (SELECT value FROM json_each(?))`
tx.Exec(query, dbutil.QuickJsonArr(historyIds))
return nil
})
}
+16
View File
@@ -0,0 +1,16 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package history
type HistoryInfoType struct {
HistoryType string `json:"historytype"`
SessionId string `json:"sessionid,omitempty"`
ScreenId string `json:"screenid,omitempty"`
Items []*HistoryItemType `json:"items"`
Show bool `json:"show"`
}
func (HistoryInfoType) GetType() string {
return "history"
}
+4 -3
View File
@@ -23,6 +23,7 @@ import (
"github.com/wavetermdev/waveterm/wavesrv/pkg/rtnstate"
"github.com/wavetermdev/waveterm/wavesrv/pkg/scbase"
"github.com/wavetermdev/waveterm/wavesrv/pkg/sstore"
"github.com/wavetermdev/waveterm/wavesrv/pkg/telemetry"
)
const PCloudEndpoint = "https://api.waveterm.dev/central"
@@ -156,7 +157,7 @@ func SendTelemetry(ctx context.Context, force bool) error {
if !force && clientData.ClientOpts.NoTelemetry {
return nil
}
activity, err := sstore.GetNonUploadedActivity(ctx)
activity, err := telemetry.GetNonUploadedActivity(ctx)
if err != nil {
return fmt.Errorf("cannot get activity: %v", err)
}
@@ -164,7 +165,7 @@ func SendTelemetry(ctx context.Context, force bool) error {
return nil
}
log.Printf("[pcloud] sending telemetry data\n")
dayStr := sstore.GetCurDayStr()
dayStr := telemetry.GetCurDayStr()
defaultShellType := shellapi.DetectLocalShellType()
input := TelemetryInputType{UserId: clientData.UserId, ClientId: clientData.ClientId, CurDay: dayStr, DefaultShell: defaultShellType, Activity: activity}
req, err := makeAnonPostReq(ctx, TelemetryUrl, input)
@@ -175,7 +176,7 @@ func SendTelemetry(ctx context.Context, force bool) error {
if err != nil {
return err
}
err = sstore.MarkActivityAsUploaded(ctx, activity)
err = telemetry.MarkActivityAsUploaded(ctx, activity)
if err != nil {
return fmt.Errorf("error marking activity as uploaded: %v", err)
}
+6 -5
View File
@@ -11,6 +11,7 @@ import (
"github.com/wavetermdev/waveterm/wavesrv/pkg/remote"
"github.com/wavetermdev/waveterm/wavesrv/pkg/rtnstate"
"github.com/wavetermdev/waveterm/wavesrv/pkg/sstore"
"github.com/wavetermdev/waveterm/wavesrv/pkg/telemetry"
)
type NoTelemetryInputType struct {
@@ -19,11 +20,11 @@ type NoTelemetryInputType struct {
}
type TelemetryInputType struct {
UserId string `json:"userid"`
ClientId string `json:"clientid"`
CurDay string `json:"curday"`
DefaultShell string `json:"defaultshell"`
Activity []*sstore.ActivityType `json:"activity"`
UserId string `json:"userid"`
ClientId string `json:"clientid"`
CurDay string `json:"curday"`
DefaultShell string `json:"defaultshell"`
Activity []*telemetry.ActivityType `json:"activity"`
}
type WebShareUpdateType struct {
+162
View File
@@ -0,0 +1,162 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package playbook
import (
"context"
"fmt"
"github.com/google/uuid"
"github.com/wavetermdev/waveterm/wavesrv/pkg/dbutil"
"github.com/wavetermdev/waveterm/wavesrv/pkg/sstore"
)
type PlaybookType struct {
PlaybookId string `json:"playbookid"`
PlaybookName string `json:"playbookname"`
Description string `json:"description"`
EntryIds []string `json:"entryids"`
// this is not persisted to DB, just for transport to FE
Entries []*PlaybookEntry `json:"entries"`
}
func (p *PlaybookType) ToMap() map[string]interface{} {
rtn := make(map[string]interface{})
rtn["playbookid"] = p.PlaybookId
rtn["playbookname"] = p.PlaybookName
rtn["description"] = p.Description
rtn["entryids"] = dbutil.QuickJsonArr(p.EntryIds)
return rtn
}
func (p *PlaybookType) FromMap(m map[string]interface{}) bool {
dbutil.QuickSetStr(&p.PlaybookId, m, "playbookid")
dbutil.QuickSetStr(&p.PlaybookName, m, "playbookname")
dbutil.QuickSetStr(&p.Description, m, "description")
dbutil.QuickSetJsonArr(&p.Entries, m, "entries")
return true
}
// reorders p.Entries to match p.EntryIds
func (p *PlaybookType) OrderEntries() {
if len(p.Entries) == 0 {
return
}
m := make(map[string]*PlaybookEntry)
for _, entry := range p.Entries {
m[entry.EntryId] = entry
}
newList := make([]*PlaybookEntry, 0, len(p.EntryIds))
for _, entryId := range p.EntryIds {
entry := m[entryId]
if entry != nil {
newList = append(newList, entry)
}
}
p.Entries = newList
}
// removes from p.EntryIds (not from p.Entries)
func (p *PlaybookType) RemoveEntry(entryIdToRemove string) {
if len(p.EntryIds) == 0 {
return
}
newList := make([]string, 0, len(p.EntryIds)-1)
for _, entryId := range p.EntryIds {
if entryId == entryIdToRemove {
continue
}
newList = append(newList, entryId)
}
p.EntryIds = newList
}
type PlaybookEntry struct {
PlaybookId string `json:"playbookid"`
EntryId string `json:"entryid"`
Alias string `json:"alias"`
CmdStr string `json:"cmdstr"`
UpdatedTs int64 `json:"updatedts"`
CreatedTs int64 `json:"createdts"`
Description string `json:"description"`
Remove bool `json:"remove,omitempty"`
}
func CreatePlaybook(ctx context.Context, name string) (*PlaybookType, error) {
return sstore.WithTxRtn(ctx, func(tx *sstore.TxWrap) (*PlaybookType, error) {
query := `SELECT playbookid FROM playbook WHERE name = ?`
if tx.Exists(query, name) {
return nil, fmt.Errorf("playbook %q already exists", name)
}
rtn := &PlaybookType{}
rtn.PlaybookId = uuid.New().String()
rtn.PlaybookName = name
query = `INSERT INTO playbook ( playbookid, playbookname, description, entryids)
VALUES (:playbookid,:playbookname,:description,:entryids)`
tx.Exec(query, rtn.ToMap())
return rtn, nil
})
}
func selectPlaybook(tx *sstore.TxWrap, playbookId string) *PlaybookType {
query := `SELECT * FROM playbook where playbookid = ?`
playbook := dbutil.GetMapGen[*PlaybookType](tx, query, playbookId)
return playbook
}
func AddPlaybookEntry(ctx context.Context, entry *PlaybookEntry) error {
if entry.EntryId == "" {
return fmt.Errorf("invalid entryid")
}
return sstore.WithTx(ctx, func(tx *sstore.TxWrap) error {
playbook := selectPlaybook(tx, entry.PlaybookId)
if playbook == nil {
return fmt.Errorf("cannot add entry, playbook does not exist")
}
query := `SELECT entryid FROM playbook_entry WHERE entryid = ?`
if tx.Exists(query, entry.EntryId) {
return fmt.Errorf("cannot add entry, entryid already exists")
}
query = `INSERT INTO playbook_entry ( entryid, playbookid, description, alias, cmdstr, createdts, updatedts)
VALUES (:entryid,:playbookid,:description,:alias,:cmdstr,:createdts,:updatedts)`
tx.Exec(query, entry)
playbook.EntryIds = append(playbook.EntryIds, entry.EntryId)
query = `UPDATE playbook SET entryids = ? WHERE playbookid = ?`
tx.Exec(query, dbutil.QuickJsonArr(playbook.EntryIds), entry.PlaybookId)
return nil
})
}
func RemovePlaybookEntry(ctx context.Context, playbookId string, entryId string) error {
return sstore.WithTx(ctx, func(tx *sstore.TxWrap) error {
playbook := selectPlaybook(tx, playbookId)
if playbook == nil {
return fmt.Errorf("cannot remove playbook entry, playbook does not exist")
}
query := `SELECT entryid FROM playbook_entry WHERE entryid = ?`
if !tx.Exists(query, entryId) {
return fmt.Errorf("cannot remove playbook entry, entry does not exist")
}
query = `DELETE FROM playbook_entry WHERE entryid = ?`
tx.Exec(query, entryId)
playbook.RemoveEntry(entryId)
query = `UPDATE playbook SET entryids = ? WHERE playbookid = ?`
tx.Exec(query, dbutil.QuickJsonArr(playbook.EntryIds), playbookId)
return nil
})
}
func GetPlaybookById(ctx context.Context, playbookId string) (*PlaybookType, error) {
return sstore.WithTxRtn(ctx, func(tx *sstore.TxWrap) (*PlaybookType, error) {
rtn := selectPlaybook(tx, playbookId)
if rtn == nil {
return nil, nil
}
query := `SELECT * FROM playbook_entry WHERE playbookid = ?`
tx.Select(&rtn.Entries, query, playbookId)
rtn.OrderEntries()
return rtn, nil
})
}
+4 -3
View File
@@ -39,6 +39,7 @@ import (
"github.com/wavetermdev/waveterm/wavesrv/pkg/scbus"
"github.com/wavetermdev/waveterm/wavesrv/pkg/scpacket"
"github.com/wavetermdev/waveterm/wavesrv/pkg/sstore"
"github.com/wavetermdev/waveterm/wavesrv/pkg/telemetry"
"github.com/wavetermdev/waveterm/wavesrv/pkg/userinput"
"golang.org/x/crypto/ssh"
@@ -1419,8 +1420,8 @@ func getStateVarsFromInitPk(initPk *packet.InitPacketType) map[string]string {
return rtn
}
func makeReinitErrorUpdate(shellType string) sstore.ActivityUpdate {
rtn := sstore.ActivityUpdate{}
func makeReinitErrorUpdate(shellType string) telemetry.ActivityUpdate {
rtn := telemetry.ActivityUpdate{}
if shellType == packet.ShellType_bash {
rtn.ReinitBashErrors = 1
} else if shellType == packet.ShellType_zsh {
@@ -1441,7 +1442,7 @@ func (msh *MShellProc) ReInit(ctx context.Context, ck base.CommandKey, shellType
}
defer func() {
if rtnErr != nil {
sstore.UpdateActivityWrap(ctx, makeReinitErrorUpdate(shellType), "reiniterror")
telemetry.UpdateActivityWrap(ctx, makeReinitErrorUpdate(shellType), "reiniterror")
}
}()
startTs := time.Now()
File diff suppressed because it is too large Load Diff
-65
View File
@@ -11,10 +11,8 @@ import (
"io/fs"
"log"
"os"
"path"
"time"
"github.com/google/uuid"
"github.com/wavetermdev/waveterm/waveshell/pkg/cirfile"
"github.com/wavetermdev/waveterm/waveshell/pkg/shexec"
"github.com/wavetermdev/waveterm/wavesrv/pkg/scbase"
@@ -126,69 +124,6 @@ func ReadPtyOutFile(ctx context.Context, screenId string, lineId string, offset
return f.ReadAtWithMax(ctx, offset, maxSize)
}
type SessionDiskSizeType struct {
NumFiles int
TotalSize int64
ErrorCount int
Location string
}
func directorySize(dirName string) (SessionDiskSizeType, error) {
var rtn SessionDiskSizeType
rtn.Location = dirName
entries, err := os.ReadDir(dirName)
if err != nil {
return rtn, err
}
for _, entry := range entries {
if entry.IsDir() {
rtn.ErrorCount++
continue
}
finfo, err := entry.Info()
if err != nil {
rtn.ErrorCount++
continue
}
rtn.NumFiles++
rtn.TotalSize += finfo.Size()
}
return rtn, nil
}
func SessionDiskSize(sessionId string) (SessionDiskSizeType, error) {
sessionDir, err := scbase.EnsureSessionDir(sessionId)
if err != nil {
return SessionDiskSizeType{}, err
}
return directorySize(sessionDir)
}
func FullSessionDiskSize() (map[string]SessionDiskSizeType, error) {
sdir := scbase.GetSessionsDir()
entries, err := os.ReadDir(sdir)
if err != nil {
return nil, err
}
rtn := make(map[string]SessionDiskSizeType)
for _, entry := range entries {
if !entry.IsDir() {
continue
}
name := entry.Name()
_, err = uuid.Parse(name)
if err != nil {
continue
}
diskSize, err := directorySize(path.Join(sdir, name))
if err != nil {
continue
}
rtn[name] = diskSize
}
return rtn, nil
}
func DeletePtyOutFile(ctx context.Context, screenId string, lineId string) error {
ptyOutFileName, err := scbase.PtyOutFile(screenId, lineId)
if err != nil {
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More