import { keyframes } from "@emotion/core"; import styled from "@emotion/styled"; import { Spinner } from "evergreen-ui"; import Fuse from "fuse.js"; import * as _ from "lodash"; import React, { createRef, useEffect, useRef, useState } from "react"; import SimpleBar from "simplebar-react"; import { LIGHT_SECONDARY_FIVE, LIGHT_SECONDARY_THREE, LIGHT_SECONDARY_TWO, LIGHT_TERTIARY_THREE, Theme, } from "../../../../shared/colors"; import { Command, CommandLineModes, useCommandLineContext, } from "../../../providers/CommandLineProvider"; import ClickOutside from "../ClickOutside"; import Input from "../InputWithShortcuts"; type Props = {}; type State = { search: string; }; function CommandLine(props: Props) { const context = useCommandLineContext(); const { commands, isVisible, toggle, prompt, placeholder, isLoading, mode, addOptions, setIsVisible, promptCallback, } = context; const commandLineInputRef = createRef(); const [search, setSearch] = useState(""); const [currentIndex, setCurrentIndex] = useState(0); const [activeCommand, setActiveCommand] = useState(null); const commandRef = useRef(null); const [isTrackingMouse, setIsTrackingMouse] = useState(false); const [isPressed, setIsPressed] = useState(false); const [animationVisible, setAnimationVisible] = useState(false); const currentCommand = commands[currentIndex]; useEffect(() => { if (search && commandLineInputRef.current) { commandLineInputRef.current.select(); } }, [isVisible]); useEffect(() => { setCurrentIndex(0); }, [commands]); const leaveAnimation = () => { setAnimationVisible(true); }; const bubbleUpEscape = () => { if (!_.isEmpty(search)) { // claer the search first before existing setSearch(""); return; } else { // previously active command onCnancel if (activeCommand?.onCancel) { activeCommand.onCancel(); } else if (currentCommand?.onCancel) { currentCommand.onCancel(); } setActiveCommand(null); leaveAnimation(); } }; const handlePromptAction = async () => { if (mode !== CommandLineModes.PROMPT) { return; } await promptCallback(search); }; let shownCommmands: Command[] = []; if (commands.length > 0 && mode === CommandLineModes.COMMAND) { shownCommmands = commands; } else if (mode === CommandLineModes.OPTION) { shownCommmands = commands.filter((command) => command.type === "OPTION"); } else if (commands.length === 0 || mode === CommandLineModes.PROMPT) { // couldn't find any commands.... show a zero state, like superhuman? shownCommmands = []; } // Filter oout duplicates shownCommmands = _.uniqBy(shownCommmands, (command) => command.id); // Apply search filters if (!_.isEmpty(search)) { // run the frontend saerch const fuse = new Fuse(shownCommmands, { includeScore: true, keys: ["title"], }); const result = fuse.search(search); shownCommmands = result.map((result) => result.item); } // Apply ranking by priority shownCommmands = _.orderBy(shownCommmands, ["priority"], ["desc"]); const updateSearch = (event: React.ChangeEvent) => { setSearch(event.target.value); }; const handleSubmit = (index: number) => { // submitting const submitCommand = shownCommmands[currentIndex]; if (submitCommand?.onSelect) { submitCommand.onSelect(); // new active command setActiveCommand(submitCommand); // clear input setSearch(""); } }; const handleKeyDown = (event: any) => { if (event.key === "Enter") { setIsPressed(true); } }; const handleKeyUp = (event: any) => { setIsPressed(false); // if start typing, don't track mouse setIsTrackingMouse(false); if (!isVisible) { return; } const { key } = event; let handled = true; const totalLength = shownCommmands.length; if (key === "Enter") { handleSubmit(currentIndex); } else if (key === "ArrowDown") { if (currentIndex === totalLength - 1) { setCurrentIndex(0); } else { setCurrentIndex((prev) => prev + 1); } if (commandRef.current) { commandRef.current.scrollIntoView({ behavior: "auto", block: "nearest", }); } } else if (key === "ArrowUp") { if (currentIndex === 0) { setCurrentIndex(totalLength - 1); } else { setCurrentIndex((prev) => prev - 1); } if (commandRef.current) { commandRef.current.scrollIntoView({ behavior: "auto", block: "nearest", }); } } else { handled = false; } if (handled) { event.stopPropagation(); event.preventDefault(); } }; useEffect(() => { window.addEventListener("keyup", handleKeyUp); window.addEventListener("keydown", handleKeyDown); return () => { window.removeEventListener("keyup", handleKeyUp); window.removeEventListener("keydown", handleKeyDown); }; }); if (!isVisible) { return null; } if (isLoading) { const currentActive = shownCommmands[currentIndex]; return ( {prompt}

{currentActive.title}

); } return ( { toggle(); // previously active command onCnancel if (activeCommand?.onCancel) { activeCommand.onCancel(); } else if (currentCommand?.onCancel) { currentCommand.onCancel(); } }} > { // Do this to have the leave animation if (animationVisible) { setIsVisible(false); setAnimationVisible(false); } }} > {prompt} { // if leave, track mouse again setIsTrackingMouse(true); }} onMouseEnter={() => { setIsTrackingMouse(true); }} > {shownCommmands.map((command, index) => { return ( { if (isTrackingMouse) { setCurrentIndex(index); } }} key={command.id} active={index === currentIndex} ref={index === currentIndex ? commandRef : undefined} onClick={() => { handleSubmit(index); }} >

{command.title || "Untitled"}

); })}
); } export default CommandLine; const zoomKeyframe = (visible: boolean) => keyframes` from { transform: ${!visible ? "scale3d(1, 1, 1)" : "scale3d(.9, .9, .9)"}; opacity: ${!visible ? 1 : 0}; } to { transform: ${!visible ? "scale3d(.9, .9, .9)" : "scale3d(1, 1, 1)"}; opacity: ${!visible ? 0 : 1}; } `; const Container = styled.div<{ isPressed: boolean; visible: boolean }>` display: flex; align-self: center; position: absolute; left: calc(50% - 400px); top: ${(props) => (props.isPressed ? "calc(5% + 1px)" : "5%")}; background-color: #212121; margin: auto; z-index: 999; padding-top: 16px; border-radius: 4px; box-shadow: ${(props) => props.isPressed ? "0 10px 20px 0 rgba(0, 0, 0, 0.1), 0 13px 35px 0 rgba(0, 0, 0, 0.3)" : "0 15px 25px 0 rgba(0, 0, 0, 0.1), 0 19px 45px 0 rgba(0, 0, 0, 0.3)"}; animation: ${(props) => zoomKeyframe(props.visible)} both cubic-bezier(0.4, 0, 0, 1.5); animation-duration: 0.1s; transition: 0.06s ease-in-out; `; const CommandLineDisplay = styled.div` width: 800px; display: flex; flex-direction: column; `; const CommandLinePrompt = styled.div` color: #adadad; padding: 0px 24px 8px; border-bottom: 1px solid #3c3d3c; `; const CommandLineInput = styled(Input)` -webkit-appearance: none; border: none; background-image: none; background-color: transparent; :focus { outline: none; } ::placeholder { color: #a0a0a0; } color: rgb(244, 244, 246); padding: 16px 24px; font-size: 24px; `; const CommandItem = styled.div` display: flex; align-items: center; background-image: none; background-color: ${(props) => (props.active ? "#3C3D3C" : "transparent")}; padding: ${(props) => (props.active ? "8px 24px 8px 22px" : "8px 24px")}; cursor: pointer; color: ${(props) => props.active ? LIGHT_SECONDARY_FIVE : LIGHT_SECONDARY_THREE}; ${(props) => props.active && `border-left: 2px solid ${LIGHT_TERTIARY_THREE};`} transition: color 0.08s ease-in-out; `;