Add jline-based console with colors and tab-completion

This commit is contained in:
Minecrell 2015-11-21 09:35:27 +01:00
parent 1246f1a791
commit e24c38bbb0
12 changed files with 541 additions and 5 deletions

View file

@ -8,6 +8,11 @@
{ {
"name": "net.minecraft:launchwrapper:1.12" "name": "net.minecraft:launchwrapper:1.12"
}, },
{
"name": "jline:jline:2.13",
"children": ["sources"],
"url" : "http://repo.maven.apache.org/maven2"
},
{ {
"name": "com.google.code.findbugs:jsr305:1.3.9", "name": "com.google.code.findbugs:jsr305:1.3.9",
"children": ["sources"], "children": ["sources"],

View file

@ -39,6 +39,13 @@
"name": "org.ow2.asm:asm-all:5.0.3", "name": "org.ow2.asm:asm-all:5.0.3",
"serverreq":true "serverreq":true
}, },
{
"name": "jline:jline:2.13",
"url" : "http://files.minecraftforge.net/maven/",
"checksums" : [ "TODO", "TODO" ],
"serverreq":true,
"clientreq":false
},
{ {
"name": "com.typesafe.akka:akka-actor_2.11:2.3.3", "name": "com.typesafe.akka:akka-actor_2.11:2.3.3",
"url" : "http://files.minecraftforge.net/maven/", "url" : "http://files.minecraftforge.net/maven/",

View file

@ -8,7 +8,15 @@
private static final String __OBFID = "CL_00001784"; private static final String __OBFID = "CL_00001784";
public DedicatedServer(File p_i1508_1_) public DedicatedServer(File p_i1508_1_)
@@ -113,6 +114,8 @@ @@ -88,6 +89,7 @@
private static final String __OBFID = "CL_00001786";
public void run()
{
+ if (net.minecraftforge.server.console.TerminalHandler.handleCommands(DedicatedServer.this)) return;
BufferedReader bufferedreader = new BufferedReader(new InputStreamReader(System.in));
String s4;
@@ -113,6 +115,8 @@
field_155771_h.warn("To start the server with more ram, launch it as \"java -Xmx1024M -Xms1024M -jar minecraft_server.jar\""); field_155771_h.warn("To start the server with more ram, launch it as \"java -Xmx1024M -Xms1024M -jar minecraft_server.jar\"");
} }
@ -17,7 +25,7 @@
field_155771_h.info("Loading properties"); field_155771_h.info("Loading properties");
this.field_71340_o = new PropertyManager(new File("server.properties")); this.field_71340_o = new PropertyManager(new File("server.properties"));
this.field_154332_n = new ServerEula(new File("eula.txt")); this.field_154332_n = new ServerEula(new File("eula.txt"));
@@ -204,6 +207,7 @@ @@ -204,6 +208,7 @@
} }
else else
{ {
@ -25,7 +33,7 @@
this.func_152361_a(new DedicatedPlayerList(this)); this.func_152361_a(new DedicatedPlayerList(this));
long j = System.nanoTime(); long j = System.nanoTime();
@@ -250,6 +254,7 @@ @@ -250,6 +255,7 @@
this.func_71191_d((this.func_71207_Z() + 8) / 16 * 16); this.func_71191_d((this.func_71207_Z() + 8) / 16 * 16);
this.func_71191_d(MathHelper.func_76125_a(this.func_71207_Z(), 64, 256)); this.func_71191_d(MathHelper.func_76125_a(this.func_71207_Z(), 64, 256));
this.field_71340_o.func_73667_a("max-build-height", Integer.valueOf(this.func_71207_Z())); this.field_71340_o.func_73667_a("max-build-height", Integer.valueOf(this.func_71207_Z()));
@ -33,7 +41,7 @@
field_155771_h.info("Preparing level \"" + this.func_71270_I() + "\""); field_155771_h.info("Preparing level \"" + this.func_71270_I() + "\"");
this.func_71247_a(this.func_71270_I(), this.func_71270_I(), k, worldtype, s2); this.func_71247_a(this.func_71270_I(), this.func_71270_I(), k, worldtype, s2);
long i1 = System.nanoTime() - j; long i1 = System.nanoTime() - j;
@@ -278,7 +283,7 @@ @@ -278,10 +284,11 @@
thread1.start(); thread1.start();
} }
@ -42,3 +50,7 @@
} }
} }
} }
+ public void func_145747_a(net.minecraft.util.IChatComponent message) { field_155771_h.info(message.func_150254_d()); }
public void func_71235_a(WorldSettings.GameType p_71235_1_)
{

View file

@ -0,0 +1,10 @@
--- ../src-base/minecraft/net/minecraft/util/ChatComponentStyle.java
+++ ../src-work/minecraft/net/minecraft/util/ChatComponentStyle.java
@@ -81,7 +81,6 @@
return stringbuilder.toString();
}
- @SideOnly(Side.CLIENT)
public final String func_150254_d()
{
StringBuilder stringbuilder = new StringBuilder();

View file

@ -0,0 +1,10 @@
--- ../src-base/minecraft/net/minecraft/util/ChatStyle.java
+++ ../src-work/minecraft/net/minecraft/util/ChatStyle.java
@@ -230,7 +230,6 @@
return this;
}
- @SideOnly(Side.CLIENT)
public String func_150218_j()
{
if (this.func_150229_g())

View file

@ -0,0 +1,10 @@
--- ../src-base/minecraft/net/minecraft/util/IChatComponent.java
+++ ../src-work/minecraft/net/minecraft/util/IChatComponent.java
@@ -32,7 +32,6 @@
String func_150260_c();
- @SideOnly(Side.CLIENT)
String func_150254_d();
List func_150253_a();

View file

@ -1,9 +1,28 @@
package net.minecraftforge.fml.common.launcher; package net.minecraftforge.fml.common.launcher;
import java.io.File;
import java.util.List;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.LoggerContext;
import net.minecraft.launchwrapper.LaunchClassLoader; import net.minecraft.launchwrapper.LaunchClassLoader;
import net.minecraftforge.fml.relauncher.FMLLaunchHandler; import net.minecraftforge.fml.relauncher.FMLLaunchHandler;
public class FMLServerTweaker extends FMLTweaker { public class FMLServerTweaker extends FMLTweaker {
@Override
public void acceptOptions(List<String> args, File gameDir, File assetsDir, String profile)
{
super.acceptOptions(args, gameDir, assetsDir, profile);
if (System.getProperty("log4j.configurationFile") == null)
{
System.setProperty("log4j.configurationFile", "log4j2_server.xml");
((LoggerContext) LogManager.getContext(false)).reconfigure();
}
}
@Override @Override
public String getLaunchTarget() public String getLaunchTarget()
{ {
@ -16,6 +35,11 @@ public class FMLServerTweaker extends FMLTweaker {
// The log4j2 queue is excluded so it is correctly visible from the obfuscated // The log4j2 queue is excluded so it is correctly visible from the obfuscated
// and deobfuscated parts of the code. Without, the UI won't show anything // and deobfuscated parts of the code. Without, the UI won't show anything
classLoader.addClassLoaderExclusion("com.mojang.util.QueueLogAppender"); classLoader.addClassLoaderExclusion("com.mojang.util.QueueLogAppender");
classLoader.addClassLoaderExclusion("jline.");
classLoader.addClassLoaderExclusion("org.fusesource.");
classLoader.addClassLoaderExclusion("net.minecraftforge.server.console.TerminalConsoleAppender");
FMLLaunchHandler.configureForServerLaunch(classLoader, this); FMLLaunchHandler.configureForServerLaunch(classLoader, this);
FMLLaunchHandler.appendCoreMods(); FMLLaunchHandler.appendCoreMods();
} }

View file

@ -0,0 +1,96 @@
package net.minecraftforge.server.console;
import static com.google.common.base.Preconditions.checkNotNull;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import jline.console.completer.Completer;
import net.minecraft.server.dedicated.DedicatedServer;
public final class ConsoleCommandCompleter implements Completer
{
private static final Logger logger = LogManager.getLogger();
private final DedicatedServer server;
public ConsoleCommandCompleter(DedicatedServer server)
{
this.server = checkNotNull(server, "server");
}
@Override
public int complete(String buffer, int cursor, List<CharSequence> candidates)
{
int len = buffer.length();
buffer = buffer.trim();
if (buffer.isEmpty())
{
return cursor;
}
boolean prefix;
if (buffer.charAt(0) != '/')
{
buffer = '/' + buffer;
prefix = false;
}
else
{
prefix = true;
}
final String input = buffer;
@SuppressWarnings("unchecked")
Future<List<String>> tabComplete = this.server.callFromMainThread(new Callable<List<String>>() {
@Override
public List<String> call() throws Exception
{
return ConsoleCommandCompleter.this.server.getTabCompletions(ConsoleCommandCompleter.this.server, input,
ConsoleCommandCompleter.this.server.getPosition());
}
});
try
{
List<String> completions = tabComplete.get();
if (prefix)
{
candidates.addAll(completions);
}
else
{
for (String completion : completions)
{
candidates.add(completion.charAt(0) == '/' ? completion.substring(1) : completion);
}
}
int pos = buffer.lastIndexOf(' ');
if (pos == -1)
{
return cursor - len;
}
else
{
return cursor - len + pos;
}
}
catch (InterruptedException e)
{
Thread.currentThread().interrupt();
}
catch (ExecutionException e)
{
logger.error("Failed to tab complete", e);
}
return cursor;
}
}

View file

@ -0,0 +1,63 @@
package net.minecraftforge.server.console;
import java.util.Map;
import java.util.regex.Pattern;
import org.fusesource.jansi.Ansi;
import com.google.common.base.Function;
import com.google.common.collect.ImmutableMap;
import net.minecraft.util.EnumChatFormatting;
public final class ConsoleFormatter implements Function<String, String>
{
public ConsoleFormatter()
{
}
private static final String RESET = Ansi.ansi().reset().toString();
private static final ImmutableMap<Pattern, String> REPLACEMENTS = ImmutableMap.<Pattern, String> builder()
.put(compile(EnumChatFormatting.BLACK), Ansi.ansi().reset().fg(Ansi.Color.BLACK).boldOff().toString())
.put(compile(EnumChatFormatting.DARK_BLUE), Ansi.ansi().reset().fg(Ansi.Color.BLUE).boldOff().toString())
.put(compile(EnumChatFormatting.DARK_GREEN), Ansi.ansi().reset().fg(Ansi.Color.GREEN).boldOff().toString())
.put(compile(EnumChatFormatting.DARK_AQUA), Ansi.ansi().reset().fg(Ansi.Color.CYAN).boldOff().toString())
.put(compile(EnumChatFormatting.DARK_RED), Ansi.ansi().reset().fg(Ansi.Color.RED).boldOff().toString())
.put(compile(EnumChatFormatting.DARK_PURPLE), Ansi.ansi().reset().fg(Ansi.Color.MAGENTA).boldOff().toString())
.put(compile(EnumChatFormatting.GOLD), Ansi.ansi().reset().fg(Ansi.Color.YELLOW).boldOff().toString())
.put(compile(EnumChatFormatting.GRAY), Ansi.ansi().reset().fg(Ansi.Color.WHITE).boldOff().toString())
.put(compile(EnumChatFormatting.DARK_GRAY), Ansi.ansi().reset().fg(Ansi.Color.BLACK).bold().toString())
.put(compile(EnumChatFormatting.BLUE), Ansi.ansi().reset().fg(Ansi.Color.BLUE).bold().toString())
.put(compile(EnumChatFormatting.GREEN), Ansi.ansi().reset().fg(Ansi.Color.GREEN).bold().toString())
.put(compile(EnumChatFormatting.AQUA), Ansi.ansi().reset().fg(Ansi.Color.CYAN).bold().toString())
.put(compile(EnumChatFormatting.RED), Ansi.ansi().reset().fg(Ansi.Color.RED).bold().toString())
.put(compile(EnumChatFormatting.LIGHT_PURPLE), Ansi.ansi().reset().fg(Ansi.Color.MAGENTA).bold().toString())
.put(compile(EnumChatFormatting.YELLOW), Ansi.ansi().reset().fg(Ansi.Color.YELLOW).bold().toString())
.put(compile(EnumChatFormatting.WHITE), Ansi.ansi().reset().fg(Ansi.Color.WHITE).bold().toString())
.put(compile(EnumChatFormatting.OBFUSCATED), Ansi.ansi().a(Ansi.Attribute.BLINK_SLOW).toString())
.put(compile(EnumChatFormatting.BOLD), Ansi.ansi().a(Ansi.Attribute.UNDERLINE_DOUBLE).toString())
.put(compile(EnumChatFormatting.STRIKETHROUGH), Ansi.ansi().a(Ansi.Attribute.STRIKETHROUGH_ON).toString())
.put(compile(EnumChatFormatting.UNDERLINE), Ansi.ansi().a(Ansi.Attribute.UNDERLINE).toString())
.put(compile(EnumChatFormatting.ITALIC), Ansi.ansi().a(Ansi.Attribute.ITALIC).toString())
.put(compile(EnumChatFormatting.RESET), RESET)
.build();
private static Pattern compile(EnumChatFormatting formatting)
{
return Pattern.compile(formatting.toString(), Pattern.LITERAL | Pattern.CASE_INSENSITIVE);
}
@Override
public String apply(String text)
{
for (Map.Entry<Pattern, String> entry : REPLACEMENTS.entrySet())
{
text = entry.getKey().matcher(text).replaceAll(entry.getValue());
}
return text + RESET;
}
}

View file

@ -0,0 +1,168 @@
package net.minecraftforge.server.console;
import static jline.TerminalFactory.OFF;
import static jline.console.ConsoleReader.RESET_LINE;
import static org.apache.logging.log4j.core.helpers.Booleans.parseBoolean;
import java.io.IOException;
import java.io.PrintStream;
import java.io.Serializable;
import java.io.Writer;
import org.apache.logging.log4j.core.Filter;
import org.apache.logging.log4j.core.Layout;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.appender.AbstractAppender;
import org.apache.logging.log4j.core.config.plugins.Plugin;
import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
import org.apache.logging.log4j.core.config.plugins.PluginElement;
import org.apache.logging.log4j.core.config.plugins.PluginFactory;
import org.apache.logging.log4j.core.layout.PatternLayout;
import org.apache.logging.log4j.util.PropertiesUtil;
import org.fusesource.jansi.AnsiConsole;
import com.google.common.base.Function;
import com.google.common.base.Functions;
import jline.TerminalFactory;
import jline.console.ConsoleReader;
@Plugin(name = "TerminalConsole", category = "Core", elementType = "appender", printObject = true)
public class TerminalConsoleAppender extends AbstractAppender
{
private static final boolean ENABLE_JLINE = PropertiesUtil.getProperties().getBooleanProperty("jline.enable", true);
private static final PrintStream out = System.out;
private static boolean initialized;
private static ConsoleReader reader;
public static ConsoleReader getReader()
{
return reader;
}
private static Function<String, String> formatter = Functions.identity();
public static void setFormatter(Function<String, String> format)
{
formatter = format != null ? format : Functions.<String> identity();
}
protected TerminalConsoleAppender(String name, Filter filter, Layout<? extends Serializable> layout, boolean ignoreExceptions)
{
super(name, filter, layout, ignoreExceptions);
}
@PluginFactory
public static TerminalConsoleAppender createAppender(@PluginAttribute("name") String name, @PluginElement("Filters") Filter filter,
@PluginElement("Layout") Layout<? extends Serializable> layout, @PluginAttribute("ignoreExceptions") String ignore)
{
if (name == null)
{
LOGGER.error("No name provided for TerminalConsoleAppender");
return null;
}
if (layout == null)
{
layout = PatternLayout.createLayout(null, null, null, null, null);
}
boolean ignoreExceptions = parseBoolean(ignore, true);
// This is handled by jline
System.setProperty("log4j.skipJansi", "true");
return new TerminalConsoleAppender(name, filter, layout, ignoreExceptions);
}
@Override
public void start()
{
// Initialize the reader if that hasn't happened yet
if (!initialized && reader == null)
{
initialized = true;
if (ENABLE_JLINE)
{
final boolean hasConsole = System.console() != null;
if (hasConsole)
{
try
{
AnsiConsole.systemInstall();
reader = new ConsoleReader();
reader.setExpandEvents(false);
}
catch (Exception e)
{
LOGGER.warn("Failed to initialize terminal. Falling back to default.", e);
}
}
if (reader == null)
{
// Eclipse doesn't support colors and characters like \r so enabling jline2 on it will
// just cause a lot of issues with empty lines and weird characters.
// Enable jline2 only on IntelliJ IDEA to prevent that.
// Also see: https://bugs.eclipse.org/bugs/show_bug.cgi?id=76936
if (hasConsole || System.getProperty("java.class.path").contains("idea_rt.jar"))
{
// Disable advanced jline features
TerminalFactory.configure(OFF);
TerminalFactory.reset();
try
{
reader = new ConsoleReader();
reader.setExpandEvents(false);
}
catch (Exception e)
{
LOGGER.warn("Failed to initialize fallback terminal. Falling back to standard output console.", e);
}
}
else
{
LOGGER.warn("Disabling terminal, you're running in an unsupported environment.");
}
}
}
}
super.start();
}
@Override
public void append(LogEvent event)
{
if (reader != null)
{
try
{
Writer out = reader.getOutput();
out.write(RESET_LINE);
out.write(formatEvent(event));
reader.drawLine();
reader.flush();
}
catch (IOException ignored)
{
}
}
else
{
out.print(formatEvent(event));
}
}
protected String formatEvent(LogEvent event)
{
return formatter.apply(getLayout().toSerializable(event).toString());
}
}

View file

@ -0,0 +1,70 @@
package net.minecraftforge.server.console;
import java.io.IOException;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import com.google.common.base.Function;
import jline.console.ConsoleReader;
import net.minecraft.server.dedicated.DedicatedServer;
import net.minecraft.util.EnumChatFormatting;
public final class TerminalHandler
{
private static final Logger logger = LogManager.getLogger();
private TerminalHandler()
{
}
public static boolean handleCommands(DedicatedServer server)
{
final ConsoleReader reader = TerminalConsoleAppender.getReader();
if (reader != null)
{
TerminalConsoleAppender.setFormatter(new ConsoleFormatter());
reader.addCompleter(new ConsoleCommandCompleter(server));
String line;
while (!server.isServerStopped() && server.isServerRunning())
{
try
{
line = reader.readLine("> ");
if (line != null)
{
line = line.trim();
if (!line.isEmpty())
{
server.addPendingCommand(line, server);
}
}
}
catch (IOException e)
{
logger.error("Exception handling console input", e);
}
}
return true;
}
else
{
TerminalConsoleAppender.setFormatter(new Function<String, String>() {
@Override
public String apply(String text)
{
return EnumChatFormatting.getTextWithoutFormattingCodes(text);
}
});
return false;
}
}
}

View file

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN" packages="com.mojang.util,net.minecraftforge.server.console">
<Appenders>
<TerminalConsole name="FmlConsole">
<PatternLayout pattern="[%d{HH:mm:ss}] [%t/%level] [%logger]: %msg%n" />
</TerminalConsole>
<TerminalConsole name="Console">
<PatternLayout pattern="[%d{HH:mm:ss}] [%t/%level]: %msg%n" />
</TerminalConsole>
<!-- Keep a console appender open so log4j2 doesn't close our main out stream if we redirect System.out to the logger -->
<Console name="SysOut" target="SYSTEM_OUT"/>
<Queue name="ServerGuiConsole">
<PatternLayout pattern="[%d{HH:mm:ss}] [%t/%level] [%logger]: %replace{%msg}{(?i)\u00A7[0-9A-FK-OR]}{}%n" />
</Queue>
<RollingRandomAccessFile name="File" fileName="logs/latest.log" filePattern="logs/%d{yyyy-MM-dd}-%i.log.gz">
<PatternLayout pattern="[%d{HH:mm:ss}] [%t/%level]: %msg%n" />
<Policies>
<TimeBasedTriggeringPolicy />
<OnStartupTriggeringPolicy />
</Policies>
</RollingRandomAccessFile>
<Routing name="FmlFile">
<Routes pattern="$${ctx:side}">
<Route>
<RollingRandomAccessFile name="FmlFile" fileName="logs/fml-${ctx:side}-latest.log" filePattern="logs/fml-${ctx:side}-%i.log">
<PatternLayout pattern="[%d{HH:mm:ss}] [%t/%level] [%logger/%X{mod}]: %replace{%msg}{(?i)\u00A7[0-9A-FK-OR]}{}%n" />
<DefaultRolloverStrategy max="3" fileIndex="max" />
<Policies>
<OnStartupTriggeringPolicy />
</Policies>
</RollingRandomAccessFile>
</Route>
<Route key="$${ctx:side}">
<RandomAccessFile name="FmlFile" fileName="logs/fml-junk-earlystartup.log" >
<PatternLayout pattern="[%d{HH:mm:ss}] [%t/%level] [%logger]: %replace{%msg}{(?i)\u00A7[0-9A-FK-OR]}{}%n" />
</RandomAccessFile>
</Route>
</Routes>
</Routing>
</Appenders>
<Loggers>
<Logger level="info" name="com.mojang" additivity="false">
<AppenderRef ref="Console" level="INFO" />
<AppenderRef ref="File" />
<AppenderRef ref="ServerGuiConsole" level="INFO" />
</Logger>
<Logger level="info" name="net.minecraft" additivity="false">
<filters>
<MarkerFilter marker="NETWORK_PACKETS" onMatch="DENY" onMismatch="NEUTRAL" />
</filters>
<AppenderRef ref="Console" level="INFO" />
<AppenderRef ref="File" />
<AppenderRef ref="ServerGuiConsole" level="INFO" />
</Logger>
<Root level="all">
<AppenderRef ref="FmlConsole" level="INFO" />
<AppenderRef ref="ServerGuiConsole" level="INFO" />
<AppenderRef ref="FmlFile"/>
</Root>
</Loggers>
</Configuration>