--mode interactive -> basic selection and confirmation dialogs for the CLI

This commit is contained in:
Reinhard Pointner 2016-11-25 04:13:39 +08:00
parent 9a756aa3f5
commit 934976c0a2
12 changed files with 226 additions and 40 deletions

View File

@ -34,5 +34,6 @@
<classpathentry kind="lib" path="lib/ivy/jar/streamex.jar"/>
<classpathentry kind="lib" path="lib/jars/AppleJavaExtensions.jar"/>
<classpathentry kind="lib" path="lib/ivy/jar/controlsfx.jar" sourcepath="lib/ivy/source/controlsfx.jar"/>
<classpathentry kind="lib" path="lib/ivy/jar/lanterna.jar" sourcepath="lib/ivy/source/lanterna.jar"/>
<classpathentry kind="output" path="bin"/>
</classpath>

View File

@ -253,6 +253,11 @@
<include name="controlsfx.properties" />
</zipfileset>
<zipfileset src="${dir.lib}/ivy/jar/lanterna.jar">
<include name="com/googlecode/lanterna/**" />
<include name="**/*.properties" />
</zipfileset>
<!-- include classes and native libraries -->
<zipfileset src="${dir.lib}/ivy/jar/jna.jar">
<include name="com/sun/jna/**" />

View File

@ -25,6 +25,7 @@
<dependency rev="latest.release" org="com.optimaize.languagedetector" name="language-detector" />
<dependency rev="latest.release" org="one.util" name="streamex" />
<dependency rev="latest.release" org="org.controlsfx" name="controlsfx" />
<dependency rev="latest.release" org="com.googlecode.lanterna" name="lanterna" />
<!-- FileBot Scripting -->
<dependency rev="latest.release" org="org.apache.ant" name="ant" />

View File

@ -1,12 +1,14 @@
package net.filebot;
import java.io.File;
public interface RenameAction {
File rename(File from, File to) throws Exception;
default boolean canRevert() {
return true;
}
}

View File

@ -129,6 +129,11 @@ public enum StandardRenameAction implements RenameAction {
public File rename(File from, File to) throws IOException {
return FileUtilities.resolve(from, to);
}
@Override
public boolean canRevert() {
return false;
}
};
public String getDisplayName() {

View File

@ -124,6 +124,10 @@ public class ArgumentBean {
return rename || getSubtitles || check || list || mediaInfo || revert || extract || script != null;
}
public boolean isInteractive() {
return "interactive".equalsIgnoreCase(mode) && System.console() != null;
}
public boolean printVersion() {
return version;
}

View File

@ -20,12 +20,15 @@ public class ArgumentProcessor {
public int run(ArgumentBean args) {
try {
// interactive mode enables basic selection and confirmation dialogs in the CLI
CmdlineInterface cli = args.isInteractive() ? new CmdlineOperationsTextUI() : new CmdlineOperations();
if (args.script == null) {
// execute command
return runCommand(args);
return runCommand(cli, args);
} else {
// execute user script
runScript(args);
runScript(cli, args);
// script finished successfully
log.finest("Done ヾ(@⌒ー⌒@)");
@ -46,9 +49,7 @@ public class ArgumentProcessor {
return 1;
}
public int runCommand(ArgumentBean args) throws Exception {
CmdlineInterface cli = new CmdlineOperations();
public int runCommand(CmdlineInterface cli, ArgumentBean args) throws Exception {
// sanity checks
if (args.getSubtitles && args.recursive) {
throw new CmdlineException("`filebot -get-subtitles -r` has been disabled due to abuse. Please see http://bit.ly/suball for details.");
@ -103,13 +104,13 @@ public class ArgumentProcessor {
return 0;
}
public void runScript(ArgumentBean args) throws Throwable {
public void runScript(CmdlineInterface cli, ArgumentBean args) throws Throwable {
Bindings bindings = new SimpleBindings();
bindings.put(ScriptShell.SHELL_ARGV_BINDING_NAME, args.getArgumentArray());
bindings.put(ScriptShell.SHELL_ARGS_BINDING_NAME, args);
bindings.put(ScriptShell.ARGV_BINDING_NAME, args.getFiles(false));
ScriptSource source = ScriptSource.findScriptProvider(args.script);
ScriptShell shell = new ScriptShell(source.getScriptProvider(args.script), args.defines);
ScriptShell shell = new ScriptShell(source.getScriptProvider(args.script), cli, args.defines);
shell.runScript(source.accept(args.script), bindings);
}

View File

@ -601,7 +601,7 @@ public class CmdlineOperations implements CmdlineInterface {
destination = resolve(source, destination);
}
if (!destination.equals(source) && destination.exists() && renameAction != StandardRenameAction.TEST) {
if (!destination.equals(source) && destination.exists() && renameAction.canRevert()) {
if (conflictAction == ConflictAction.FAIL) {
throw new CmdlineException("File already exists: " + destination);
}
@ -632,14 +632,16 @@ public class CmdlineOperations implements CmdlineInterface {
}
} finally {
// update rename history
if (renameAction.canRevert()) {
HistorySpooler.getInstance().append(renameLog.entrySet());
}
// printer number of renamed files if any
log.fine(format("Processed %d files", renameLog.size()));
}
// write metadata into xattr if xattr is enabled
if (matches != null && renameLog.size() > 0 && renameAction != StandardRenameAction.TEST) {
if (matches != null && renameLog.size() > 0 && renameAction.canRevert()) {
for (Match<File, ?> match : matches) {
File source = match.getValue();
Object infoObject = match.getCandidate();
@ -868,7 +870,7 @@ public class CmdlineOperations implements CmdlineInterface {
return output;
}
private List<SearchResult> selectSearchResult(String query, Collection<? extends SearchResult> options, boolean alias, boolean strict) throws Exception {
protected List<SearchResult> selectSearchResult(String query, Collection<? extends SearchResult> options, boolean alias, boolean strict) throws Exception {
List<SearchResult> probableMatches = getProbableMatches(query, options, alias, strict);
if (probableMatches.isEmpty() || (strict && probableMatches.size() != 1)) {

View File

@ -0,0 +1,162 @@
package net.filebot.cli;
import static java.util.Arrays.*;
import static java.util.Collections.*;
import static net.filebot.media.MediaDetection.*;
import java.io.File;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
import com.googlecode.lanterna.TextColor;
import com.googlecode.lanterna.bundle.LanternaThemes;
import com.googlecode.lanterna.gui2.BasicWindow;
import com.googlecode.lanterna.gui2.Button;
import com.googlecode.lanterna.gui2.CheckBoxList;
import com.googlecode.lanterna.gui2.DefaultWindowManager;
import com.googlecode.lanterna.gui2.Direction;
import com.googlecode.lanterna.gui2.EmptySpace;
import com.googlecode.lanterna.gui2.GridLayout;
import com.googlecode.lanterna.gui2.LocalizedString;
import com.googlecode.lanterna.gui2.MultiWindowTextGUI;
import com.googlecode.lanterna.gui2.Panel;
import com.googlecode.lanterna.gui2.Panels;
import com.googlecode.lanterna.gui2.Separator;
import com.googlecode.lanterna.gui2.Window.Hint;
import com.googlecode.lanterna.gui2.dialogs.ListSelectDialogBuilder;
import com.googlecode.lanterna.screen.Screen;
import com.googlecode.lanterna.screen.TerminalScreen;
import com.googlecode.lanterna.terminal.DefaultTerminalFactory;
import com.googlecode.lanterna.terminal.Terminal;
import net.filebot.RenameAction;
import net.filebot.similarity.Match;
import net.filebot.web.SearchResult;
public class CmdlineOperationsTextUI extends CmdlineOperations {
public static final String DEFAULT_THEME = "businessmachine";
private Terminal terminal;
private Screen screen;
private MultiWindowTextGUI ui;
public CmdlineOperationsTextUI() throws Exception {
terminal = new DefaultTerminalFactory().createTerminal();
screen = new TerminalScreen(terminal);
ui = new MultiWindowTextGUI(screen, new DefaultWindowManager(), new EmptySpace(TextColor.ANSI.DEFAULT));
// use green matrix-style theme
ui.setTheme(LanternaThemes.getRegisteredTheme(DEFAULT_THEME));
}
public <T> T onScreen(Supplier<T> dialog) throws Exception {
try {
screen.startScreen();
return dialog.get();
} finally {
screen.stopScreen();
}
}
@Override
public List<File> renameAll(Map<File, File> renameMap, RenameAction renameAction, ConflictAction conflictAction, List<Match<File, ?>> matches) throws Exception {
// manually confirm each file mapping
Map<File, File> selection = onScreen(() -> confirmRenameMap(renameMap, renameAction, conflictAction));
return super.renameAll(selection, renameAction, conflictAction, matches);
}
@Override
protected List<SearchResult> selectSearchResult(String query, Collection<? extends SearchResult> options, boolean alias, boolean strict) throws Exception {
List<SearchResult> matches = getProbableMatches(query, options, alias, false);
// manually select option if there is more than one
if (matches.size() > 1) {
return onScreen(() -> confirmSearchResult(query, matches));
}
return matches;
}
protected List<SearchResult> confirmSearchResult(String query, List<SearchResult> options) {
ListSelectDialogBuilder<SearchResult> dialog = new ListSelectDialogBuilder<SearchResult>();
dialog.setTitle("Multiple Options");
dialog.setDescription(String.format("Select best match for \"%s\"", query));
dialog.setExtraWindowHints(singleton(Hint.CENTERED));
options.forEach(dialog::addListItem);
// show UI
SearchResult selection = dialog.build().showDialog(ui);
if (selection == null) {
return emptyList();
}
return singletonList(selection);
}
protected Map<File, File> confirmRenameMap(Map<File, File> renameMap, RenameAction renameAction, ConflictAction conflictAction) {
Map<File, File> selection = new LinkedHashMap<File, File>();
BasicWindow dialog = new BasicWindow();
dialog.setTitle(String.format("%s / %s", renameAction, conflictAction));
dialog.setHints(asList(Hint.MODAL, Hint.CENTERED));
CheckBoxList<CheckBoxListItem> checkBoxList = new CheckBoxList<CheckBoxListItem>();
int columnSize = renameMap.keySet().stream().mapToInt(f -> f.getName().length()).max().orElse(0);
String labelFormat = "%-" + columnSize + "s\t=>\t%s";
renameMap.forEach((k, v) -> {
checkBoxList.addItem(new CheckBoxListItem(String.format(labelFormat, k.getName(), v.getName()), k, v), true);
});
Button continueButton = new Button(LocalizedString.OK.toString(), () -> {
checkBoxList.getCheckedItems().forEach(it -> selection.put(it.key, it.value));
dialog.close();
});
Button cancelButton = new Button(LocalizedString.Cancel.toString(), () -> {
selection.clear();
dialog.close();
});
Panel contentPane = new Panel();
contentPane.setLayoutManager(new GridLayout(1));
contentPane.addComponent(checkBoxList.setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.BEGINNING, GridLayout.Alignment.BEGINNING, true, true, 1, 1)));
contentPane.addComponent(new Separator(Direction.HORIZONTAL).setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.FILL, GridLayout.Alignment.CENTER, true, false, 1, 1)));
contentPane.addComponent(Panels.grid(2, continueButton, cancelButton).setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.END, GridLayout.Alignment.CENTER, false, false, 1, 1)));
dialog.setComponent(contentPane);
ui.addWindowAndWait(dialog);
return selection;
}
protected static class CheckBoxListItem {
public final String label;
public final File key;
public final File value;
public CheckBoxListItem(String label, File key, File value) {
this.label = label;
this.key = key;
this.value = value;
}
@Override
public String toString() {
return label;
}
}
}

View File

@ -160,7 +160,7 @@ public class GroovyPad extends JFrame {
protected ScriptShell createScriptShell() {
try {
return new ScriptShell(s -> ScriptSource.GITHUB_STABLE.getScriptProvider(s).getScript(s), new HashMap<String, Object>());
return new ScriptShell(s -> ScriptSource.GITHUB_STABLE.getScriptProvider(s).getScript(s), new CmdlineOperations(), new HashMap<String, Object>());
} catch (Exception e) {
throw new RuntimeException(e);
}
@ -226,7 +226,7 @@ public class GroovyPad extends JFrame {
public void run() {
try {
Bindings bindings = new SimpleBindings();
bindings.put(ScriptShell.SHELL_ARGV_BINDING_NAME, Settings.getApplicationArguments().getArgumentArray());
bindings.put(ScriptShell.SHELL_ARGS_BINDING_NAME, Settings.getApplicationArguments());
bindings.put(ScriptShell.ARGV_BINDING_NAME, Settings.getApplicationArguments().getFiles(false));
result = shell.evaluate(script, bindings);

View File

@ -21,12 +21,13 @@ public class ScriptShell {
public static final String ARGV_BINDING_NAME = "args";
public static final String SHELL_BINDING_NAME = "__shell";
public static final String SHELL_ARGV_BINDING_NAME = "__args";
public static final String SHELL_CLI_BINDING_NAME = "__cli";
public static final String SHELL_ARGS_BINDING_NAME = "__args";
private final ScriptEngine engine;
private final ScriptProvider scriptProvider;
public ScriptShell(ScriptProvider scriptProvider, Map<String, ?> globals) throws ScriptException {
public ScriptShell(ScriptProvider scriptProvider, CmdlineInterface cli, Map<String, ?> globals) throws ScriptException {
this.engine = createScriptEngine();
this.scriptProvider = scriptProvider;
@ -36,6 +37,7 @@ public class ScriptShell {
// bind API objects
bindings.put(SHELL_BINDING_NAME, this);
bindings.put(SHELL_CLI_BINDING_NAME, cli);
// setup script context
engine.getContext().setBindings(bindings, ScriptContext.GLOBAL_SCOPE);

View File

@ -78,6 +78,18 @@ public abstract class ScriptShellBaseClass extends Script {
}
}
private ArgumentBean getArgumentBean() {
return (ArgumentBean) getBinding().getVariable(ScriptShell.SHELL_ARGS_BINDING_NAME);
}
private ScriptShell getShell() {
return (ScriptShell) getBinding().getVariable(ScriptShell.SHELL_BINDING_NAME);
}
private CmdlineInterface getCLI() {
return (CmdlineInterface) getBinding().getVariable(ScriptShell.SHELL_CLI_BINDING_NAME);
}
public void include(String input) throws Throwable {
try {
executeScript(input, null, null, null);
@ -109,12 +121,11 @@ public abstract class ScriptShellBaseClass extends Script {
parameters.putAll(bindings);
}
parameters.put(ScriptShell.SHELL_ARGV_BINDING_NAME, argv != null ? argv.toArray(new String[0]) : new String[0]);
parameters.put(ScriptShell.SHELL_ARGS_BINDING_NAME, new ArgumentBean(argv != null ? argv.toArray(new String[0]) : new String[0]));
parameters.put(ScriptShell.ARGV_BINDING_NAME, args != null ? new ArrayList<File>(args) : new ArrayList<File>());
// run given script
ScriptShell shell = (ScriptShell) getBinding().getVariable(ScriptShell.SHELL_BINDING_NAME);
return shell.runScript(input, parameters);
return getShell().runScript(input, parameters);
}
public Object tryQuietly(Closure<?> c) {
@ -317,8 +328,6 @@ public abstract class ScriptShellBaseClass extends Script {
action, conflict, query, filter, format, db, order, lang, output, encoding, strict, forceExtractAll
}
private static final CmdlineInterface cli = new CmdlineOperations();
public List<File> rename(Map<String, ?> parameters) throws Exception {
List<File> input = getInputFileList(parameters);
Map<Option, Object> option = getDefaultOptions(parameters);
@ -327,9 +336,9 @@ public abstract class ScriptShellBaseClass extends Script {
try {
if (input.isEmpty() && !getInputFileMap(parameters).isEmpty()) {
return cli.rename(getInputFileMap(parameters), action, asString(option.get(Option.conflict)));
return getCLI().rename(getInputFileMap(parameters), action, asString(option.get(Option.conflict)));
} else {
return cli.rename(input, action, asString(option.get(Option.conflict)), asString(option.get(Option.output)), asString(option.get(Option.format)), asString(option.get(Option.db)), asString(option.get(Option.query)), asString(option.get(Option.order)), asString(option.get(Option.filter)), asString(option.get(Option.lang)), strict);
return getCLI().rename(input, action, asString(option.get(Option.conflict)), asString(option.get(Option.output)), asString(option.get(Option.format)), asString(option.get(Option.db)), asString(option.get(Option.query)), asString(option.get(Option.order)), asString(option.get(Option.filter)), asString(option.get(Option.lang)), strict);
}
} catch (Exception e) {
printException(e);
@ -344,7 +353,7 @@ public abstract class ScriptShellBaseClass extends Script {
boolean strict = DefaultTypeTransformation.castToBoolean(option.get(Option.strict));
try {
return cli.getSubtitles(input, asString(option.get(Option.db)), asString(option.get(Option.query)), asString(option.get(Option.lang)), asString(option.get(Option.output)), asString(option.get(Option.encoding)), asString(option.get(Option.format)), strict);
return getCLI().getSubtitles(input, asString(option.get(Option.db)), asString(option.get(Option.query)), asString(option.get(Option.lang)), asString(option.get(Option.output)), asString(option.get(Option.encoding)), asString(option.get(Option.format)), strict);
} catch (Exception e) {
printException(e);
}
@ -358,7 +367,7 @@ public abstract class ScriptShellBaseClass extends Script {
boolean strict = DefaultTypeTransformation.castToBoolean(option.get(Option.strict));
try {
return cli.getMissingSubtitles(input, asString(option.get(Option.db)), asString(option.get(Option.query)), asString(option.get(Option.lang)), asString(option.get(Option.output)), asString(option.get(Option.encoding)), asString(option.get(Option.format)), strict);
return getCLI().getMissingSubtitles(input, asString(option.get(Option.db)), asString(option.get(Option.query)), asString(option.get(Option.lang)), asString(option.get(Option.output)), asString(option.get(Option.encoding)), asString(option.get(Option.format)), strict);
} catch (Exception e) {
printException(e);
}
@ -370,7 +379,7 @@ public abstract class ScriptShellBaseClass extends Script {
List<File> input = getInputFileList(parameters);
try {
return cli.check(input);
return getCLI().check(input);
} catch (Exception e) {
printException(e);
}
@ -383,7 +392,7 @@ public abstract class ScriptShellBaseClass extends Script {
Map<Option, Object> option = getDefaultOptions(parameters);
try {
return cli.compute(input, asString(option.get(Option.output)), asString(option.get(Option.encoding)));
return getCLI().compute(input, asString(option.get(Option.output)), asString(option.get(Option.encoding)));
} catch (Exception e) {
printException(e);
}
@ -398,7 +407,7 @@ public abstract class ScriptShellBaseClass extends Script {
boolean forceExtractAll = DefaultTypeTransformation.castToBoolean(option.get(Option.forceExtractAll));
try {
return cli.extract(input, asString(option.get(Option.output)), asString(option.get(Option.conflict)), filter, forceExtractAll);
return getCLI().extract(input, asString(option.get(Option.output)), asString(option.get(Option.conflict)), filter, forceExtractAll);
} catch (Exception e) {
printException(e);
}
@ -410,7 +419,7 @@ public abstract class ScriptShellBaseClass extends Script {
Map<Option, Object> option = getDefaultOptions(parameters);
try {
return cli.fetchEpisodeList(asString(option.get(Option.query)), asString(option.get(Option.format)), asString(option.get(Option.db)), asString(option.get(Option.order)), asString(option.get(Option.filter)), asString(option.get(Option.lang)));
return getCLI().fetchEpisodeList(asString(option.get(Option.query)), asString(option.get(Option.format)), asString(option.get(Option.db)), asString(option.get(Option.order)), asString(option.get(Option.filter)), asString(option.get(Option.lang)));
} catch (Exception e) {
printException(e);
}
@ -426,7 +435,7 @@ public abstract class ScriptShellBaseClass extends Script {
Map<Option, Object> option = getDefaultOptions(parameters);
try {
return cli.getMediaInfo(input, asString(option.get(Option.format)), asString(option.get(Option.filter)));
return getCLI().getMediaInfo(input, asString(option.get(Option.format)), asString(option.get(Option.filter)));
} catch (Exception e) {
printException(e);
}
@ -484,14 +493,6 @@ public abstract class ScriptShellBaseClass extends Script {
return files;
}
private ArgumentBean getArgumentBean() {
try {
return new ArgumentBean((String[]) getBinding().getVariable(ScriptShell.SHELL_ARGV_BINDING_NAME));
} catch (Exception e) {
throw new IllegalStateException(e.getMessage(), e);
}
}
private Map<Option, Object> getDefaultOptions(Map<String, ?> parameters) throws Exception {
Map<Option, Object> options = new EnumMap<Option, Object>(Option.class);