WIP Implementing fancy CLI input handling (not working).

This commit is contained in:
Jonathan Bernard 2016-02-10 05:15:39 -06:00
parent f5ab973b43
commit 6a73b1ca8c

View File

@ -4,10 +4,13 @@ import com.jdbernard.wdiwtlt.ConfigWrapper
import com.jdbernard.wdiwtlt.MediaLibrary import com.jdbernard.wdiwtlt.MediaLibrary
import com.jdbernard.wdiwtlt.db.ORM import com.jdbernard.wdiwtlt.db.ORM
import com.jdbernard.io.NonBlockingInputStreamReader import com.jdbernard.io.NonBlockingInputStreamReader
import com.jdbernard.util.AnsiEscapeCodeSequence as ANSI
import com.zaxxer.hikari.HikariConfig import com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource import com.zaxxer.hikari.HikariDataSource
import org.docopt.Docopt import org.docopt.Docopt
import static com.jdbernard.util.AnsiEscapeCodeSequence.*
public class CommandLineInterface { public class CommandLineInterface {
public static final VERSION = "ALPHA" public static final VERSION = "ALPHA"
@ -21,6 +24,10 @@ Usage:
wdiwtlt --help wdiwtlt --help
Options: Options:
-c --config <config-file>
Config file for wdiwtlt CLI.
-L --library-root <root-dir> -L --library-root <root-dir>
The path to a local media library directory. Providing a local library The path to a local media library directory. Providing a local library
@ -37,8 +44,27 @@ Options:
Configuration: Configuration:
""" """
private Properties cliConfig
private MediaLibrary library 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) { public static void main(String[] args) {
def opts = new Docopt(DOC).withVersion("wdiwtlt v$VERSION").parse(args) def opts = new Docopt(DOC).withVersion("wdiwtlt v$VERSION").parse(args)
@ -49,22 +75,43 @@ Configuration:
System.out.err.println("wdiwtlt: $msg") System.out.err.println("wdiwtlt: $msg")
System.exit(1) } 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) if (!cfgFile || !cfgFile.exists() || !cfgFile.isFile())
File libRoot = opts["--library-root"] ? cfgFile = new File("wdiwtlt.cli.properties")
new File(opts["--library-root"]) :
new File(wdiwtltCfg.libraryRootPath)
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: " + exitErr("Library root does not exist or is not a directory: " +
libRoot.canonicalPath) libRoot.canonicalPath)
// Get the library database // Get the library database
HikariConfig hcfg HikariConfig hcfg
File dbCfgFile = opts["--database-config"] ? File dbCfgFile = new File(
new File(opts["--database-config"]) : null 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()) { if (dbCfgFile && dbCfgFile.exists() && dbCfgFile.isFile()) {
Properties props = new Properties() Properties props = new Properties()
try { dbCfgFile.withInputStream { props.load(it) } } try { dbCfgFile.withInputStream { props.load(it) } }
@ -72,7 +119,17 @@ Configuration:
if (props) hcfg = new HikariConfig(props) } 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.") if (!hcfg) exitErr("Cannot load database configuation.")
// Create our database instance // Create our database instance
@ -85,14 +142,170 @@ Configuration:
e.localizedMessage) } e.localizedMessage) }
// Create our CLI object // 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() cliInst.repl()
} }
public CommandLineInterface(MediaLibrary library) { public CommandLineInterface(MediaLibrary library, InputStream sin,
PrintStream out, Properties cliConfig) {
this.cliConfig = cliConfig
this.library = library 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
} }
} }