Compare commits

..

3 Commits

Author SHA1 Message Date
Evan Simkowitz 35cf4b6a3b Merge branch 'main' into evan/inshellisense 2024-04-22 13:54:42 -07:00
Sylvie Crowe 8fdcf10cae fix: ignore computer sleep for telemetry clock (#577)
* fix: ignore computer sleep for telemetry clock

This uses unix time with regular integer comparisons to prevent the
telemetry clock from needing to wait for 8 hours of the program running.
Now, it only needs to wait 8 real-time hours instead.

* drop telemetry time to 4 hours, tick every 10 minutes
2024-04-19 18:26:31 -07:00
Mike Sawka 6336f87cf2 tab context menu (w/ close tab option) (#583)
* create tab context menu (for close tab)

* small update to ctx menu, also make all tab deletes (including keybinding) not popup confirm modal if less than 10 blocks
2024-04-19 18:25:26 -07:00
14 changed files with 114 additions and 1285 deletions
@@ -7,7 +7,6 @@ import { observer } from "mobx-react";
import { If } from "tsx-control-statements/components";
import "./suggestionview.less";
import { getAll, getFirst } from "@/autocomplete/runtime/utils";
export const SuggestionView: React.FC = observer(() => {
const [selectedSuggestion, setSelectedSuggestion] = React.useState<number>(0);
@@ -61,15 +60,14 @@ export const SuggestionView: React.FC = observer(() => {
</If>
{suggestions?.suggestions.map((suggestion, idx) => (
<div
key={getFirst(suggestion.name)}
key={suggestion.name}
title={suggestion.description}
className={cn("suggestion-item", { "is-selected": selectedSuggestion === idx })}
onClick={() => {
setSuggestion(idx);
}}
>
{suggestion.icon} {getAll(suggestion.name).join(",")}{" "}
{suggestion.description ? `- ${suggestion.description}` : ""}
{suggestion.icon} {suggestion.name} {suggestion.description ? `- ${suggestion.description}` : ""}
</div>
))}
</AuxiliaryCmdView>
@@ -102,6 +102,19 @@ class HistoryKeybindings extends React.Component<{}, {}> {
}
}
const SuggestionsInfoMsg = (props: { suggestions: SuggestionBlob }) => {
const suggestions = props.suggestions.suggestions;
return (
<div className="cmd-input-info">
{suggestions.map((s) => (
<div key={s.name} className="cmd-input-info-item">
{s.icon} {s.name}
</div>
))}
</div>
);
};
class CmdInputKeybindings extends React.Component<{ inputObject: TextAreaInput }, {}> {
curPress: string;
lastTab: boolean;
+55 -1
View File
@@ -12,6 +12,7 @@ import * as constants from "@/app/appconst";
import { Reorder } from "framer-motion";
import { MagicLayout } from "@/app/magiclayout";
import { TabIcon } from "@/elements/tabicon";
import * as appconst from "@/app/appconst";
@mobxReact.observer
class ScreenTab extends React.Component<
@@ -62,6 +63,59 @@ class ScreenTab extends React.Component<
})();
}
@boundMethod
onContextMenu(e: React.MouseEvent) {
e.preventDefault();
e.stopPropagation();
let { screen, activeScreenId } = this.props;
if (activeScreenId != screen.screenId) {
// only show context menu for active tab
GlobalCommandRunner.switchScreen(screen.screenId);
return;
}
let colorSubMenu: ContextMenuItem[] = [];
for (let color of appconst.TabColors) {
colorSubMenu.push({
label: color,
click: () => {
GlobalCommandRunner.screenSetSettings(screen.screenId, { tabcolor: color }, false);
},
});
}
let menu: ContextMenuItem[] = [
{
label: "New Tab",
click: () => {
GlobalCommandRunner.createNewScreen();
},
},
{
type: "separator",
},
{
label: "Set Tab Color",
submenu: colorSubMenu,
},
{
label: "All Tab Settings",
click: () => {
GlobalModel.tabSettingsOpen.set(true);
},
},
{
type: "separator",
},
{
label: "Close Tab",
click: () => {
GlobalModel.onCloseCurrentTab();
},
},
];
GlobalModel.contextMenuModel.showContextMenu(menu, { x: e.clientX, y: e.clientY });
return;
}
render() {
let { screen, activeScreenId, index, onSwitchScreen } = this.props;
let archived = screen.archived.get() ? (
@@ -83,7 +137,7 @@ class ScreenTab extends React.Component<
"color-" + screen.getTabColor()
)}
onPointerDown={() => onSwitchScreen(screen.screenId)}
onContextMenu={(event) => this.openScreenSettings(event, screen)}
onContextMenu={this.onContextMenu}
onDragEnd={this.handleDragEnd}
>
<div className="background"></div>
+2 -1
View File
@@ -121,7 +121,8 @@ class TabSettings extends React.Component<{ screen: Screen }, {}> {
if (screen == null) {
return;
}
if (screen.getScreenLines().lines.length == 0) {
let numLines = screen.getScreenLines().lines.length;
if (numLines < 10) {
GlobalCommandRunner.screenDelete(screen.screenId, false);
GlobalModel.modalsModel.popModal();
return;
+1 -1
View File
@@ -15,7 +15,7 @@ export type Suggestion = {
};
export type SuggestionBlob = {
suggestions: Fig.Suggestion[];
suggestions: Suggestion[];
argumentDescription?: string;
charactersToDrop?: number;
};
File diff suppressed because it is too large Load Diff
+2 -11
View File
@@ -11,7 +11,6 @@ import speclist, {
// @ts-ignore
} from "@withfig/autocomplete/build/index";
import { parseCommand, CommandToken } from "./parser";
import { Newton } from "./newton";
import { getArgDrivenRecommendation, getSubcommandDrivenRecommendation } from "./suggestion";
import { SuggestionBlob } from "./model";
import { buildExecuteShellCommand, resolveCwd } from "./utils";
@@ -96,13 +95,6 @@ const lazyLoadSpecLocation = async (location: Fig.SpecLocation): Promise<Fig.Spe
export const getSuggestions = async (cmd: string, cwd: string, shell: Shell): Promise<SuggestionBlob | undefined> => {
const activeCmd = parseCommand(cmd);
const parserCmd = activeCmd.map((c) => c.token);
if (cmd.endsWith(" ")) {
parserCmd.push(" ");
}
const parser = new Newton(undefined, parserCmd, cwd);
const sugg = await parser.generateSuggestions();
log.debug("newton done");
log.debug("activeCmd", activeCmd);
if (activeCmd.length === 0) {
return;
@@ -141,8 +133,7 @@ export const getSuggestions = async (cmd: string, cwd: string, shell: Shell): Pr
}
if (result == null) return;
log.debug("result og", result);
log.debug("result newton", sugg);
log.debug("result", result);
return { ...result, charactersToDrop };
};
@@ -165,7 +156,7 @@ const getSubcommand = (spec?: Fig.Spec): Fig.Subcommand | undefined => {
if (spec == null) return;
if (typeof spec === "function") {
const potentialSubcommand = spec();
if (Object.hasOwn(potentialSubcommand, "name")) {
if (Object.prototype.hasOwnProperty.call(potentialSubcommand, "name")) {
return potentialSubcommand as Fig.Subcommand;
}
return;
+23 -35
View File
@@ -11,7 +11,6 @@ import { runTemplates } from "./template";
import { Suggestion, SuggestionBlob } from "./model";
import { getApi } from "@/models";
import log from "../utils/log";
import { getAll, matchAny } from "./utils";
enum SuggestionIcons {
File = "đź“„",
@@ -25,7 +24,7 @@ enum SuggestionIcons {
Default = "đź“€",
}
export const getIcon = (icon: string | undefined, suggestionType: Fig.SuggestionType | undefined): string => {
const getIcon = (icon: string | undefined, suggestionType: Fig.SuggestionType | undefined): string => {
// TODO: enable fig icons once spacing is better
// if (icon && /[^\u0000-\u00ff]/.test(icon)) {
// return icon;
@@ -74,7 +73,7 @@ function filter<
filterStrategy: FilterStrategy | undefined,
partialCmd: string | undefined,
suggestionType: Fig.SuggestionType | undefined
): Fig.Suggestion[] {
): Suggestion[] {
log.debug("filter", suggestions, filterStrategy, partialCmd, suggestionType);
if (!partialCmd)
return suggestions
@@ -144,16 +143,18 @@ function filter<
}
}
export type FilterStrategy = "fuzzy" | "prefix" | "default";
type FilterStrategy = "fuzzy" | "prefix" | "default";
export const generatorSuggestionsTokens = async (
const generatorSuggestions = async (
generator: Fig.SingleOrArray<Fig.Generator> | undefined,
tokens: string[],
acceptedTokens: CommandToken[],
filterStrategy: FilterStrategy | undefined,
partialCmd: string | undefined,
cwd: string
) => {
): Promise<Suggestion[]> => {
const generators = generator instanceof Array ? generator : generator ? [generator] : [];
const tokens = acceptedTokens.map((t) => t.token);
if (partialCmd) tokens.push(partialCmd);
const suggestions = (await Promise.all(generators.map((gen) => runGenerator(gen, tokens, cwd)))).flat();
return filter<Fig.Suggestion>(
suggestions.map((suggestion) => ({ ...suggestion, priority: suggestion.priority ?? 60 })),
@@ -163,43 +164,30 @@ export const generatorSuggestionsTokens = async (
);
};
export const generatorSuggestions = async (
generator: Fig.SingleOrArray<Fig.Generator> | undefined,
acceptedTokens: CommandToken[],
filterStrategy: FilterStrategy | undefined,
partialCmd: string | undefined,
cwd: string
): Promise<Fig.Suggestion[]> => {
const generators = generator instanceof Array ? generator : generator ? [generator] : [];
const tokens = acceptedTokens.map((t) => t.token);
if (partialCmd) tokens.push(partialCmd);
return generatorSuggestionsTokens(generators, tokens, filterStrategy, partialCmd, cwd);
};
export const templateSuggestions = async (
const templateSuggestions = async (
templates: Fig.Template | undefined,
filterStrategy: FilterStrategy | undefined,
partialCmd: string | undefined,
cwd: string
): Promise<Fig.Suggestion[]> => {
): Promise<Suggestion[]> => {
log.debug("templateSuggestions", templates, filterStrategy, partialCmd);
return filter<Fig.Suggestion>(await runTemplates(templates ?? [], cwd), filterStrategy, partialCmd, undefined);
};
export const suggestionSuggestions = (
const suggestionSuggestions = (
suggestions: (string | Fig.Suggestion)[] | undefined,
filterStrategy: FilterStrategy | undefined,
partialCmd: string | undefined
): Fig.Suggestion[] => {
): Suggestion[] => {
const cleanedSuggestions = suggestions?.map((s) => (typeof s === "string" ? { name: s } : s)) ?? [];
return filter<Fig.Suggestion>(cleanedSuggestions ?? [], filterStrategy, partialCmd, undefined);
};
export const subcommandSuggestions = (
const subcommandSuggestions = (
subcommands: Fig.Subcommand[] | undefined,
filterStrategy: FilterStrategy | undefined,
partialCmd: string | undefined
): Fig.Suggestion[] => {
): Suggestion[] => {
return filter<Fig.Subcommand>(subcommands ?? [], filterStrategy, partialCmd, "subcommand");
};
@@ -208,7 +196,7 @@ const optionSuggestions = (
acceptedTokens: CommandToken[],
filterStrategy: FilterStrategy | undefined,
partialCmd: string | undefined
): Fig.Suggestion[] => {
): Suggestion[] => {
log.debug("optionSuggestions", options, acceptedTokens, filterStrategy, partialCmd);
const usedOptions = new Set(acceptedTokens.filter((t) => t.isOption).map((t) => t.token));
const validOptions = options?.filter(
@@ -217,23 +205,23 @@ const optionSuggestions = (
return filter<Fig.Option>(validOptions ?? [], filterStrategy, partialCmd, "option");
};
const removeAcceptedSuggestions = (suggestions: Fig.Suggestion[], acceptedTokens: CommandToken[]): Fig.Suggestion[] => {
const removeAcceptedSuggestions = (suggestions: Suggestion[], acceptedTokens: CommandToken[]): Suggestion[] => {
const seen = new Set<string>(acceptedTokens.map((t) => t.token));
return suggestions.filter((s) => matchAny(s.name, (n) => !seen.has(n)));
return suggestions.filter((s) => s.allNames.every((n) => !seen.has(n)));
};
const removeDuplicateSuggestion = (suggestions: Fig.Suggestion[]): Fig.Suggestion[] => {
const removeDuplicateSuggestion = (suggestions: Suggestion[]): Suggestion[] => {
const seen = new Set<string>();
return suggestions
.map((s) => {
if (matchAny(s.name, (n) => seen.has(n))) return null;
for (const name of s.name) seen.add(name);
if (seen.has(s.name)) return null;
seen.add(s.name);
return s;
})
.filter((s): s is Fig.Suggestion => s != null);
.filter((s): s is Suggestion => s != null);
};
const removeEmptySuggestion = (suggestions: Fig.Suggestion[]): Fig.Suggestion[] => {
const removeEmptySuggestion = (suggestions: Suggestion[]): Suggestion[] => {
return suggestions.filter((s) => s.name.length > 0);
};
@@ -257,7 +245,7 @@ export const getSubcommandDrivenRecommendation = async (
partialCmd = partialToken.isPathComplete ? "" : getApi().pathBaseName(partialCmd ?? "");
}
const suggestions: Fig.Suggestion[] = [];
const suggestions: Suggestion[] = [];
const argLength = subcommand.args instanceof Array ? subcommand.args.length : subcommand.args ? 1 : 0;
const allOptions = persistentOptions.concat(subcommand.options ?? []);
log.debug("allOptions", allOptions, persistentOptions, subcommand.options, subcommand.args, argLength);
-1
View File
@@ -41,7 +41,6 @@ const historyTemplate = (): Fig.TemplateSuggestion[] => {
templateType: "history",
},
insertValue,
type: "special",
});
}
}
+3 -219
View File
@@ -52,17 +52,6 @@ export const buildExecuteShellCommand =
return output;
};
export const resolveCwdToken = async (
token: string,
cwd: string,
shell: Shell
): Promise<{ cwd: string; pathy: boolean; complete: boolean }> => {
if (token == null) return { cwd, pathy: false, complete: false };
const sep = shell == Shell.Bash ? "/" : getApi().pathSep();
if (!token.includes(sep)) return { cwd, pathy: false, complete: false };
return { cwd: cwd, pathy: true, complete: token.endsWith(sep) };
};
export const resolveCwd = async (
cmdToken: CommandToken | undefined,
cwd: string,
@@ -70,15 +59,11 @@ export const resolveCwd = async (
): Promise<{ cwd: string; pathy: boolean; complete: boolean }> => {
if (cmdToken == null) return { cwd, pathy: false, complete: false };
const { token } = cmdToken;
return resolveCwdToken(token, cwd, shell);
const sep = shell == Shell.Bash ? "/" : getApi().pathSep();
if (!token.includes(sep)) return { cwd, pathy: false, complete: false };
return { cwd: cwd, pathy: true, complete: token.endsWith(sep) };
};
/**
* Retrieves the contents of the specified directory on the active remote machine.
* @param cwd The directory whose contents should be returned.
* @param tempType The template to use when returning the contents. If "folders" is passed, only the directories within the specified directory will be returned. Otherwise, all the contents will be returned.
* @returns The contents of the directory formatted to the specified template.
*/
export const getCompletionSuggestions = async (
cwd: string,
tempType: "filepaths" | "folders"
@@ -101,204 +86,3 @@ export const getCompletionSuggestions = async (
}
}
};
/**
* Runs the comparator function on each value and returns true if any of them match
* @param values The value(s) to check
* @param comparator The function to use to compare the values
* @returns True if any of the values match the comparator
*/
export function matchAny<T>(values: Fig.SingleOrArray<T>, comparator: (a: T) => boolean) {
if (Array.isArray(values)) {
for (const value of values) {
if (comparator(value)) {
return true;
}
}
return false;
} else {
return comparator(values);
}
}
/**
* Checks if any of the values start with the input string
* @param values The value(s) to check
* @param input The input to check against
* @returns True if any of the values start with the input
*/
export function startsWithAny(values: Fig.SingleOrArray<string>, input: string) {
return matchAny(values, (a) => a.startsWith(input));
}
/**
* Checks if any of the values are not equal to the input
* @param values The value(s) to check
* @param input The input to check against
* @returns True if any of the values are not equal to the input
*/
export function equalsAny<T>(values: Fig.SingleOrArray<T>, input: T) {
return matchAny(values, (a) => a == input);
}
/**
* Checks if any of the values of the SingleOrArray are not in the array
* @param values The value(s) to check
* @param arr The array to check against
* @returns True if any of the values are not in the array
*/
export function notInAny<T>(values: Fig.SingleOrArray<T>, arr: T[]) {
return matchAny(values, (a) => !arr.includes(a));
}
/**
* Get the first element of a Fig.SingleOrArray<T>.
* @param values Either a single value or an array of values of the specified type.
* @returns The first element of the array, or the value.
*/
export function getFirst<T>(values: Fig.SingleOrArray<T>): T {
if (Array.isArray(values)) {
return values[0];
}
return values;
}
/**
* Get all elements of a Fig.SingleOrArray<T>
* @param values Either a single value or an array of values of the specified type.
* @returns The array of values, or an empty array
*/
export function getAll<T>(values: Fig.SingleOrArray<T> | undefined): T[] {
if (Array.isArray(values)) {
return values;
}
return [values as T];
}
/**
* Checks if a string is an option, i.e. starts with "--".
* @param value The string to check.
* @returns True if the string is an option.
*/
export function isOption(value: string): boolean {
return value.startsWith("--");
}
/**
* Checks if a string is a flag, i.e. starts with "-" but not "--".
* @param value The string to check.
* @returns True if the string is a flag.
*/
export function isFlag(value: string): boolean {
return value.startsWith("-") && !isOption(value);
}
/**
* Checks if a string is either a flag or an option, i.e. starts with "-".
* @param value The string to check.
* @returns True if the string is a flag or an option.
*/
export function isFlagOrOption(value: string): boolean {
return value.startsWith("-");
}
/**
* Get the flag of a Fig.SingleOrArray<string>.
* @param values Either a string or an array of strings.
* @returns The flag, or undefined if none is found.
*/
export function getFlag(values: Fig.SingleOrArray<string>): string | undefined {
if (Array.isArray(values)) {
for (const value of values) {
if (isFlag(value)) {
return value;
}
}
} else if (isFlag(values)) {
return values;
}
return undefined;
}
/**
* Checks if a string is a command, i.e. not a flag or option.
* @param value The string to check.
* @returns True if the string is a command.
*/
export function isCommand(value: string): boolean {
return !isFlag(value) && !isOption(value);
}
/**
* Checks if an option suggestion contains a flag. If so, modifies the name to include the preceding flags.
* @param option The option to modify.
* @param precedingFlags The preceding flags to prepend to the suggestion name.
* @returns The modified option suggestion.
*/
export function modifyPosixFlags(option: Fig.Option, precedingFlags: string): Fig.Option {
// We only want to modify the name if the option is a flag
if (option.name) {
// Get the name of the flag without the preceding "-"
const name = getFlag(option.name)?.slice(1);
if (name) {
// Shallow copy the option so we can modify the name without modifying the original spec.
option = { ...option };
// We want to prepend the existing flags to the name, except for the suggestion of the last flag (i.e. the `c` of an input `-abc`), which we want to replace with the existing flags.
// The end result is that we will suggest -abc instead of -a -b -c. We do not want -abb. The case of -aba should already be covered by filterFlags.
if (name === precedingFlags.at(-1)) {
option.name = "-" + precedingFlags;
} else {
option.name = "-" + precedingFlags + name;
}
}
}
return option;
}
/**
* Sort suggestions in-place by priority, then by name.
* @param suggestions The suggestions to sort.
*/
export function sortSuggestions(suggestions: Fig.Suggestion[]) {
suggestions.sort((a, b) => {
if (a.priority == b.priority) {
if (a.name) {
if (b.name) {
return getFirst(a.name).trim().localeCompare(getFirst(b.name));
} else {
return -1;
}
} else if (b.name) {
return 1;
}
}
return (b.priority ?? 0) - (a.priority ?? 0);
});
}
/**
* Merge two subcommand objects, with the second subcommand taking precedence in case of conflicts.
* @param subcommand1 The first subcommand.
* @param subcommand2 The second subcommand.
* @returns The merged subcommand.
*/
export function mergeSubcomands(subcommand1: Fig.Subcommand, subcommand2: Fig.Subcommand): Fig.Subcommand {
log.debug("merging two subcommands", subcommand1, subcommand2);
const newCommand: Fig.Subcommand = { ...subcommand1 };
// Merge the generated spec with the existing spec
for (const key in subcommand2) {
if (Array.isArray(subcommand2[key])) {
newCommand[key] = [...subcommand2[key], ...(newCommand[key] ?? [])];
continue;
} else if (typeof subcommand2[key] === "object") {
newCommand[key] = { ...subcommand2[key], ...(newCommand[key] ?? {}) };
} else {
newCommand[key] = subcommand2[key];
}
}
log.debug("merged subcommand:", newCommand);
return newCommand;
}
+1 -5
View File
@@ -125,11 +125,7 @@ export class AutocompleteModel {
let retVal = "";
if (autocompleteSuggestions != null && autocompleteSuggestions.suggestions.length > index) {
const suggestion = autocompleteSuggestions.suggestions[index];
if (typeof suggestion.name === "string") {
retVal = suggestion.name;
} else if (suggestion.name.length > 0) {
retVal = suggestion.name[0];
}
retVal = suggestion.name;
if (suggestion.insertValue) {
retVal = suggestion.insertValue;
}
+6 -1
View File
@@ -609,6 +609,11 @@ class Model {
if (activeScreen == null) {
return;
}
let numLines = activeScreen.getScreenLines().lines.length;
if (numLines < 10) {
GlobalCommandRunner.screenDelete(activeScreen.screenId, false);
return;
}
const rtnp = this.showAlert({
message: "Are you sure you want to delete this tab?",
confirm: true,
@@ -617,7 +622,7 @@ class Model {
if (!result) {
return;
}
GlobalCommandRunner.screenDelete(activeScreen.screenId, true);
GlobalCommandRunner.screenDelete(activeScreen.screenId, false);
});
}
+1 -2
View File
@@ -1,10 +1,9 @@
{
"include": ["src/**/*", "types/**/*"],
"compilerOptions": {
"target": "es6",
"target": "es5",
"module": "commonjs",
"jsx": "preserve",
"lib": ["ES2022"],
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
+5 -6
View File
@@ -67,8 +67,8 @@ const WSStateReconnectTime = 30 * time.Second
const WSStatePacketChSize = 20
const InitialTelemetryWait = 30 * time.Second
const TelemetryTick = 30 * time.Minute
const TelemetryInterval = 8 * time.Hour
const TelemetryTick = 10 * time.Minute
const TelemetryInterval = 4 * time.Hour
const MaxWriteFileMemSize = 20 * (1024 * 1024) // 20M
@@ -930,12 +930,11 @@ func checkNewReleaseWrapper() {
}
func telemetryLoop() {
var lastSent time.Time
var nextSend int64
time.Sleep(InitialTelemetryWait)
for {
dur := time.Since(lastSent)
if lastSent.IsZero() || dur >= TelemetryInterval {
lastSent = time.Now()
if time.Now().Unix() > nextSend {
nextSend = time.Now().Add(TelemetryInterval).Unix()
sendTelemetryWrapper()
checkNewReleaseWrapper()
}