diff --git a/cli/src/main/groovy/com/jdbernard/wdiwtlt/cli/CommandLineInterface.groovy b/cli/src/main/groovy/com/jdbernard/wdiwtlt/cli/CommandLineInterface.groovy index 91820f2..56756c4 100644 --- a/cli/src/main/groovy/com/jdbernard/wdiwtlt/cli/CommandLineInterface.groovy +++ b/cli/src/main/groovy/com/jdbernard/wdiwtlt/cli/CommandLineInterface.groovy @@ -4,10 +4,13 @@ import com.jdbernard.wdiwtlt.ConfigWrapper import com.jdbernard.wdiwtlt.MediaLibrary import com.jdbernard.wdiwtlt.db.ORM import com.jdbernard.io.NonBlockingInputStreamReader +import com.jdbernard.util.AnsiEscapeCodeSequence as ANSI import com.zaxxer.hikari.HikariConfig import com.zaxxer.hikari.HikariDataSource import org.docopt.Docopt +import static com.jdbernard.util.AnsiEscapeCodeSequence.* + public class CommandLineInterface { public static final VERSION = "ALPHA" @@ -21,6 +24,10 @@ Usage: wdiwtlt --help Options: + -c --config + + Config file for wdiwtlt CLI. + -L --library-root The path to a local media library directory. Providing a local library @@ -37,8 +44,27 @@ Options: Configuration: """ + private Properties cliConfig private MediaLibrary library + private String[] linesBuffer = new String[1024] + private StringBuilder currentLine = new StringBuilder() + private int nextAvailableLineIdx = 0 + private int newLineIdx = 0 + private String selectedLineIdx = 0 + private InputStream sin + + private PrintStream out + + private boolean running + + private String titleStyle, normalStyle, promptStyle, errorStyle + private String clearRemainingLine = new ANSI().eraseLine(Erase.ToEnd).toString() + private String clearLine = new ANSI().eraseLine(Erase.All).toString() + + private String errorMsg = "" + private int lastPromptLineCount = 0 + public static void main(String[] args) { def opts = new Docopt(DOC).withVersion("wdiwtlt v$VERSION").parse(args) @@ -49,22 +75,43 @@ Configuration: System.out.err.println("wdiwtlt: $msg") System.exit(1) } - def wdiwtltCfg = new ConfigWrapper() + // Look for a given CLI config file. + def givenCfg = new Properties() + File cfgFile + if (opts["--config"]) cfgFile = new File(opts["--config"]) - // Get the library root (if local) - File libRoot = opts["--library-root"] ? - new File(opts["--library-root"]) : - new File(wdiwtltCfg.libraryRootPath) + if (!cfgFile || !cfgFile.exists() || !cfgFile.isFile()) + cfgFile = new File("wdiwtlt.cli.properties") - if (libRoot && (!libRoot.exist() || !libRoot.isDirectory())) + if (!cfgFile || !cfgFile.exists() || !cfgFile.isFile()) + cfgFile = new File(System.getenv().HOME, ".wdiwtlt.cli.properties") + + if (cfgFile.exists() && cfgFile.isFile()) { + try { cfgFile.withInputStream { givenCfg.load(it) } } + catch (Exception e) { givenCfg.clear() } } + + // Just in case, load up the general WDIWTLT default config service + def wdiwtltDefaultConfig = new ConfigWrapper() + + // Find the library root directory (TODO: eventually this should be + // enabled only when local mode is chosen). + File libRoot = new File( + opts["--library-root"] ?: + givenCfg[ConfigWrapper.LIBRARY_DIR_KEY] ?: + wdiwtltDefaultConfig.libraryRootPath) + + if (libRoot && (!libRoot.exists() || !libRoot.isDirectory())) exitErr("Library root does not exist or is not a directory: " + libRoot.canonicalPath) // Get the library database HikariConfig hcfg - File dbCfgFile = opts["--database-config"] ? - new File(opts["--database-config"]) : null + File dbCfgFile = new File( + opts["--database-config"] ?: + givenCfg[ConfigWrapper.DB_CONFIG_FILE_KEY] ?: + "database.properties") + // Try to load from the given DB config file if (dbCfgFile && dbCfgFile.exists() && dbCfgFile.isFile()) { Properties props = new Properties() try { dbCfgFile.withInputStream { props.load(it) } } @@ -72,7 +119,17 @@ Configuration: if (props) hcfg = new HikariConfig(props) } - if (!hcfg) hcfg = wdiwtltCfg.hikariConfig + // Look to see if database connection properties are given in the CLI + // config file. + if (givenCfg && givenCfg["database.config.dataSourceClassName"]) { + Properties props = new Properties() + props.putAll(givenCfg + .findAll { it.key.startsWith("database.config.") } + .collectEntries { [it.key[16..-1], it.value] } ) + if (props) hcfg = new HikariConfig(props) } + + // Fall back to the WDIWTLT general database defaults + if (!hcfg) hcfg = wdiwtltDefaultConfig.hikariConfig if (!hcfg) exitErr("Cannot load database configuation.") // Create our database instance @@ -85,14 +142,170 @@ Configuration: e.localizedMessage) } // Create our CLI object - def cliInst = new CommandLineInterface(new MediaLibrary(orm, libRoot)) + def cliInst = new CommandLineInterface(new MediaLibrary(orm, libRoot), + System.in, System.out, givenCfg) cliInst.repl() } - public CommandLineInterface(MediaLibrary library) { + public CommandLineInterface(MediaLibrary library, InputStream sin, + PrintStream out, Properties cliConfig) { + this.cliConfig = cliConfig this.library = library + this.out = out + this.sin = sin + + setupTextStyles() } - public void repl(InputStream sin, PrintStream out) { + void setupTextStyles() { + titleStyle = new ANSI().color(Colors.BLUE, Colors.DEFAULT, true).toString() + normalStyle = new ANSI().resetText().toString() + promptStyle = new ANSI().color(Colors.YELLOW, Colors.DEFAULT, true).toString() + errorStyle = new ANSI().color(Colors.RED, Colors.DEFAULT, true).toString() + } + + public void repl() { + this.running = true + String line = null + + printPrompt() + while(this.running) { + + line = readInputLine() + if (line != null) { + out.flush() + errorMsg = "" + switch(line) { + case ~/quit|exit|\u0004/: + running = false + break + + default: + errorMsg = "Unrecognized command: '$line'" + Thread.sleep(200) + break + } + printPrompt(true) + } else { + printPrompt(true) + Thread.sleep(200) + } + } + } + + private void printPrompt(boolean redraw = false) { + StringBuilder prompt = new StringBuilder() + + if (redraw) prompt.append(new ANSI() + .saveCursor() + .cursorPrevLine(lastPromptLineCount)) + .append(clearLine) + + prompt.append(titleStyle) + .append("WDIWTLT - v") + .append(VERSION) + .append("-- ") + .append(normalStyle) + .append("TODO\n") + + if (errorMsg) prompt + .append(clearLine) + .append(errorStyle) + .append(errorMsg) + .append("\n") + + if (redraw) prompt.append(new ANSI().restoreCursor()) + else prompt.append(promptStyle) + .append("> ") + .append(normalStyle) + + + lastPromptLineCount = 1 + (errorMsg ? 1 : 0) + + out.print(prompt.toString()) + out.flush() + } + + private void replaceCurrentLine(String newLine) { + String toPrint = new StringBuilder() + .append(new ANSI().cursorHorizontalAbsolute(3)) + .append(clearRemainingLine) + .append(newLine) + .toString() + currentLine = new StringBuilder(newLine) + out.print(toPrint) + out.flush() + } + + private String readInputLine() { + int bytesAvailable = sin.available() + if (bytesAvailable) { + byte[] input = new byte[bytesAvailable] + sin.read(input) + + int idx = 0 + while (idx < input.length) { + switch (input[idx]) { + case 0x08: // backspace + case 0x0B: // enter + linesBuffer[newLineIdx] = currentLine.toString() + newLineIdx = (newLineIdx + 1) % linesBuffer.length + linesBuffer[newLineIdx] = null + replaceCurrentLine("") + break + case 0x1B: // escape (prefix to arrows, etc) + // arrows + if (input.length > idx + 2 && input[idx + 1] == 0x5B) { + switch(input[idx + 2]) { + case 0x41: // up + int ni = selectedLineIdx + ni = (ni - 1 + linesBuffer.length) % + linesBuffer.length + if (linesBuffer[ni] != null) { + selectedLineIdx = ni + replaceCurrentLine(linesBuffer[ni]) } + else { + out.print(new ANSI().cursorBackwards(4) + + clearRemainingLine) + out.flush() } + break + case 0x42: // down + int ni = selectedLineIdx + ni = (ni + 1) % linesBuffer.length + if (linesBuffer[ni] != null) { + selectedLineIdx = ni + replaceCurrentLine(linesBuffer[ni]) } + else { + out.print(new ANSI().cursorBackwards(4) + + clearRemainingLine) + out.flush() } + break + case 0x43: case 0x44: // right & left + out.print(new ANSI().cursorBackwards(4) + + clearRemainingLine) + out.flush() + break + default: break + } + + idx += 2 + } + + else currentLine.append((char)input[idx]) + break + default: + currentLine.append((char)input[idx]) + break + } + idx++ + } + } + + if (linesBuffer[nextAvailableLineIdx] != null) { + String line = linesBuffer[nextAvailableLineIdx] + nextAvailableLineIdx = + (nextAvailableLineIdx + 1) % linesBuffer.length + return line } + else return null } }