mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-01-04 18:59:08 +01:00
Merge branch 'main' of github.com:wavetermdev/waveterm into red/new-tabs
This commit is contained in:
commit
5ae6a32222
@ -7,16 +7,6 @@
|
||||
:root {
|
||||
--fa-style-family: "Font Awesome 6 Sharp";
|
||||
|
||||
/* these variables are overridden by user settings */
|
||||
/*
|
||||
--termfontfamily: "Hack";
|
||||
--termfontsize: 13px;
|
||||
--termlineheight: 15px;
|
||||
--termpad: 7px;
|
||||
--termfontsize-sm: 11px;
|
||||
--termlineheight-sm: 13px;
|
||||
*/
|
||||
|
||||
/* base fonts */
|
||||
--base-font: normal 15px / normal "Lato", sans-serif;
|
||||
|
||||
@ -52,12 +42,10 @@
|
||||
--app-bg-color: black;
|
||||
--app-accent-color: rgb(88, 193, 66);
|
||||
--app-accent-bg-color: rgba(88, 193, 66, 0.25);
|
||||
--app-error-color: rgb(229, 77, 46);
|
||||
--app-warning-color: rgb(224, 185, 86);
|
||||
--app-success-color: rgb(78, 154, 6);
|
||||
--app-text-color: rgb(211, 215, 207);
|
||||
--app-text-primary-color: rgb(255, 255, 255);
|
||||
--app-text-secondary-color: rgb(195, 200, 194);
|
||||
--app-text-disabled-color: rgb(173, 173, 173);
|
||||
--app-border-color: rgb(51, 51, 51);
|
||||
--app-maincontent-bg-color: #333;
|
||||
--app-panel-bg-color: rgba(21, 23, 21, 1);
|
||||
@ -65,17 +53,15 @@
|
||||
--app-icon-color: rgb(139, 145, 138);
|
||||
--app-icon-hover-color: #fff;
|
||||
--app-selected-mask-color: rgba(255, 255, 255, 0.06);
|
||||
|
||||
/* icon colors */
|
||||
--app-error-color: rgb(229, 77, 46);
|
||||
--app-warning-color: rgb(224, 185, 86);
|
||||
--app-success-color: rgb(78, 154, 6);
|
||||
--app-idle-color: var(--app-text-secondary-color);
|
||||
|
||||
/* just for macos */
|
||||
--app-border-radius-darwin: 10px;
|
||||
|
||||
/* global generic colors */
|
||||
--app-black: rgb(0, 0, 0);
|
||||
|
||||
/* scrollbar colors */
|
||||
/* --scrollbar-background-color: rgba(21, 23, 21, 1); */
|
||||
--scrollbar-background-color: var(--app-bg-color);
|
||||
--scrollbar-thumb-color: rgba(255, 255, 255, 0.3);
|
||||
--scrollbar-thumb-hover-color: rgba(255, 255, 255, 0.5);
|
||||
@ -116,28 +102,9 @@
|
||||
--markdown-bg-color: rgb(35, 35, 35);
|
||||
--markdown-outline-color: var(--form-element-primary-color);
|
||||
|
||||
/* status(remote) colors */
|
||||
/* todo: all status colors must be unified */
|
||||
--status-connected-color: var(--app-success-color);
|
||||
--status-connecting-color: var(--app-warning-color);
|
||||
--status-error-color: var(--app-error-color);
|
||||
--status-disconnected-color: var(--app-text-secondary-color);
|
||||
|
||||
/* status indicator colors */
|
||||
/* todo: all status colors must be unified */
|
||||
--status-indicator-color: var(--app-text-color);
|
||||
--status-indicator-error: var(--status-error-color);
|
||||
--status-indicator-success: var(--status-connected-color);
|
||||
|
||||
/* status(version) colors */
|
||||
/* todo: all status colors must be unified */
|
||||
--status-outdated-color: var(--status-connecting-color);
|
||||
--status-updated-color: var(--status-connected-color);
|
||||
|
||||
/* term status colors */
|
||||
/* todo: all status colors must be unified */
|
||||
--term-error-color: var(--status-error-color);
|
||||
--term-warning-color: var(--status-connecting-color);
|
||||
--term-error-color: var(--app-error-color);
|
||||
--term-warning-color: var(--app-warning-color);
|
||||
|
||||
/* hotkey colors */
|
||||
--hotkey-text-color: var(--app-text-secondary-color);
|
||||
@ -178,17 +145,6 @@
|
||||
--line-actions-active-color: rgba(255, 255, 255, 1);
|
||||
--line-actions-bg-color: rgba(255, 255, 255, 0.15);
|
||||
|
||||
/* view colors */
|
||||
/* todo: bookmarks is a view, colors must be unified with --view* colors */
|
||||
--bookmarks-text-color: rgb(211, 215, 207);
|
||||
--bookmarks-textarea-bg-color: rgb(0, 0, 0);
|
||||
--bookmarks-disabled-text-color: rgb(173, 173, 173);
|
||||
--bookmarks-control-hover-color: rgb(255, 255, 255);
|
||||
|
||||
/* view colors */
|
||||
--view-error-color: var(--app-error-color);
|
||||
--view-text-color: var(--app-text-color);
|
||||
|
||||
/* table colors */
|
||||
--table-border-color: rgba(241, 246, 243, 0.15);
|
||||
--table-thead-border-top-color: rgba(250, 250, 250, 0.1);
|
||||
@ -199,9 +155,6 @@
|
||||
--table-tr-selected-bg-color: #222;
|
||||
--table-tr-selected-hover-bg-color: #333;
|
||||
|
||||
/* session colors */
|
||||
--session-bg-color: rgba(13, 13, 13, 0.85);
|
||||
|
||||
/* cmdinput colors */
|
||||
--cmdinput-textarea-bg-color: #171717;
|
||||
--cmdinput-text-error-color: var(--term-red);
|
||||
@ -223,4 +176,12 @@
|
||||
--modal-header-bottom-border-color: rgba(241, 246, 243, 0.15);
|
||||
|
||||
--logo-button-hover-bg-color: var(--app-accent-bg-color);
|
||||
|
||||
--xterm-viewport-border-color: rgba(241, 246, 243, 0.15);
|
||||
|
||||
--datepicker-cell-hover-bg-color: rgba(255, 255, 255, 0.06);
|
||||
--datepicker-cell-other-text-color: rgba(255, 255, 255, 0.3);
|
||||
--datepicker-header-fade-color: rgba(255, 255, 255, 0.4);
|
||||
--datepicker-year-header-bg-color: rgba(255, 255, 255, 0.2); /* Light grey background */
|
||||
--datepicker-year-header-border-color: rgba(241, 246, 243, 0.15); /* Light grey border */
|
||||
}
|
||||
|
@ -31,7 +31,7 @@
|
||||
--button-secondary-bg-color: rgba(200, 200, 200, 0.3); /* Semi-transparent light gray */
|
||||
|
||||
/* view colors */
|
||||
--view-text-color: var(--app-text-color);
|
||||
--app-text-color: var(--app-text-color);
|
||||
|
||||
/* table colors */
|
||||
--table-thead-border-top-color: rgba(0, 0, 0, 0.2);
|
||||
@ -71,6 +71,16 @@
|
||||
--line-actions-inactive-color: rgba(0, 0, 0, 0.3);
|
||||
--line-actions-active-color: rgba(0, 0, 0, 1);
|
||||
|
||||
--logo-button-hover-bg-color: #f0f0f0;
|
||||
|
||||
--xterm-viewport-border-color: rgba(0, 0, 0, 0.3);
|
||||
|
||||
--datepicker-cell-hover-bg-color: #f0f0f0;
|
||||
--datepicker-cell-other-text-color: rgba(0, 0, 0, 0.3);
|
||||
--datepicker-header-fade-color: rgba(0, 0, 0, 0.4);
|
||||
--datepicker-year-header-bg-color: #f5f5f5;
|
||||
--datepicker-year-header-border-color: #dcdcdc;
|
||||
|
||||
/* toggle colors */
|
||||
--toggle-thumb-color: var(--app-bg-color);
|
||||
}
|
||||
|
@ -735,28 +735,28 @@ a.a-block {
|
||||
.status-icon.status-connected {
|
||||
path,
|
||||
circle {
|
||||
fill: var(--status-connected-color);
|
||||
fill: var(--app-success-color);
|
||||
}
|
||||
}
|
||||
|
||||
.status-icon.status-connecting {
|
||||
path,
|
||||
circle {
|
||||
fill: var(--status-connecting-color);
|
||||
fill: var(--app-warning-color);
|
||||
}
|
||||
}
|
||||
|
||||
.status-icon.status-disconnected {
|
||||
path,
|
||||
circle {
|
||||
fill: var(--status-disconnected-color);
|
||||
fill: var(--app-idle-color);
|
||||
}
|
||||
}
|
||||
|
||||
.status-icon.status-error {
|
||||
path,
|
||||
circle {
|
||||
fill: var(--status-error-color);
|
||||
fill: var(--app-text-disabled-color);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6,11 +6,11 @@
|
||||
}
|
||||
|
||||
.bookmarks-list {
|
||||
color: var(--bookmarks-text-color);
|
||||
color: var(--app-text-color);
|
||||
margin: 4px 10px 5px 5px;
|
||||
|
||||
.no-bookmarks {
|
||||
color: var(--bookmarks-text-color);
|
||||
color: var(--app-text-color);
|
||||
padding: 30px 10px 35px 10px;
|
||||
border-bottom: 1px solid white;
|
||||
}
|
||||
@ -46,21 +46,21 @@
|
||||
}
|
||||
|
||||
label {
|
||||
color: var(--bookmarks-text-color);
|
||||
color: var(--app-text-color);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 80%;
|
||||
min-width: 50%;
|
||||
color: var(--bookmarks-text-color);
|
||||
background-color: var(--bookmarks-textarea-bg-color);
|
||||
color: var(--app-text-color);
|
||||
background-color: var(--form-element-bg-color);
|
||||
}
|
||||
|
||||
.bookmark-id-div {
|
||||
display: none;
|
||||
position: absolute;
|
||||
color: var(--bookmarks-disabled-text-color);
|
||||
color: var(--app-text-disabled-color);
|
||||
right: 5px;
|
||||
bottom: 2px;
|
||||
font-size: 0.8em;
|
||||
@ -75,7 +75,7 @@
|
||||
flex-direction: row;
|
||||
|
||||
visibility: hidden;
|
||||
color: var(--bookmarks-text-color);
|
||||
color: var(--app-text-color);
|
||||
|
||||
.bookmark-control:first-child {
|
||||
margin-left: 0;
|
||||
@ -85,10 +85,6 @@
|
||||
margin-left: 10px;
|
||||
cursor: pointer;
|
||||
padding: 2px;
|
||||
|
||||
&:hover {
|
||||
color: var(--bookmarks-control-hover-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,6 @@
|
||||
.wave-button {
|
||||
background: none;
|
||||
color: inherit;
|
||||
border: none;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
outline: inherit;
|
||||
display: flex;
|
||||
@ -12,64 +10,64 @@
|
||||
border-radius: 6px;
|
||||
height: auto;
|
||||
line-height: 1.5;
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
|
||||
color: var(--form-element-text-color);
|
||||
background: var(--form-element-primary-color);
|
||||
i {
|
||||
fill: var(--form-element-text-color);
|
||||
}
|
||||
|
||||
&.primary {
|
||||
color: var(--form-element-text-color);
|
||||
background: var(--form-element-primary-color);
|
||||
i {
|
||||
fill: var(--form-element-text-color);
|
||||
}
|
||||
}
|
||||
|
||||
&.primary.outlined {
|
||||
background: none;
|
||||
border: 1px solid var(--form-element-primary-color);
|
||||
|
||||
i {
|
||||
fill: var(--form-element-primary-color);
|
||||
}
|
||||
}
|
||||
|
||||
&.solid {
|
||||
color: var(--form-element-text-color);
|
||||
background: var(--form-element-primary-color);
|
||||
|
||||
i {
|
||||
fill: var(--form-element-text-color);
|
||||
}
|
||||
}
|
||||
|
||||
&.outlined {
|
||||
background: none;
|
||||
border: 1px solid var(--form-element-primary-color);
|
||||
}
|
||||
|
||||
&.ghost {
|
||||
background: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--form-element-text-color);
|
||||
&.primary.ghost {
|
||||
background: none;
|
||||
i {
|
||||
fill: var(--form-element-primary-color);
|
||||
}
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
color: var(--form-element-text-color);
|
||||
background: none;
|
||||
color: var(--form-element-text-color);
|
||||
background: var(--form-element-secondary-color);
|
||||
box-shadow: none;
|
||||
|
||||
&.solid {
|
||||
color: var(--form-element-text-color);
|
||||
background: var(--form-element-secondary-color);
|
||||
box-shadow: none;
|
||||
i {
|
||||
fill: var(--form-element-text-color);
|
||||
}
|
||||
}
|
||||
|
||||
&.outlined {
|
||||
background: none;
|
||||
border: 1px solid var(--form-element-secondary-color);
|
||||
}
|
||||
&.secondary.outlined {
|
||||
background: none;
|
||||
border: 1px solid var(--form-element-text-color);
|
||||
}
|
||||
|
||||
&.ghost {
|
||||
background: none;
|
||||
padding: 6px 10px;
|
||||
&.secondary.ghost {
|
||||
background: none;
|
||||
}
|
||||
|
||||
i {
|
||||
fill: var(--form-element-primary-color);
|
||||
}
|
||||
}
|
||||
&.secondary.danger {
|
||||
color: var(--app-text-disabled-color);
|
||||
}
|
||||
|
||||
&.small {
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
border-radius: 3.6px;
|
||||
}
|
||||
|
||||
&.term-inline {
|
||||
|
183
src/app/common/elements/datepicker.less
Normal file
183
src/app/common/elements/datepicker.less
Normal file
@ -0,0 +1,183 @@
|
||||
.day-picker-modal {
|
||||
background-color: var(--form-element-bg-color);
|
||||
z-index: 1000;
|
||||
width: 250px;
|
||||
height: 293px;
|
||||
font-size: 13px;
|
||||
border: 1px solid var(--form-element-border-color);
|
||||
border-radius: 4px;
|
||||
color: var(--form-element-text-color);
|
||||
}
|
||||
|
||||
.day-picker-input {
|
||||
cursor: default;
|
||||
border: 1px solid var(--form-element-border-color) !important;
|
||||
height: 34px;
|
||||
user-select: none;
|
||||
display: flex;
|
||||
border-radius: 4px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 15px !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
input {
|
||||
outline: none !important;
|
||||
border: none !important;
|
||||
height: 100%;
|
||||
padding: 0 !important;
|
||||
cursor: default !important;
|
||||
}
|
||||
|
||||
.fa-calendar {
|
||||
margin-left: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.day-picker-header {
|
||||
color: var(--form-element-text-color);
|
||||
padding: 10px 10px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 40px;
|
||||
position: sticky;
|
||||
|
||||
div {
|
||||
cursor: default;
|
||||
font-weight: bold;
|
||||
user-select: none;
|
||||
|
||||
&.fade {
|
||||
color: var(--datepicker-header-fade-color);
|
||||
}
|
||||
}
|
||||
|
||||
.arrows {
|
||||
display: flex;
|
||||
|
||||
.wave-button {
|
||||
padding: 5px !important;
|
||||
font-size: 18px !important;
|
||||
cursor: default !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.day-header {
|
||||
display: flex;
|
||||
color: var(--form-element-text-color);
|
||||
padding: 0 10px;
|
||||
|
||||
.day-header-cell {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
color: var(--form-element-text-color);
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
|
||||
.day-picker {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: 0;
|
||||
padding: 0 10px 10px;
|
||||
|
||||
.day {
|
||||
padding: 10px 4px;
|
||||
color: var(--form-element-text-color);
|
||||
text-align: center;
|
||||
cursor: default;
|
||||
border-radius: 3px;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--datepicker-cell-hover-bg-color);
|
||||
}
|
||||
&.selected {
|
||||
background-color: var(--form-element-primary-color);
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.other-month {
|
||||
color: var(--datepicker-cell-other-text-color);
|
||||
}
|
||||
}
|
||||
|
||||
.year-month-accordion-wrapper {
|
||||
overflow-y: auto;
|
||||
height: 243px;
|
||||
margin-top: 8px;
|
||||
padding: 0 10px 10px;
|
||||
|
||||
.year-month-accordion {
|
||||
color: var(--form-element-text-color);
|
||||
}
|
||||
|
||||
.year-header {
|
||||
padding: 5px 10px;
|
||||
background-color: var(--datepicker-year-header-bg-color);
|
||||
border-bottom: 1px solid var(--datepicker-year-header-border-color);
|
||||
margin-bottom: 5px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
cursor: default;
|
||||
color: var(--form-element-text-color);
|
||||
user-select: none;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.month-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 5px;
|
||||
padding: 10px;
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.3s ease;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.month {
|
||||
padding: 5px;
|
||||
text-align: center;
|
||||
border-radius: 3px;
|
||||
color: var(--form-element-text-color);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--datepicker-cell-hover-bg-color);
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background-color: var(--form-element-primary-color);
|
||||
}
|
||||
}
|
||||
|
||||
.expanded {
|
||||
max-height: 500px;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-arrow {
|
||||
display: inline-block;
|
||||
margin-left: 5px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 5px solid transparent;
|
||||
border-right: 5px solid transparent;
|
||||
border-top: 5px solid var(--form-element-text-color);
|
||||
vertical-align: middle;
|
||||
user-select: none;
|
||||
|
||||
&.fade {
|
||||
border-top: 5px solid var(--datepicker-header-fade-color);
|
||||
}
|
||||
}
|
446
src/app/common/elements/datepicker.tsx
Normal file
446
src/app/common/elements/datepicker.tsx
Normal file
@ -0,0 +1,446 @@
|
||||
import React, { useState, useEffect, useRef, createRef } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import dayjs from "dayjs";
|
||||
import cs from "classnames";
|
||||
import { Button } from "@/elements";
|
||||
import { If } from "tsx-control-statements/components";
|
||||
|
||||
import "./datepicker.less";
|
||||
|
||||
interface YearRefs {
|
||||
[key: number]: React.RefObject<HTMLDivElement>;
|
||||
}
|
||||
|
||||
type DatePickerProps = {
|
||||
selectedDate: Date;
|
||||
onSelectDate: (date: Date) => void;
|
||||
format?: string;
|
||||
};
|
||||
|
||||
const DatePicker: React.FC<DatePickerProps> = ({ selectedDate, format = "MM/DD/YYYY", onSelectDate }) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [selDate, setSelDate] = useState(dayjs(selectedDate)); // Initialize with dayjs object
|
||||
const [showYearAccordion, setShowYearAccordion] = useState(false);
|
||||
const [expandedYear, setExpandedYear] = useState<number | null>(selDate.year());
|
||||
const yearRefs = useRef<YearRefs>({});
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
const modalRef = useRef<HTMLDivElement>(null);
|
||||
const calendarIconRef = useRef(null);
|
||||
const inputRefs = useRef({ YYYY: null, MM: null, DD: null });
|
||||
// Extract delimiter using regex
|
||||
const delimiter = format.replace(/[0-9YMD]/g, "")[0] || "/";
|
||||
// Split format and create state for each part
|
||||
const formatParts = format.split(delimiter);
|
||||
const [dateParts, setDateParts] = useState({
|
||||
YYYY: selDate.format("YYYY"),
|
||||
MM: selDate.format("MM"),
|
||||
DD: selDate.format("DD"),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
inputRefs.current = {
|
||||
YYYY: createRef(),
|
||||
MM: createRef(),
|
||||
DD: createRef(),
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (showYearAccordion && expandedYear && yearRefs.current[expandedYear]) {
|
||||
yearRefs.current[expandedYear].current?.scrollIntoView({
|
||||
block: "nearest",
|
||||
});
|
||||
}
|
||||
}, [showYearAccordion, expandedYear]);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setDateParts({
|
||||
YYYY: selDate.format("YYYY"),
|
||||
MM: selDate.format("MM"),
|
||||
DD: selDate.format("DD"),
|
||||
});
|
||||
}, [selDate]);
|
||||
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
// Check if the click is on the calendar icon
|
||||
if (calendarIconRef.current && calendarIconRef.current.contains(event.target as Node)) {
|
||||
// Click is on the calendar icon, do nothing
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the click is outside the modal
|
||||
if (modalRef.current && !modalRef.current.contains(event.target as Node)) {
|
||||
setIsOpen(false); // Close the modal
|
||||
}
|
||||
};
|
||||
|
||||
const handleDayClick = (date: Date) => {
|
||||
const newDate = dayjs(date);
|
||||
setSelDate(newDate); // Update selDate with the new dayjs object
|
||||
onSelectDate && onSelectDate(date); // Call parent's onSelectDate
|
||||
setIsOpen(false); // Close the picker
|
||||
};
|
||||
|
||||
const changeMonth = (delta: number) => {
|
||||
const newDate = selDate.add(delta, "month");
|
||||
setSelDate(newDate);
|
||||
onSelectDate && onSelectDate(newDate.toDate());
|
||||
};
|
||||
|
||||
const renderHeader = () => {
|
||||
return (
|
||||
<div className="day-picker-header">
|
||||
<div
|
||||
className={cs({ fade: showYearAccordion })}
|
||||
onClick={() => {
|
||||
if (!showYearAccordion) {
|
||||
setExpandedYear(selDate.year()); // Set expandedYear when opening accordion
|
||||
}
|
||||
setShowYearAccordion(!showYearAccordion);
|
||||
}}
|
||||
>
|
||||
{selDate.format("MMMM YYYY")}
|
||||
<span className={cs("dropdown-arrow", { fade: showYearAccordion })}></span>
|
||||
</div>
|
||||
<If condition={!showYearAccordion}>
|
||||
<div className="arrows">
|
||||
<Button className="secondary ghost" onClick={() => changeMonth(-1)}>
|
||||
↑
|
||||
</Button>
|
||||
<Button className="secondary ghost" onClick={() => changeMonth(1)}>
|
||||
↓
|
||||
</Button>
|
||||
</div>
|
||||
</If>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderDayHeaders = () => {
|
||||
const daysOfWeek = ["S", "M", "T", "W", "T", "F", "S"]; // First letter of each day
|
||||
return (
|
||||
<div className="day-header">
|
||||
{daysOfWeek.map((day, i) => (
|
||||
<div key={`${day}-${i}`} className="day-header-cell">
|
||||
{day}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderDays = () => {
|
||||
const days = [];
|
||||
const startDay = selDate.startOf("month");
|
||||
const endDay = selDate.endOf("month");
|
||||
const startDate = startDay.day(); // 0 for Sunday, 1 for Monday, ..., 6 for Saturday
|
||||
|
||||
// Previous month's filler days
|
||||
const previousMonth = startDay.subtract(1, "month");
|
||||
const daysInPreviousMonth = previousMonth.daysInMonth();
|
||||
for (let i = daysInPreviousMonth - startDate + 1; i <= daysInPreviousMonth; i++) {
|
||||
const dayDate = previousMonth.date(i);
|
||||
days.push(
|
||||
<div
|
||||
key={`prev-month-day-${i}`}
|
||||
className="day other-month"
|
||||
onClick={() => handleDayClick(dayDate.toDate())}
|
||||
>
|
||||
{i}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Current month's days
|
||||
for (
|
||||
let dayCount = 1;
|
||||
startDay.add(dayCount - 1, "day").isBefore(endDay) ||
|
||||
startDay.add(dayCount - 1, "day").isSame(endDay, "day");
|
||||
dayCount++
|
||||
) {
|
||||
const currentDate = startDay.add(dayCount - 1, "day");
|
||||
days.push(
|
||||
<div
|
||||
key={dayCount}
|
||||
className={`day ${selDate.isSame(currentDate, "day") ? "selected" : ""}`}
|
||||
onClick={() => handleDayClick(currentDate.toDate())}
|
||||
>
|
||||
{dayCount}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Next month's filler days
|
||||
while (days.length < 42) {
|
||||
const fillerDayCount = days.length - daysInPreviousMonth - endDay.date();
|
||||
const dayDate = endDay.add(fillerDayCount + 1, "day");
|
||||
days.push(
|
||||
<div
|
||||
key={`next-month-day-${dayDate.format("YYYY-MM-DD")}`}
|
||||
className="day other-month"
|
||||
onClick={() => handleDayClick(dayDate.toDate())}
|
||||
>
|
||||
{dayDate.date()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return days;
|
||||
};
|
||||
|
||||
const calculatePosition = (): React.CSSProperties => {
|
||||
if (wrapperRef.current) {
|
||||
const rect = wrapperRef.current.getBoundingClientRect();
|
||||
return {
|
||||
position: "absolute",
|
||||
top: `${rect.bottom + window.scrollY + 2}px`,
|
||||
left: `${rect.left + window.scrollX}px`,
|
||||
};
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
const populateYears = () => {
|
||||
const currentYear = dayjs().year();
|
||||
const startYear = currentYear - 10;
|
||||
const endYear = currentYear + 10;
|
||||
const yearsRange = [];
|
||||
|
||||
for (let year = startYear; year <= endYear; year++) {
|
||||
yearsRange.push(year);
|
||||
yearRefs.current[year] = React.createRef();
|
||||
}
|
||||
|
||||
return yearsRange;
|
||||
};
|
||||
|
||||
const handleMonthYearSelect = (month: number, year: number) => {
|
||||
const newDate = dayjs(new Date(year, month - 1));
|
||||
setSelDate(newDate);
|
||||
setShowYearAccordion(false); // Close accordion
|
||||
onSelectDate && onSelectDate(newDate.toDate());
|
||||
};
|
||||
|
||||
const renderYearMonthAccordion = () => {
|
||||
const years = populateYears();
|
||||
const currentYear = selDate.year();
|
||||
|
||||
return (
|
||||
<div className="year-month-accordion-wrapper">
|
||||
<div className="year-month-accordion">
|
||||
{years.map((year) => (
|
||||
<div key={year} ref={yearRefs.current[year]}>
|
||||
<div
|
||||
className="year-header"
|
||||
data-year={year}
|
||||
onClick={() => setExpandedYear(year === expandedYear ? null : year)}
|
||||
>
|
||||
{year}
|
||||
</div>
|
||||
<If condition={expandedYear === year}>
|
||||
<div
|
||||
className={cs("month-container", {
|
||||
expanded: expandedYear === year,
|
||||
})}
|
||||
>
|
||||
{Array.from({ length: 12 }, (_, i) => i + 1).map((month) => (
|
||||
<div
|
||||
key={month}
|
||||
className={cs("month", {
|
||||
selected: year === currentYear && month === selDate.month() + 1,
|
||||
})}
|
||||
onClick={() => handleMonthYearSelect(month, year)}
|
||||
>
|
||||
{dayjs(new Date(year, month - 1)).format("MMM")}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</If>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const toggleModal = () => {
|
||||
setIsOpen((prevIsOpen) => !prevIsOpen);
|
||||
setShowYearAccordion(false);
|
||||
};
|
||||
|
||||
const dayPickerModal = isOpen
|
||||
? ReactDOM.createPortal(
|
||||
<div ref={modalRef} className="day-picker-modal" style={calculatePosition()}>
|
||||
{renderHeader()}
|
||||
{showYearAccordion && renderYearMonthAccordion()}
|
||||
<If condition={!showYearAccordion}>
|
||||
<>
|
||||
{renderDayHeaders()}
|
||||
<div className="day-picker">{renderDays()}</div>
|
||||
</>
|
||||
</If>
|
||||
</div>,
|
||||
document.getElementById("app")!
|
||||
)
|
||||
: null;
|
||||
|
||||
const handleDatePartChange = (part, value) => {
|
||||
const newDateParts = { ...dateParts, [part]: value };
|
||||
setDateParts(newDateParts);
|
||||
|
||||
// Construct a new date from the updated parts
|
||||
const newDate = dayjs(`${newDateParts.YYYY}-${newDateParts.MM}-${newDateParts.DD}`);
|
||||
if (newDate.isValid()) {
|
||||
onSelectDate(newDate.toDate()); // Call onSelectDate with the new date
|
||||
}
|
||||
};
|
||||
|
||||
const handleArrowNavigation = (event, currentPart) => {
|
||||
const currentIndex = formatParts.indexOf(currentPart);
|
||||
let targetInput;
|
||||
|
||||
if (event.key === "ArrowLeft" && currentIndex > 0) {
|
||||
targetInput = inputRefs.current[formatParts[currentIndex - 1]].current;
|
||||
} else if (event.key === "ArrowRight" && currentIndex < formatParts.length - 1) {
|
||||
targetInput = inputRefs.current[formatParts[currentIndex + 1]].current;
|
||||
}
|
||||
|
||||
if (targetInput) {
|
||||
targetInput.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (event, currentPart) => {
|
||||
const key = event.key;
|
||||
|
||||
if (key === "ArrowLeft" || key === "ArrowRight") {
|
||||
// Handle arrow navigation without selecting text
|
||||
handleArrowNavigation(event, currentPart);
|
||||
return;
|
||||
}
|
||||
|
||||
if (key === " ") {
|
||||
// Handle spacebar press to toggle the modal
|
||||
toggleModal();
|
||||
return;
|
||||
}
|
||||
|
||||
if (key.match(/[0-9]/)) {
|
||||
// Handle numeric keys
|
||||
event.preventDefault();
|
||||
const maxLength = currentPart === "YYYY" ? 4 : 2;
|
||||
const newValue = event.target.value.length < maxLength ? event.target.value + key : key;
|
||||
let selectionTimeoutId = null;
|
||||
handleDatePartChange(currentPart, newValue);
|
||||
|
||||
// Clear any existing timeout
|
||||
if (selectionTimeoutId !== null) {
|
||||
clearTimeout(selectionTimeoutId);
|
||||
}
|
||||
|
||||
// Re-focus and select the input after state update
|
||||
selectionTimeoutId = setTimeout(() => {
|
||||
event.target.focus();
|
||||
event.target.select();
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFocus = (event) => {
|
||||
event.target.select();
|
||||
};
|
||||
|
||||
// Prevent use from selecting text in the input
|
||||
const handleMouseDown = (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
handleFocus(event);
|
||||
};
|
||||
|
||||
const handleIconKeyDown = (event) => {
|
||||
if (event.key === "Enter") {
|
||||
toggleModal();
|
||||
}
|
||||
};
|
||||
|
||||
const setInputWidth = (inputRef, value) => {
|
||||
const span = document.createElement("span");
|
||||
document.body.appendChild(span);
|
||||
span.style.font = "inherit";
|
||||
span.style.visibility = "hidden";
|
||||
span.style.position = "absolute";
|
||||
span.textContent = value;
|
||||
const textWidth = span.offsetWidth;
|
||||
document.body.removeChild(span);
|
||||
|
||||
if (inputRef.current) {
|
||||
inputRef.current.style.width = `${textWidth}px`;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// This timeout ensures that the effect runs after the DOM updates
|
||||
const timeoutId = setTimeout(() => {
|
||||
formatParts.forEach((part) => {
|
||||
const inputRef = inputRefs.current[part];
|
||||
if (inputRef && inputRef.current) {
|
||||
setInputWidth(inputRef, dateParts[part]);
|
||||
}
|
||||
});
|
||||
}, 0);
|
||||
|
||||
return () => clearTimeout(timeoutId); // Cleanup timeout on unmount
|
||||
}, []);
|
||||
|
||||
const renderDatePickerInput = () => {
|
||||
return (
|
||||
<div className="day-picker-input">
|
||||
{formatParts.map((part, index) => {
|
||||
const inputRef = inputRefs.current[part];
|
||||
|
||||
return (
|
||||
<React.Fragment key={part}>
|
||||
{index > 0 && <span>{delimiter}</span>}
|
||||
<input
|
||||
readOnly
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={dateParts[part]}
|
||||
onChange={(e) => handleDatePartChange(part, e.target.value)}
|
||||
onKeyDown={(e) => handleKeyDown(e, part)}
|
||||
onMouseDown={handleMouseDown}
|
||||
onFocus={handleFocus}
|
||||
maxLength={part === "YYYY" ? 4 : 2}
|
||||
className="date-input"
|
||||
placeholder={part}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
<i
|
||||
ref={calendarIconRef}
|
||||
className="fa-sharp fa-regular fa-calendar"
|
||||
onClick={toggleModal}
|
||||
onKeyDown={handleIconKeyDown}
|
||||
tabIndex={0} // Makes the icon focusable
|
||||
role="button" // Semantic role for accessibility
|
||||
aria-label="Toggle date picker" // Accessible label for screen readers
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={wrapperRef}>
|
||||
{renderDatePickerInput()}
|
||||
{dayPickerModal}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { DatePicker };
|
@ -17,3 +17,4 @@ export { TextField } from "./textfield";
|
||||
export { Toggle } from "./toggle";
|
||||
export { Tooltip } from "./tooltip";
|
||||
export { TabIcon } from "./tabicon";
|
||||
export { DatePicker } from "./datepicker";
|
||||
|
@ -11,18 +11,18 @@
|
||||
}
|
||||
|
||||
.dot.green {
|
||||
background-color: var(--status-connected-color);
|
||||
background-color: var(--app-success-color);
|
||||
}
|
||||
|
||||
.dot.red {
|
||||
background-color: var(--status-error-color);
|
||||
background-color: var(--app-text-disabled-color);
|
||||
}
|
||||
|
||||
.dot.gray {
|
||||
background-color: var(--status-disconnected-color);
|
||||
background-color: var(--app-idle-color);
|
||||
}
|
||||
|
||||
.dot.yellow {
|
||||
background-color: var(--status-connecting-color);
|
||||
background-color: var(--app-warning-color);
|
||||
}
|
||||
}
|
||||
|
@ -73,18 +73,18 @@ The following accounts for a debounce in the status indicator. We will only disp
|
||||
visibility: hidden;
|
||||
}
|
||||
.spin #spinner {
|
||||
stroke: var(--status-indicator-color);
|
||||
stroke: var(--app-text-color);
|
||||
}
|
||||
&.error #indicator {
|
||||
visibility: visible;
|
||||
fill: var(--status-indicator-error);
|
||||
fill: var(--app-text-disabled-color);
|
||||
}
|
||||
&.success #indicator {
|
||||
visibility: visible;
|
||||
fill: var(--status-indicator-success);
|
||||
fill: var(--app-success-color);
|
||||
}
|
||||
&.output #indicator {
|
||||
visibility: visible;
|
||||
fill: var(--status-indicator-color);
|
||||
fill: var(--app-text-color);
|
||||
}
|
||||
}
|
||||
|
@ -70,7 +70,7 @@
|
||||
margin-bottom: 5px;
|
||||
|
||||
i {
|
||||
color: var(--status-updated-color);
|
||||
color: var(--app-success-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -78,7 +78,7 @@
|
||||
.status.outdated {
|
||||
div {
|
||||
i {
|
||||
color: var(--status-outdated-color);
|
||||
color: var(--app-warning-color);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -14,7 +14,7 @@ import "./alert.less";
|
||||
class AlertModal extends React.Component<{}, {}> {
|
||||
@boundMethod
|
||||
closeModal(): void {
|
||||
GlobalModel.cancelAlert();
|
||||
GlobalModel.modalsModel.popModal(() => GlobalModel.cancelAlert());
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
|
@ -10,7 +10,7 @@ import cn from "classnames";
|
||||
import { GlobalModel, GlobalCommandRunner, Screen } from "@/models";
|
||||
import { Toggle, InlineSettingsTextEdit, SettingsError, Modal, Dropdown, Tooltip } from "@/elements";
|
||||
import * as util from "@/util/util";
|
||||
import { TabIcon } from "@/common/elements/tabicon";
|
||||
import { TabIcon, Button } from "@/elements";
|
||||
import { ReactComponent as GlobeIcon } from "@/assets/icons/globe.svg";
|
||||
import { ReactComponent as StatusCircleIcon } from "@/assets/icons/statuscircle.svg";
|
||||
import * as appconst from "@/app/appconst";
|
||||
@ -334,12 +334,9 @@ class ScreenSettingsModal extends React.Component<{}, {}> {
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="settings-input">
|
||||
<div
|
||||
onClick={this.handleDeleteScreen}
|
||||
className="button is-prompt-danger is-outlined is-small"
|
||||
>
|
||||
<Button onClick={this.handleDeleteScreen} className="secondary small danger">
|
||||
Delete Tab
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<SettingsError errorMessage={this.errorMessage} />
|
||||
|
@ -6,6 +6,7 @@ import * as mobxReact from "mobx-react";
|
||||
import * as mobx from "mobx";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
import { GlobalModel, GlobalCommandRunner, Session } from "@/models";
|
||||
import { Button } from "@/elements";
|
||||
import { Toggle, InlineSettingsTextEdit, SettingsError, Modal, Tooltip } from "@/elements";
|
||||
import * as util from "@/util/util";
|
||||
|
||||
@ -133,12 +134,9 @@ class SessionSettingsModal extends React.Component<{}, {}> {
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="settings-input">
|
||||
<div
|
||||
onClick={this.handleDeleteSession}
|
||||
className="button is-prompt-danger is-outlined is-small"
|
||||
>
|
||||
<Button onClick={this.handleDeleteSession} className="secondary small danger">
|
||||
Delete Workspace
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<SettingsError errorMessage={this.errorMessage} />
|
||||
|
@ -192,7 +192,7 @@ class ViewRemoteConnDetailModal extends React.Component<{}, {}> {
|
||||
</Button>
|
||||
);
|
||||
let archiveButton = (
|
||||
<Button className="secondary" onClick={() => this.clickArchive()}>
|
||||
<Button className="secondary danger" onClick={() => this.clickArchive()}>
|
||||
Delete
|
||||
</Button>
|
||||
);
|
||||
@ -207,7 +207,7 @@ class ViewRemoteConnDetailModal extends React.Component<{}, {}> {
|
||||
}
|
||||
if (remote.sshconfigsrc == "sshconfig-import") {
|
||||
archiveButton = (
|
||||
<Button className="secondary" onClick={() => this.clickArchive()}>
|
||||
<Button className="secondary danger" onClick={() => this.clickArchive()}>
|
||||
Delete
|
||||
<Tooltip
|
||||
message={
|
||||
|
@ -33,13 +33,13 @@
|
||||
th {
|
||||
height: 32px;
|
||||
padding: 5px 15px 5px 10px;
|
||||
color: var(--view-text-color);
|
||||
color: var(--app-text-color);
|
||||
}
|
||||
}
|
||||
|
||||
tr.connections-item {
|
||||
border-bottom: 1px solid var(--table-tr-border-bottom-color);
|
||||
color: var(--view-text-color);
|
||||
color: var(--app-text-color);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
|
@ -14,7 +14,7 @@ import localizedFormat from "dayjs/plugin/localizedFormat";
|
||||
import customParseFormat from "dayjs/plugin/customParseFormat";
|
||||
import { Line } from "@/app/line/linecomps";
|
||||
import { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "@/util/keyutil";
|
||||
import { TextField, Dropdown, Button } from "@/elements";
|
||||
import { TextField, Dropdown, Button, DatePicker } from "@/elements";
|
||||
|
||||
import { ReactComponent as AngleDownIcon } from "@/assets/icons/history/angle-down.svg";
|
||||
import { ReactComponent as ChevronLeftIcon } from "@/assets/icons/history/chevron-left.svg";
|
||||
@ -286,15 +286,16 @@ class HistoryView extends React.Component<{}, {}> {
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleFromTsChange(e: any): void {
|
||||
handleFromTsChange(date: Date): void {
|
||||
let hvm = GlobalModel.historyViewModel;
|
||||
let newDate = e.target.value;
|
||||
let newDate = dayjs(date).format("YYYY-MM-DD");
|
||||
let today = dayjs().format("YYYY-MM-DD");
|
||||
if (newDate == "" || newDate == today) {
|
||||
hvm.setFromDate(null);
|
||||
return;
|
||||
}
|
||||
hvm.setFromDate(e.target.value);
|
||||
console.log;
|
||||
hvm.setFromDate(newDate);
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
@ -472,13 +473,7 @@ class HistoryView extends React.Component<{}, {}> {
|
||||
/>
|
||||
<div className="fromts">
|
||||
<div className="fromts-text">From: </div>
|
||||
<div className="hoverEffect">
|
||||
<input
|
||||
type="date"
|
||||
onChange={this.handleFromTsChange}
|
||||
value={this.searchFromTsInputValue()}
|
||||
/>
|
||||
</div>
|
||||
<DatePicker selectedDate={new Date()} onSelectDate={this.handleFromTsChange} />
|
||||
</div>
|
||||
<div
|
||||
className="filter-cmds search-checkbox hoverEffect"
|
||||
|
@ -1,6 +1,4 @@
|
||||
.plugins-view {
|
||||
background-color: var(--session-bg-color);
|
||||
|
||||
.header {
|
||||
margin: 1.5em 1.5em 0.5em;
|
||||
|
||||
|
@ -138,7 +138,7 @@
|
||||
color: var(--app-warning-color);
|
||||
align-items: center;
|
||||
|
||||
.button {
|
||||
.wave-button {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ import cn from "classnames";
|
||||
import dayjs from "dayjs";
|
||||
import localizedFormat from "dayjs/plugin/localizedFormat";
|
||||
import { GlobalModel, GlobalCommandRunner, Screen } from "@/models";
|
||||
import { renderCmdText } from "@/elements";
|
||||
import { Button } from "@/elements";
|
||||
import { TextAreaInput } from "./textareainput";
|
||||
import { InfoMsg } from "./infomsg";
|
||||
import { HistoryInfo } from "./historyinfo";
|
||||
@ -188,12 +188,12 @@ class CmdInput extends React.Component<{}, {}> {
|
||||
<span className="remote-name">[{GlobalModel.resolveRemoteIdToFullRef(remote.remoteid)}]</span>
|
||||
is {remote.status}
|
||||
<If condition={remote.status != "connecting"}>
|
||||
<div
|
||||
className="button is-wave-green is-outlined is-small"
|
||||
<Button
|
||||
className="secondary small connect"
|
||||
onClick={() => this.clickConnectRemote(remote.remoteid)}
|
||||
>
|
||||
connect now
|
||||
</div>
|
||||
</Button>
|
||||
</If>
|
||||
</div>
|
||||
</If>
|
||||
|
@ -95,33 +95,6 @@
|
||||
height: 100%;
|
||||
overflow-x: hidden;
|
||||
|
||||
.rendermode-tag {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
background-color: rgba(78, 154, 6, 0.65);
|
||||
color: var(--app-black);
|
||||
padding: 2px 8px 2px 4px;
|
||||
border-bottom-left-radius: 5px;
|
||||
z-index: 10;
|
||||
|
||||
&.is-active {
|
||||
color: var(--app-text-color);
|
||||
}
|
||||
|
||||
.render-mode {
|
||||
padding-top: 2px;
|
||||
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
color: var(--app-text-color);
|
||||
|
||||
&:hover {
|
||||
color: var(--app-text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.window-empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -129,11 +102,11 @@
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
height: 100%;
|
||||
color: #ccc;
|
||||
color: var(--app-text-color);
|
||||
|
||||
code {
|
||||
background-color: transparent;
|
||||
color: #4e9a06;
|
||||
color: var(--term-green);
|
||||
}
|
||||
|
||||
&.should-fade {
|
||||
@ -142,44 +115,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.share-tag {
|
||||
color: var(--app-text-color);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 40%;
|
||||
background-color: darken(rgb(0, 177, 10), 20%);
|
||||
padding: 2px 8px 2px 4px;
|
||||
z-index: 11;
|
||||
|
||||
/* border-radius: 0 0 5px 5px; */
|
||||
opacity: 0.8;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.share-tag-link {
|
||||
margin-top: 10px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.share-tag-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
opacity: 1;
|
||||
padding: 20px;
|
||||
width: 250px;
|
||||
border: 1px solid #ccc;
|
||||
border-top: 0;
|
||||
|
||||
.share-tag-link {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filter-running {
|
||||
position: relative;
|
||||
display: flex;
|
||||
|
@ -694,20 +694,6 @@ class ScreenWindowView extends React.Component<{ session: Session; screen: Scree
|
||||
let renderMode = this.renderMode.get();
|
||||
return (
|
||||
<div className="window-view" ref={this.windowViewRef} style={{ width: this.props.width }}>
|
||||
<div
|
||||
key="rendermode-tag"
|
||||
className={cn("rendermode-tag", { "is-active": isActive })}
|
||||
style={{ display: "none" }}
|
||||
>
|
||||
<div className="render-mode" onClick={this.toggleRenderMode}>
|
||||
<If condition={renderMode == "normal"}>
|
||||
<i title="collapse" className="fa-sharp fa-solid fa-arrows-to-line" />
|
||||
</If>
|
||||
<If condition={renderMode == "collapsed"}>
|
||||
<i title="expand" className="fa-sharp fa-solid fa-arrows-from-line" />
|
||||
</If>
|
||||
</div>
|
||||
</div>
|
||||
<If condition={lines.length == 0}>
|
||||
<If condition={screen.nextLineNum.get() == 1}>
|
||||
<NewTabSettings screen={screen} />
|
||||
@ -718,40 +704,13 @@ class ScreenWindowView extends React.Component<{ session: Session; screen: Scree
|
||||
<div key="window-empty" className={cn("window-empty")}>
|
||||
<div>
|
||||
<code className="text-standard">
|
||||
[workspace="{session.name.get()}" screen="{screen.name.get()}"]
|
||||
[workspace="{session.name.get()}" tab="{screen.name.get()}"]
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</If>
|
||||
</If>
|
||||
<If condition={screen.isWebShared()}>
|
||||
<div key="share-tag" className="share-tag">
|
||||
<If condition={this.shareCopied.get()}>
|
||||
<div className="copied-indicator" />
|
||||
</If>
|
||||
<div className="share-tag-title">
|
||||
<i title="archived" className="fa-sharp fa-solid fa-share-nodes" /> web shared
|
||||
</div>
|
||||
<div className="share-tag-link">
|
||||
<div className="button is-wave-green is-outlined is-small" onClick={this.copyShareLink}>
|
||||
<span>copy link</span>
|
||||
<span className="icon">
|
||||
<i className="fa-sharp fa-solid fa-copy" />
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="button is-wave-green is-outlined is-small"
|
||||
onClick={this.openScreenSettings}
|
||||
>
|
||||
<span>open settings</span>
|
||||
<span className="icon">
|
||||
<i className="fa-sharp fa-solid fa-cog" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</If>
|
||||
<If condition={lines.length > 0}>
|
||||
<LinesView
|
||||
screen={screen}
|
||||
|
@ -72,9 +72,6 @@ class ScreenTab extends React.Component<
|
||||
ref={this.tabRef}
|
||||
value={screen}
|
||||
id={"screentab-" + screen.screenId}
|
||||
whileDrag={{
|
||||
backgroundColor: "rgba(13, 13, 13, 0.85)",
|
||||
}}
|
||||
data-screenid={screen.screenId}
|
||||
className={cn(
|
||||
"screen-tab",
|
||||
|
@ -99,6 +99,7 @@
|
||||
flex-direction: row;
|
||||
position: relative;
|
||||
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.
|
||||
|
@ -70,10 +70,6 @@ class RightSidebarModel implements SidebarModel {
|
||||
}
|
||||
|
||||
getCollapsed(): boolean {
|
||||
// disable right sidebar in production
|
||||
if (!this.globalModel.isDev) {
|
||||
return true;
|
||||
}
|
||||
const clientData = this.globalModel.clientData.get();
|
||||
const collapsed = clientData?.clientopts?.rightsidebar?.collapsed;
|
||||
if (this.isDragging.get()) {
|
||||
|
@ -62,7 +62,7 @@
|
||||
right: 12rem;
|
||||
border-radius: 5px;
|
||||
background-color: var(--form-element-secondary-color);
|
||||
color: var(--app-error-color);
|
||||
color: var(--app-text-disabled-color);
|
||||
z-index: 1;
|
||||
padding: 0 0.8em;
|
||||
font-size: 0.8em;
|
||||
|
4
src/types/custom.d.ts
vendored
4
src/types/custom.d.ts
vendored
@ -797,12 +797,16 @@ declare global {
|
||||
};
|
||||
|
||||
type FileInfoType = {
|
||||
type: string;
|
||||
name: string;
|
||||
size: number;
|
||||
modts: number;
|
||||
isdir: boolean;
|
||||
perm: number;
|
||||
notfound: boolean;
|
||||
modestr?: string;
|
||||
path?: string;
|
||||
outputpos?: number;
|
||||
};
|
||||
|
||||
type ExtBlob = Blob & {
|
||||
|
@ -11,9 +11,11 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"reflect"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/wavetermdev/waveterm/waveshell/pkg/base"
|
||||
"github.com/wavetermdev/waveterm/waveshell/pkg/wlog"
|
||||
@ -58,6 +60,7 @@ const (
|
||||
WriteFileReadyPacketStr = "writefileready" // rpc-response
|
||||
WriteFileDonePacketStr = "writefiledone" // rpc-response
|
||||
FileDataPacketStr = "filedata"
|
||||
FileStatPacketStr = "filestat"
|
||||
LogPacketStr = "log" // logging packet (sent from waveshell back to server)
|
||||
ShellStatePacketStr = "shellstate"
|
||||
|
||||
@ -112,6 +115,7 @@ func init() {
|
||||
TypeStrToFactory[WriteFileDonePacketStr] = reflect.TypeOf(WriteFileDonePacketType{})
|
||||
TypeStrToFactory[LogPacketStr] = reflect.TypeOf(LogPacketType{})
|
||||
TypeStrToFactory[ShellStatePacketStr] = reflect.TypeOf(ShellStatePacketType{})
|
||||
TypeStrToFactory[FileStatPacketStr] = reflect.TypeOf(FileStatPacketType{})
|
||||
|
||||
var _ RpcPacketType = (*RunPacketType)(nil)
|
||||
var _ RpcPacketType = (*GetCmdPacketType)(nil)
|
||||
@ -379,6 +383,51 @@ func MakeReInitPacket() *ReInitPacketType {
|
||||
return &ReInitPacketType{Type: ReInitPacketStr}
|
||||
}
|
||||
|
||||
type FileStatPacketType struct {
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
ModTs time.Time `json:"modts"`
|
||||
IsDir bool `json:"isdir"`
|
||||
Perm int `json:"perm"`
|
||||
ModeStr string `json:"modestr"`
|
||||
Error string `json:"error"`
|
||||
Done bool `json:"done"`
|
||||
RespId string `json:"respid"`
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
func (*FileStatPacketType) GetType() string {
|
||||
return FileStatPacketStr
|
||||
}
|
||||
|
||||
func (p *FileStatPacketType) GetResponseDone() bool {
|
||||
return p.Done
|
||||
}
|
||||
|
||||
func (p *FileStatPacketType) GetResponseId() string {
|
||||
return p.RespId
|
||||
}
|
||||
|
||||
func MakeFileStatPacketType() *FileStatPacketType {
|
||||
return &FileStatPacketType{Type: FileStatPacketStr}
|
||||
}
|
||||
|
||||
func MakeFileStatPacketFromFileInfo(finfo fs.FileInfo, err string, done bool) *FileStatPacketType {
|
||||
resp := MakeFileStatPacketType()
|
||||
resp.Error = err
|
||||
resp.Done = done
|
||||
|
||||
resp.IsDir = finfo.IsDir()
|
||||
resp.Name = finfo.Name()
|
||||
|
||||
resp.Size = finfo.Size()
|
||||
resp.ModTs = finfo.ModTime()
|
||||
resp.Perm = int(finfo.Mode().Perm())
|
||||
resp.ModeStr = finfo.Mode().String()
|
||||
return resp
|
||||
}
|
||||
|
||||
type StreamFilePacketType struct {
|
||||
Type string `json:"type"`
|
||||
ReqId string `json:"reqid"`
|
||||
|
@ -113,6 +113,9 @@ func (b bashShellApi) MakeRcFileStr(pk *packet.RunPacketType) string {
|
||||
if varDecl.IsExport() || varDecl.IsReadOnly() {
|
||||
continue
|
||||
}
|
||||
if varDecl.IsExtVar {
|
||||
continue
|
||||
}
|
||||
rcBuf.WriteString(BashDeclareStmt(varDecl))
|
||||
rcBuf.WriteString("\n")
|
||||
}
|
||||
|
@ -205,7 +205,7 @@ func bashParseDeclareOutput(state *packet.ShellState, declareBytes []byte, pvarB
|
||||
declMap[decl.Name] = decl
|
||||
}
|
||||
}
|
||||
pvarMap := parsePVarOutput(pvarBytes, false)
|
||||
pvarMap := parseExtVarOutput(pvarBytes, "", "")
|
||||
utilfn.CombineMaps(declMap, pvarMap)
|
||||
state.ShellVars = shellenv.SerializeDeclMap(declMap) // this writes out the decls in a canonical order
|
||||
if firstParseErr != nil {
|
||||
|
@ -25,10 +25,13 @@ import (
|
||||
"github.com/wavetermdev/waveterm/waveshell/pkg/base"
|
||||
"github.com/wavetermdev/waveterm/waveshell/pkg/packet"
|
||||
"github.com/wavetermdev/waveterm/waveshell/pkg/shellutil"
|
||||
"github.com/wavetermdev/waveterm/waveshell/pkg/utilfn"
|
||||
)
|
||||
|
||||
const GetStateTimeout = 5 * time.Second
|
||||
const GetGitBranchCmdStr = `printf "GITBRANCH %s\x00" "$(git rev-parse --abbrev-ref HEAD 2>/dev/null)"`
|
||||
const GetK8sContextCmdStr = `printf "K8SCONTEXT %s\x00" "$(kubectl config current-context 2>/dev/null)"`
|
||||
const GetK8sNamespaceCmdStr = `printf "K8SNAMESPACE %s\x00" "$(kubectl config view --minify --output 'jsonpath={..namespace}' 2>/dev/null)"`
|
||||
const RunCommandFmt = `%s`
|
||||
const DebugState = false
|
||||
|
||||
@ -239,7 +242,7 @@ func RunSimpleCmdInPty(ecmd *exec.Cmd) ([]byte, error) {
|
||||
return outputBuf.Bytes(), nil
|
||||
}
|
||||
|
||||
func parsePVarOutput(pvarBytes []byte, isZsh bool) map[string]*DeclareDeclType {
|
||||
func parseExtVarOutput(pvarBytes []byte, promptOutput string, zmodsOutput string) map[string]*DeclareDeclType {
|
||||
declMap := make(map[string]*DeclareDeclType)
|
||||
pvars := bytes.Split(pvarBytes, []byte{0})
|
||||
for _, pvarBA := range pvars {
|
||||
@ -251,11 +254,35 @@ func parsePVarOutput(pvarBytes []byte, isZsh bool) map[string]*DeclareDeclType {
|
||||
if pvarFields[0] == "" {
|
||||
continue
|
||||
}
|
||||
decl := &DeclareDeclType{IsZshDecl: isZsh, Args: "x"}
|
||||
if pvarFields[1] == "" {
|
||||
continue
|
||||
}
|
||||
decl := &DeclareDeclType{IsExtVar: true}
|
||||
decl.Name = "PROMPTVAR_" + pvarFields[0]
|
||||
decl.Value = shellescape.Quote(pvarFields[1])
|
||||
declMap[decl.Name] = decl
|
||||
}
|
||||
if promptOutput != "" {
|
||||
decl := &DeclareDeclType{IsExtVar: true}
|
||||
decl.Name = "PROMPTVAR_PS1"
|
||||
decl.Value = promptOutput
|
||||
declMap[decl.Name] = decl
|
||||
}
|
||||
if zmodsOutput != "" {
|
||||
var zmods []string
|
||||
lines := strings.Split(zmodsOutput, "\n")
|
||||
for _, line := range lines {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) != 2 || fields[0] != "zmodload" {
|
||||
continue
|
||||
}
|
||||
zmods = append(zmods, fields[1])
|
||||
}
|
||||
decl := &DeclareDeclType{IsExtVar: true}
|
||||
decl.Name = ZModsVarName
|
||||
decl.Value = utilfn.QuickJson(zmods)
|
||||
declMap[decl.Name] = decl
|
||||
}
|
||||
return declMap
|
||||
}
|
||||
|
||||
|
@ -30,6 +30,21 @@ const BaseZshOpts = ``
|
||||
const ZshShellVersionCmdStr = `echo zsh v$ZSH_VERSION`
|
||||
const StateOutputFdNum = 20
|
||||
|
||||
const (
|
||||
ZshSection_Version = iota
|
||||
ZshSection_Cwd
|
||||
ZshSection_Env
|
||||
ZshSection_Mods
|
||||
ZshSection_Vars
|
||||
ZshSection_Aliases
|
||||
ZshSection_Fpath
|
||||
ZshSection_Funcs
|
||||
ZshSection_PVars
|
||||
ZshSection_Prompt
|
||||
|
||||
ZshSection_NumFieldsExpected // must be last
|
||||
)
|
||||
|
||||
// TODO these need updating
|
||||
const RunZshSudoCommandFmt = `sudo -n -C %d zsh /dev/fd/%d`
|
||||
const RunZshSudoPasswordCommandFmt = `cat /dev/fd/%d | sudo -k -S -C %d zsh -c "echo '[from-mshell]'; exec %d>&-; zsh /dev/fd/%d < /dev/fd/%d"`
|
||||
@ -54,6 +69,7 @@ var ZshIgnoreVars = map[string]bool{
|
||||
"SHLVL": true,
|
||||
"TTY": true,
|
||||
"ZDOTDIR": true,
|
||||
"PPID": true,
|
||||
"epochtime": true,
|
||||
"langinfo": true,
|
||||
"keymaps": true,
|
||||
@ -77,6 +93,8 @@ var ZshIgnoreVars = map[string]bool{
|
||||
"funcsourcetrace": true,
|
||||
"funcstack": true,
|
||||
"functrace": true,
|
||||
"nameddirs": true,
|
||||
"userdirs": true,
|
||||
"parameters": true,
|
||||
"commands": true,
|
||||
"functions": true,
|
||||
@ -86,6 +104,25 @@ var ZshIgnoreVars = map[string]bool{
|
||||
"_comps": true,
|
||||
"_patcomps": true,
|
||||
"_postpatcomps": true,
|
||||
|
||||
// zsh/system
|
||||
"errnos": true,
|
||||
"sysparams": true,
|
||||
|
||||
// zsh/curses
|
||||
"ZCURSES_COLORS": true,
|
||||
"ZCURSES_COLOR_PAIRS": true,
|
||||
"zcurses_attrs": true,
|
||||
"zcurses_colors": true,
|
||||
"zcurses_keycodes": true,
|
||||
"zcurses_windows": true,
|
||||
|
||||
// not listed, but we also exclude all ZFTP_* variables
|
||||
}
|
||||
|
||||
var ZshIgnoreFuncs = map[string]bool{
|
||||
"zftp_chpwd": true,
|
||||
"zftp_progress": true,
|
||||
}
|
||||
|
||||
// only options we restore (other than ZshForceOptions)
|
||||
@ -131,11 +168,13 @@ var ZshUnsetVars = []string{
|
||||
"ZSH_EXECUTION_STRING",
|
||||
}
|
||||
|
||||
var ZshLoadMods = []string{
|
||||
"zsh/parameter",
|
||||
"zsh/langinfo",
|
||||
var ZshForceLoadMods = map[string]bool{
|
||||
"zsh/parameter": true,
|
||||
"zsh/langinfo": true,
|
||||
}
|
||||
|
||||
const ZModsVarName = "WAVESTATE_ZMODS"
|
||||
|
||||
// do not use these directly, call GetLocalMajorVersion()
|
||||
var localZshMajorVersionOnce = &sync.Once{}
|
||||
var localZshMajorVersion = ""
|
||||
@ -273,14 +312,29 @@ func (z zshShellApi) MakeRcFileStr(pk *packet.RunPacketType) string {
|
||||
rcBuf.WriteString(fmt.Sprintf("unsetopt %s\n", optName))
|
||||
}
|
||||
}
|
||||
for _, modName := range ZshLoadMods {
|
||||
for modName := range ZshForceLoadMods {
|
||||
rcBuf.WriteString(fmt.Sprintf("zmodload %s\n", modName))
|
||||
}
|
||||
modDecl := getDeclByName(varDecls, ZModsVarName)
|
||||
if modDecl != nil {
|
||||
modsArr := utilfn.QuickParseJson[[]string](modDecl.Value)
|
||||
for _, modName := range modsArr {
|
||||
if !ZshForceLoadMods[modName] {
|
||||
rcBuf.WriteString(fmt.Sprintf("zmodload %s\n", modName))
|
||||
}
|
||||
}
|
||||
}
|
||||
var postDecls []*shellenv.DeclareDeclType
|
||||
for _, varDecl := range varDecls {
|
||||
if ZshIgnoreVars[varDecl.Name] {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(varDecl.Name, "ZFTP_") {
|
||||
continue
|
||||
}
|
||||
if varDecl.IsExtVar {
|
||||
continue
|
||||
}
|
||||
if ZshUniqueArrayVars[varDecl.Name] && !varDecl.IsUniqueArray() {
|
||||
varDecl.AddFlag("U")
|
||||
}
|
||||
@ -332,6 +386,9 @@ func (z zshShellApi) MakeRcFileStr(pk *packet.RunPacketType) string {
|
||||
rcBuf.WriteString("# error decoding zsh functions\n")
|
||||
} else {
|
||||
for fnKey, fnValue := range fnMap {
|
||||
if ZshIgnoreFuncs[fnKey.ParamName] {
|
||||
continue
|
||||
}
|
||||
if fnValue == ZshFnAutoLoad {
|
||||
rcBuf.WriteString(fmt.Sprintf("autoload %s\n", shellescape.Quote(fnKey.ParamName)))
|
||||
} else {
|
||||
@ -411,6 +468,8 @@ pwd;
|
||||
printf "[%SECTIONSEP%]";
|
||||
env -0;
|
||||
printf "[%SECTIONSEP%]";
|
||||
zmodload -L
|
||||
printf "[%SECTIONSEP%]";
|
||||
typeset -p +H -m '*';
|
||||
printf "[%SECTIONSEP%]";
|
||||
for var in "${(@k)aliases}"; do
|
||||
@ -448,10 +507,16 @@ for var in "${(@k)dis_functions_source}"; do
|
||||
done
|
||||
printf "[%SECTIONSEP%]";
|
||||
[%GITBRANCH%]
|
||||
[%K8SCONTEXT%]
|
||||
[%K8SNAMESPACE%]
|
||||
printf "[%SECTIONSEP%]";
|
||||
print -P "$PS1"
|
||||
`
|
||||
cmd = strings.TrimSpace(cmd)
|
||||
cmd = strings.ReplaceAll(cmd, "[%ZSHVERSION%]", ZshShellVersionCmdStr)
|
||||
cmd = strings.ReplaceAll(cmd, "[%GITBRANCH%]", GetGitBranchCmdStr)
|
||||
cmd = strings.ReplaceAll(cmd, "[%K8SCONTEXT%]", GetK8sContextCmdStr)
|
||||
cmd = strings.ReplaceAll(cmd, "[%K8SNAMESPACE%]", GetK8sNamespaceCmdStr)
|
||||
cmd = strings.ReplaceAll(cmd, "[%PARTSEP%]", utilfn.ShellHexEscape(string(sectionSeparator[0:len(sectionSeparator)-1])))
|
||||
cmd = strings.ReplaceAll(cmd, "[%SECTIONSEP%]", utilfn.ShellHexEscape(string(sectionSeparator)))
|
||||
cmd = strings.ReplaceAll(cmd, "[%OUTPUTFD%]", fmt.Sprintf("/dev/fd/%d", fdNum))
|
||||
@ -599,6 +664,9 @@ func ParseZshFunctions(fpathArr []string, fnBytes []byte, partSeparator []byte)
|
||||
if fnName == "zshexit" {
|
||||
continue
|
||||
}
|
||||
if ZshIgnoreFuncs[fnName] {
|
||||
continue
|
||||
}
|
||||
if fnType == "functions" || fnType == "dis_functions" {
|
||||
fnBody[ZshParamKey{ParamType: fnType, ParamName: fnName}] = fnValue
|
||||
}
|
||||
@ -609,10 +677,13 @@ func ParseZshFunctions(fpathArr []string, fnBytes []byte, partSeparator []byte)
|
||||
// ok, so the trick here is that we want to only include functions that are *not* autoloaded
|
||||
// the ones that are pending autoloading or come from a source file in fpath, can just be set to autoload
|
||||
for fnKey := range fnBody {
|
||||
var inFpath bool
|
||||
source := fnSource[fnKey.ParamName]
|
||||
if isSourceFileInFpath(fpathArr, source) {
|
||||
fnBody[fnKey] = ZshFnAutoLoad
|
||||
} else if strings.TrimSpace(fnBody[fnKey]) == ZshAutoloadFnBody {
|
||||
if source != "" {
|
||||
inFpath = isSourceFileInFpath(fpathArr, source)
|
||||
}
|
||||
isAutoloadFnBody := strings.TrimSpace(fnBody[fnKey]) == ZshAutoloadFnBody
|
||||
if inFpath || isAutoloadFnBody {
|
||||
fnBody[fnKey] = ZshFnAutoLoad
|
||||
}
|
||||
}
|
||||
@ -639,11 +710,11 @@ func (z zshShellApi) ParseShellStateOutput(outputBytes []byte) (*packet.ShellSta
|
||||
versionStr := string(outputBytes[0:firstZeroIdx])
|
||||
sectionSeparator := outputBytes[firstZeroIdx+1 : firstDZeroIdx+2]
|
||||
partSeparator := sectionSeparator[0 : len(sectionSeparator)-1]
|
||||
// 8 fields: version [0], cwd [1], env [2], vars [3], aliases [4], fpath [5], functions [6], pvars [7]
|
||||
fields := bytes.Split(outputBytes, sectionSeparator)
|
||||
if len(fields) != 8 {
|
||||
// sections: see ZshSection_* consts
|
||||
sections := bytes.Split(outputBytes, sectionSeparator)
|
||||
if len(sections) != ZshSection_NumFieldsExpected {
|
||||
base.Logf("invalid -- numfields\n")
|
||||
return nil, fmt.Errorf("invalid zsh shell state output, wrong number of fields, fields=%d", len(fields))
|
||||
return nil, fmt.Errorf("invalid zsh shell state output, wrong number of sections, section=%d", len(sections))
|
||||
}
|
||||
rtn := &packet.ShellState{}
|
||||
rtn.Version = strings.TrimSpace(versionStr)
|
||||
@ -653,10 +724,10 @@ func (z zshShellApi) ParseShellStateOutput(outputBytes []byte) (*packet.ShellSta
|
||||
if _, _, err := packet.ParseShellStateVersion(rtn.Version); err != nil {
|
||||
return nil, fmt.Errorf("invalid zsh shell state output, invalid version: %v", err)
|
||||
}
|
||||
cwdStr := stripNewLineChars(string(fields[1]))
|
||||
cwdStr := stripNewLineChars(string(sections[ZshSection_Cwd]))
|
||||
rtn.Cwd = cwdStr
|
||||
zshEnv := parseZshEnv(fields[2])
|
||||
zshDecls, err := parseZshDecls(fields[3])
|
||||
zshEnv := parseZshEnv(sections[ZshSection_Env])
|
||||
zshDecls, err := parseZshDecls(sections[ZshSection_Vars])
|
||||
if err != nil {
|
||||
base.Logf("invalid - parsedecls %v\n", err)
|
||||
return nil, err
|
||||
@ -666,16 +737,15 @@ func (z zshShellApi) ParseShellStateOutput(outputBytes []byte) (*packet.ShellSta
|
||||
decl.ZshEnvValue = zshEnv[decl.ZshBoundScalar]
|
||||
}
|
||||
}
|
||||
aliasMap := parseZshAliasStateOutput(fields[4], partSeparator)
|
||||
aliasMap := parseZshAliasStateOutput(sections[ZshSection_Aliases], partSeparator)
|
||||
rtn.Aliases = string(EncodeZshMap(aliasMap))
|
||||
fpathStr := stripNewLineChars(string(string(fields[5])))
|
||||
fpathStr := stripNewLineChars(string(string(sections[ZshSection_Fpath])))
|
||||
fpathArr := strings.Split(fpathStr, ":")
|
||||
zshFuncs := ParseZshFunctions(fpathArr, fields[6], partSeparator)
|
||||
zshFuncs := ParseZshFunctions(fpathArr, sections[ZshSection_Funcs], partSeparator)
|
||||
rtn.Funcs = string(EncodeZshMap(zshFuncs))
|
||||
pvarMap := parsePVarOutput(fields[7], true)
|
||||
pvarMap := parseExtVarOutput(sections[ZshSection_PVars], string(sections[ZshSection_Prompt]), string(sections[ZshSection_Mods]))
|
||||
utilfn.CombineMaps(zshDecls, pvarMap)
|
||||
rtn.ShellVars = shellenv.SerializeDeclMap(zshDecls)
|
||||
base.Logf("parse shellstate done\n")
|
||||
return rtn, nil
|
||||
}
|
||||
|
||||
|
@ -22,6 +22,7 @@ const (
|
||||
|
||||
type DeclareDeclType struct {
|
||||
IsZshDecl bool
|
||||
IsExtVar bool // set for "special" wave internal variables
|
||||
|
||||
Args string
|
||||
Name string
|
||||
@ -36,31 +37,31 @@ type DeclareDeclType struct {
|
||||
}
|
||||
|
||||
func (d *DeclareDeclType) IsExport() bool {
|
||||
return strings.Index(d.Args, "x") >= 0
|
||||
return strings.Contains(d.Args, "x")
|
||||
}
|
||||
|
||||
func (d *DeclareDeclType) IsReadOnly() bool {
|
||||
return strings.Index(d.Args, "r") >= 0
|
||||
return strings.Contains(d.Args, "r")
|
||||
}
|
||||
|
||||
func (d *DeclareDeclType) IsZshScalarBound() bool {
|
||||
return strings.Index(d.Args, "T") >= 0
|
||||
return strings.Contains(d.Args, "T")
|
||||
}
|
||||
|
||||
func (d *DeclareDeclType) IsArray() bool {
|
||||
return strings.Index(d.Args, "a") >= 0
|
||||
return strings.Contains(d.Args, "a")
|
||||
}
|
||||
|
||||
func (d *DeclareDeclType) IsAssocArray() bool {
|
||||
return strings.Index(d.Args, "A") >= 0
|
||||
return strings.Contains(d.Args, "A")
|
||||
}
|
||||
|
||||
func (d *DeclareDeclType) IsUniqueArray() bool {
|
||||
return d.IsArray() && strings.Index(d.Args, "U") >= 0
|
||||
return d.IsArray() && strings.Contains(d.Args, "U")
|
||||
}
|
||||
|
||||
func (d *DeclareDeclType) AddFlag(flag string) {
|
||||
if strings.Index(d.Args, flag) >= 0 {
|
||||
if strings.Contains(d.Args, flag) {
|
||||
return
|
||||
}
|
||||
d.Args += flag
|
||||
@ -101,13 +102,13 @@ func (d *DeclareDeclType) SortZshFlags() {
|
||||
}
|
||||
|
||||
func (d *DeclareDeclType) DataType() string {
|
||||
if strings.Index(d.Args, "a") >= 0 {
|
||||
if strings.Contains(d.Args, "a") {
|
||||
return DeclTypeArray
|
||||
}
|
||||
if strings.Index(d.Args, "A") >= 0 {
|
||||
if strings.Contains(d.Args, "A") {
|
||||
return DeclTypeAssocArray
|
||||
}
|
||||
if strings.Index(d.Args, "i") >= 0 {
|
||||
if strings.Contains(d.Args, "i") {
|
||||
return DeclTypeInt
|
||||
}
|
||||
return DeclTypeNormal
|
||||
@ -124,7 +125,15 @@ func FindVarDecl(decls []*DeclareDeclType, name string) *DeclareDeclType {
|
||||
|
||||
// NOTE Serialize no longer writes the final null byte
|
||||
func (d *DeclareDeclType) Serialize() []byte {
|
||||
if d.IsZshDecl {
|
||||
if d.IsExtVar {
|
||||
parts := []string{
|
||||
"e1",
|
||||
d.Args,
|
||||
d.Name,
|
||||
d.Value,
|
||||
}
|
||||
return utilfn.EncodeStringArray(parts)
|
||||
} else if d.IsZshDecl {
|
||||
d.SortZshFlags()
|
||||
parts := []string{
|
||||
"z1",
|
||||
@ -149,6 +158,15 @@ func (d *DeclareDeclType) Serialize() []byte {
|
||||
// return []byte(rtn)
|
||||
}
|
||||
|
||||
func (d *DeclareDeclType) UnescapedValue() string {
|
||||
if d.IsExtVar {
|
||||
return d.Value
|
||||
}
|
||||
ectx := simpleexpand.SimpleExpandContext{}
|
||||
rtn, _ := simpleexpand.SimpleExpandPartialWord(ectx, d.Value, false)
|
||||
return rtn
|
||||
}
|
||||
|
||||
func DeclsEqual(compareName bool, d1 *DeclareDeclType, d2 *DeclareDeclType) bool {
|
||||
if d1.IsExport() != d2.IsExport() {
|
||||
return false
|
||||
@ -164,7 +182,8 @@ func DeclsEqual(compareName bool, d1 *DeclareDeclType, d2 *DeclareDeclType) bool
|
||||
|
||||
// envline should be valid
|
||||
func parseDeclLine(envLineBytes []byte) *DeclareDeclType {
|
||||
if utilfn.EncodedStringArrayHasFirstKey(envLineBytes, "z1") {
|
||||
esFirstVal := utilfn.EncodedStringArrayGetFirstVal(envLineBytes)
|
||||
if esFirstVal == "z1" {
|
||||
parts, err := utilfn.DecodeStringArray(envLineBytes)
|
||||
if err != nil {
|
||||
return nil
|
||||
@ -180,7 +199,7 @@ func parseDeclLine(envLineBytes []byte) *DeclareDeclType {
|
||||
ZshBoundScalar: parts[4],
|
||||
ZshEnvValue: parts[5],
|
||||
}
|
||||
} else if utilfn.EncodedStringArrayHasFirstKey(envLineBytes, "b1") {
|
||||
} else if esFirstVal == "b1" {
|
||||
parts, err := utilfn.DecodeStringArray(envLineBytes)
|
||||
if err != nil {
|
||||
return nil
|
||||
@ -193,8 +212,25 @@ func parseDeclLine(envLineBytes []byte) *DeclareDeclType {
|
||||
Name: parts[2],
|
||||
Value: parts[3],
|
||||
}
|
||||
} else if esFirstVal == "e1" {
|
||||
parts, err := utilfn.DecodeStringArray(envLineBytes)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if len(parts) != 4 {
|
||||
return nil
|
||||
}
|
||||
return &DeclareDeclType{
|
||||
IsExtVar: true,
|
||||
Args: parts[1],
|
||||
Name: parts[2],
|
||||
Value: parts[3],
|
||||
}
|
||||
} else if esFirstVal == "p1" {
|
||||
// deprecated
|
||||
return nil
|
||||
}
|
||||
// legacy decoding (v0)
|
||||
// legacy decoding (v0) (not an encoded string array)
|
||||
envLine := string(envLineBytes)
|
||||
eqIdx := strings.Index(envLine, "=")
|
||||
if eqIdx == -1 {
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
"bytes"
|
||||
"crypto/sha1"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
@ -400,7 +401,7 @@ func DecodeStringArray(barr []byte) ([]string, error) {
|
||||
return rtn, nil
|
||||
}
|
||||
|
||||
func EncodedStringArrayHasFirstKey(encoded []byte, firstKey string) bool {
|
||||
func EncodedStringArrayHasFirstVal(encoded []byte, firstKey string) bool {
|
||||
firstKeyBytes := NullEncodeStr(firstKey)
|
||||
if !bytes.HasPrefix(encoded, firstKeyBytes) {
|
||||
return false
|
||||
@ -411,6 +412,18 @@ func EncodedStringArrayHasFirstKey(encoded []byte, firstKey string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// on encoding error returns ""
|
||||
// this is used to perform logic on first value without decoding the entire array
|
||||
func EncodedStringArrayGetFirstVal(encoded []byte) string {
|
||||
sepIdx := bytes.IndexByte(encoded, nullEncodeSepByte)
|
||||
if sepIdx == -1 {
|
||||
str, _ := NullDecodeStr(encoded)
|
||||
return str
|
||||
}
|
||||
str, _ := NullDecodeStr(encoded[0:sepIdx])
|
||||
return str
|
||||
}
|
||||
|
||||
// encodes a string, removing null/zero bytes (and separators '|')
|
||||
// a zero byte is encoded as "\0", a '\' is encoded as "\\", sep is encoded as "\s"
|
||||
// allows for easy double splitting (first on \x00, and next on "|")
|
||||
@ -520,3 +533,22 @@ func CombineStrArrays(sarr1 []string, sarr2 []string) []string {
|
||||
}
|
||||
return rtn
|
||||
}
|
||||
|
||||
func QuickJson(v interface{}) string {
|
||||
barr, _ := json.Marshal(v)
|
||||
return string(barr)
|
||||
}
|
||||
|
||||
func QuickParseJson[T any](s string) T {
|
||||
var v T
|
||||
_ = json.Unmarshal([]byte(s), &v)
|
||||
return v
|
||||
}
|
||||
|
||||
func StrArrayToMap(sarr []string) map[string]bool {
|
||||
m := make(map[string]bool)
|
||||
for _, s := range sarr {
|
||||
m[s] = true
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
@ -677,6 +677,40 @@ func HandleRunCommand(w http.ResponseWriter, r *http.Request) {
|
||||
WriteJsonSuccess(w, update)
|
||||
}
|
||||
|
||||
func CheckIsDir(dirHandler http.Handler, fileHandler http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
configPath := r.URL.Path
|
||||
configAbsPath, err := filepath.Abs(configPath)
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
w.Write([]byte(fmt.Sprintf("error getting absolute path", err)))
|
||||
return
|
||||
}
|
||||
configBaseDir := path.Join(scbase.GetWaveHomeDir(), "config")
|
||||
configFullPath := path.Join(scbase.GetWaveHomeDir(), configAbsPath)
|
||||
if !strings.HasPrefix(configFullPath, configBaseDir) {
|
||||
w.WriteHeader(500)
|
||||
w.Write([]byte(fmt.Sprintf("error: path is not in config folder")))
|
||||
return
|
||||
}
|
||||
fstat, err := os.Stat(configFullPath)
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
w.WriteHeader(404)
|
||||
w.Write([]byte(fmt.Sprintf("file not found: ", configAbsPath)))
|
||||
return
|
||||
} else if err != nil {
|
||||
w.WriteHeader(500)
|
||||
w.Write([]byte(fmt.Sprintf("file stat err", err)))
|
||||
return
|
||||
}
|
||||
if fstat.IsDir() {
|
||||
AuthKeyMiddleWare(dirHandler).ServeHTTP(w, r)
|
||||
} else {
|
||||
AuthKeyMiddleWare(fileHandler).ServeHTTP(w, r)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func AuthKeyMiddleWare(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
reqAuthKey := r.Header.Get("X-AuthKey")
|
||||
@ -857,6 +891,39 @@ func doShutdown(reason string) {
|
||||
})
|
||||
}
|
||||
|
||||
func configDirHandler(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("running?")
|
||||
configPath := r.URL.Path
|
||||
configFullPath := path.Join(scbase.GetWaveHomeDir(), configPath)
|
||||
dirFile, err := os.Open(configFullPath)
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
w.Write([]byte(fmt.Sprintf("error opening specified dir: ", err)))
|
||||
return
|
||||
}
|
||||
entries, err := dirFile.Readdir(0)
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
w.Write([]byte(fmt.Sprintf("error getting files: ", err)))
|
||||
return
|
||||
}
|
||||
var files []*packet.FileStatPacketType
|
||||
for index := 0; index < len(entries); index++ {
|
||||
curEntry := entries[index]
|
||||
curFile := packet.MakeFileStatPacketFromFileInfo(curEntry, "", false)
|
||||
files = append(files, curFile)
|
||||
}
|
||||
dirListJson, err := json.Marshal(files)
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
w.Write([]byte(fmt.Sprintf("json err: ", err)))
|
||||
return
|
||||
}
|
||||
w.WriteHeader(200)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(dirListJson)
|
||||
}
|
||||
|
||||
func main() {
|
||||
scbase.BuildTime = BuildTime
|
||||
scbase.WaveVersion = WaveVersion
|
||||
@ -953,7 +1020,9 @@ func main() {
|
||||
gr.HandleFunc("/api/write-file", AuthKeyWrap(HandleWriteFile)).Methods("POST")
|
||||
configPath := path.Join(scbase.GetWaveHomeDir(), "config") + "/"
|
||||
log.Printf("[wave] config path: %q\n", configPath)
|
||||
gr.PathPrefix("/config/").Handler(AuthKeyMiddleWare(http.StripPrefix("/config/", http.FileServer(http.Dir(configPath)))))
|
||||
isFileHandler := http.StripPrefix("/config/", http.FileServer(http.Dir(configPath)))
|
||||
isDirHandler := http.HandlerFunc(configDirHandler)
|
||||
gr.PathPrefix("/config/").Handler(CheckIsDir(isDirHandler, isFileHandler))
|
||||
|
||||
serverAddr := MainServerAddr
|
||||
if scbase.IsDevMode() {
|
||||
|
@ -3825,7 +3825,8 @@ func HistoryViewAllCommand(ctx context.Context, pk *scpacket.FeCommandPacketType
|
||||
if pk.Kwargs["fromts"] != "" {
|
||||
fromTs, err := resolvePosInt(pk.Kwargs["fromts"], 0)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid fromts (must be unixtime (milliseconds): %v", err)
|
||||
// no error here anymore (otherwise it jams up the frontend, just ignore and set to 0)
|
||||
opts.FromTs = 0
|
||||
}
|
||||
if fromTs > 0 {
|
||||
opts.FromTs = int64(fromTs)
|
||||
|
@ -214,6 +214,7 @@ var literalRtnStateCommands = []string{
|
||||
"enable",
|
||||
"disable",
|
||||
"function",
|
||||
"zmodload",
|
||||
}
|
||||
|
||||
func getCallExprLitArg(callExpr *syntax.CallExpr, argNum int) string {
|
||||
|
@ -1784,16 +1784,22 @@ func makeTermOpts(runPk *packet.RunPacketType) sstore.TermOpts {
|
||||
return sstore.TermOpts{Rows: int64(runPk.TermOpts.Rows), Cols: int64(runPk.TermOpts.Cols), FlexRows: runPk.TermOpts.FlexRows, MaxPtySize: DefaultMaxPtySize}
|
||||
}
|
||||
|
||||
// returns (ok, currentPSC)
|
||||
// if ok is true, currentPSC will be nil
|
||||
// if ok is false, currentPSC will be the existing pending state command (not nil)
|
||||
func (msh *MShellProc) testAndSetPendingStateCmd(screenId string, rptr sstore.RemotePtrType, newCK *base.CommandKey) (bool, *base.CommandKey) {
|
||||
// returns (ok, rct)
|
||||
// if ok is true, rct will be nil
|
||||
// if ok is false, rct will be the existing pending state command (not nil)
|
||||
func (msh *MShellProc) testAndSetPendingStateCmd(screenId string, rptr sstore.RemotePtrType, newCK *base.CommandKey) (bool, *RunCmdType) {
|
||||
key := pendingStateKey{ScreenId: screenId, RemotePtr: rptr}
|
||||
msh.Lock.Lock()
|
||||
defer msh.Lock.Unlock()
|
||||
ck, found := msh.PendingStateCmds[key]
|
||||
if found {
|
||||
return false, &ck
|
||||
// we don't call GetRunningCmd here because we already hold msh.Lock
|
||||
rct := msh.RunningCmds[ck]
|
||||
if rct != nil {
|
||||
return false, rct
|
||||
}
|
||||
// ok, so rct is nil (that's strange). allow command to proceed, but log
|
||||
log.Printf("[warning] found pending state cmd with no running cmd: %s\n", ck)
|
||||
}
|
||||
if newCK != nil {
|
||||
msh.PendingStateCmds[key] = *newCK
|
||||
@ -1883,15 +1889,14 @@ func RunCommand(ctx context.Context, rcOpts RunCommandOpts, runPacket *packet.Ru
|
||||
if runPacket.ReturnState {
|
||||
newPSC = &runPacket.CK
|
||||
}
|
||||
ok, existingPSC := msh.testAndSetPendingStateCmd(screenId, remotePtr, newPSC)
|
||||
ok, existingRct := msh.testAndSetPendingStateCmd(screenId, remotePtr, newPSC)
|
||||
if !ok {
|
||||
rct := msh.GetRunningCmd(*existingPSC)
|
||||
if rct.Ephemeral {
|
||||
if existingRct.Ephemeral {
|
||||
// if the existing command is ephemeral, we cancel it and continue
|
||||
rct.EphCancled.Store(true)
|
||||
existingRct.EphCancled.Store(true)
|
||||
} else {
|
||||
line, _, err := sstore.GetLineCmdByLineId(ctx, screenId, existingPSC.GetCmdId())
|
||||
return nil, nil, makePSCLineError(*existingPSC, line, err)
|
||||
line, _, err := sstore.GetLineCmdByLineId(ctx, screenId, existingRct.CK.GetCmdId())
|
||||
return nil, nil, makePSCLineError(existingRct.CK, line, err)
|
||||
}
|
||||
}
|
||||
if newPSC != nil {
|
||||
|
@ -751,16 +751,17 @@ func FeStateFromShellState(state *packet.ShellState) map[string]string {
|
||||
}
|
||||
rtn := make(map[string]string)
|
||||
rtn["cwd"] = state.Cwd
|
||||
envMap := shellenv.EnvMapFromState(state)
|
||||
if envMap["VIRTUAL_ENV"] != "" {
|
||||
rtn["VIRTUAL_ENV"] = envMap["VIRTUAL_ENV"]
|
||||
declMap := shellenv.DeclMapFromState(state)
|
||||
if decl, ok := declMap["VIRTUAL_ENV"]; ok {
|
||||
rtn["VIRTUAL_ENV"] = decl.UnescapedValue()
|
||||
}
|
||||
if envMap["CONDA_DEFAULT_ENV"] != "" {
|
||||
rtn["CONDA_DEFAULT_ENV"] = envMap["CONDA_DEFAULT_ENV"]
|
||||
if decl, ok := declMap["CONDA_DEFAULT_ENV"]; ok {
|
||||
rtn["CONDA_DEFAULT_ENV"] = decl.UnescapedValue()
|
||||
}
|
||||
for key, val := range envMap {
|
||||
if strings.HasPrefix(key, "PROMPTVAR_") && envMap[key] != "" {
|
||||
rtn[key] = val
|
||||
for _, decl := range declMap {
|
||||
// works for both legacy and new IsExtVar decls
|
||||
if strings.HasPrefix(decl.Name, "PROMPTVAR_") {
|
||||
rtn[decl.Name] = decl.UnescapedValue()
|
||||
}
|
||||
}
|
||||
_, _, err := packet.ParseShellStateVersion(state.Version)
|
||||
|
Loading…
Reference in New Issue
Block a user