diff --git a/packages/cli/src/cmds/components/LogViewer.tsx b/packages/cli/src/cmds/components/LogViewer.tsx index 2ee0105c..e2dd7de4 100644 --- a/packages/cli/src/cmds/components/LogViewer.tsx +++ b/packages/cli/src/cmds/components/LogViewer.tsx @@ -1,14 +1,14 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useCallback } from "react"; import { Box, Text, useInput, useApp } from "ink"; import chalk from "chalk"; import fs from "node:fs"; +import { executeTests } from "../runTests"; interface LogViewerProps { env: any; logFilePath: string; onExit: () => void; onTest: () => Promise; - onGrep: () => Promise; onNextLog?: () => void; onPrevLog?: () => void; zombieInfo?: { @@ -23,7 +23,6 @@ export const LogViewer: React.FC = ({ logFilePath, onExit, onTest, - onGrep, onNextLog, onPrevLog, zombieInfo, @@ -32,8 +31,50 @@ export const LogViewer: React.FC = ({ const [tailing, setTailing] = useState(true); const [currentReadPosition, setCurrentReadPosition] = useState(0); const [isExecutingCommand, setIsExecutingCommand] = useState(false); + const [isGrepMode, setIsGrepMode] = useState(false); + const [grepInput, setGrepInput] = useState(process.env.MOON_GREP || "D01T01"); + const [showCursor, setShowCursor] = useState(true); const { exit } = useApp(); + useEffect(() => { + const hideCursor = () => { + process.stdout.write('\x1B[?25l'); + }; + + hideCursor(); + + const cursorCheck = setInterval(hideCursor, 100); + + return () => { + clearInterval(cursorCheck); + process.stdout.write('\x1B[?25h'); + }; + }, []); + + const handleGrepSubmit = useCallback(async () => { + process.env.MOON_RECYCLE = "true"; + process.env.MOON_GREP = grepInput; + const opts: any = { + testNamePattern: grepInput, + silent: true, + subDirectory: process.env.MOON_SUBDIR, + }; + opts.reporters = ["dot"]; + + setIsExecutingCommand(true); + removeWatcher(); + try { + await executeTests(env, opts); + } finally { + setIsGrepMode(false); + setIsExecutingCommand(false); + if (tailing) { + setupWatcher(); + readLog(); + } + } + }, [grepInput, env, tailing]); + const resumePauseProse = [ `, ${chalk.bgWhite.black("[p]")} Pause tail`, `, ${chalk.bgWhite.black("[r]")} Resume tail`, @@ -62,6 +103,19 @@ export const LogViewer: React.FC = ({ }; useInput((input, key) => { + if (isGrepMode) { + if (key.return) { + handleGrepSubmit(); + } else if (key.escape) { + setIsGrepMode(false); + } else if (key.backspace || key.delete) { + setGrepInput(prev => prev.slice(0, -1)); + } else if (input && !key.ctrl && !key.meta) { + setGrepInput(prev => prev + input); + } + return; + } + if (input === "q") { fs.unwatchFile(logFilePath); onExit(); @@ -92,15 +146,7 @@ export const LogViewer: React.FC = ({ } if (input === "g") { - setIsExecutingCommand(true); - removeWatcher(); - onGrep().finally(() => { - setIsExecutingCommand(false); - if (tailing) { - setupWatcher(); - readLog(); - } - }); + setIsGrepMode(true); } if (input === "," && onNextLog) { @@ -116,6 +162,17 @@ export const LogViewer: React.FC = ({ } }); + // Control blinking cursor for grep mode + useEffect(() => { + const timer = setInterval(() => { + setShowCursor(prev => !prev); + }, 500); + + return () => { + clearInterval(timer); + }; + }, [isGrepMode]); + const readLog = () => { if (!tailing) return; @@ -166,14 +223,15 @@ export const LogViewer: React.FC = ({ }, [logFilePath, tailing]); return ( - - - {logs.slice(-process.stdout.rows + 2).map((line, i) => ( + + + {logs.slice(-process.stdout.rows + 1).map((line, i) => ( {line} ))} - {!isExecutingCommand && ( - + {!isExecutingCommand && !isGrepMode && ( + + {"─".repeat(process.stdout.columns)} {bottomBarBase} {resumePauseProse[tailing ? 0 : 1]} @@ -181,6 +239,14 @@ export const LogViewer: React.FC = ({ )} + {!isExecutingCommand && isGrepMode && ( + + {"─".repeat(process.stdout.columns)} + + Pattern to filter (ID/Title) [Enter to submit, Esc to cancel]: {grepInput}{showCursor ? "▋" : " "} + + + )} ); }; diff --git a/packages/cli/src/cmds/entrypoint.ts b/packages/cli/src/cmds/entrypoint.ts index 7a3b04d4..bc1ce774 100755 --- a/packages/cli/src/cmds/entrypoint.ts +++ b/packages/cli/src/cmds/entrypoint.ts @@ -5,10 +5,33 @@ import { hideBin } from "yargs/helpers"; import { fetchArtifact, deriveTestIds, generateConfig, type fetchArtifactArgs } from "../internal"; import { main } from "./main"; import { runNetworkCmd } from "./runNetwork"; -import { testCmd, testRunArgs } from "./runTests"; +import { testCmd } from "./runTests"; import { configSetup } from "../lib/configReader"; dotenv.config(); +function handleCursor() { + const hideCursor = "\x1B[?25l"; + const showCursor = "\x1B[?25h"; + + process.stdout.write(hideCursor); + + process.on("exit", () => { + process.stdout.write(showCursor); + }); + + process.on("SIGINT", () => { + process.stdout.write(showCursor); + process.exit(0); + }); + + process.on("SIGTERM", () => { + process.stdout.write(showCursor); + process.exit(0); + }); +} + +handleCursor(); + configSetup(process.argv); export type RunCommandArgs = { diff --git a/packages/cli/src/cmds/runNetwork.tsx b/packages/cli/src/cmds/runNetwork.tsx index 49572021..1e78bcf6 100644 --- a/packages/cli/src/cmds/runNetwork.tsx +++ b/packages/cli/src/cmds/runNetwork.tsx @@ -329,7 +329,7 @@ const resolveTailChoice = async (env: Environment) => { logFilePath={logFilePath} onExit={() => resolve()} onTest={async () => { await resolveTestChoice(env, true) }} - onGrep={async () => { await resolveGrepChoice(env, true) }} + // onGrep={async () => { await resolveGrepChoice(env, true) }} onNextLog={ zombieNodes ? () => {