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.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>
Config file for wdiwtlt CLI.
-L --library-root <root-dir>
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
}
}