diff --git a/frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/ConsolePureSettings.kt b/frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/ConsolePureSettings.kt new file mode 100644 index 000000000..5a1fdec47 --- /dev/null +++ b/frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/ConsolePureSettings.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2019-2020 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AFFERO GENERAL PUBLIC LICENSE version 3 license that can be found via the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ +/* + * @author Karlatemp + */ + +package net.mamoe.mirai.console.pure + +@Retention(AnnotationRetention.BINARY) +@RequiresOptIn(level = RequiresOptIn.Level.WARNING) +@Target( + AnnotationTarget.CLASS, + AnnotationTarget.TYPEALIAS, + AnnotationTarget.FUNCTION, + AnnotationTarget.PROPERTY, + AnnotationTarget.FIELD, + AnnotationTarget.CONSTRUCTOR +) +@MustBeDocumented +annotation class ConsolePureExperimentalApi + +@ConsolePureExperimentalApi +public object ConsolePureSettings { + @JvmField + var setupAnsi: Boolean = System.getProperty("os.name") + .toLowerCase() + .contains("windows") // Just for Windows + + @JvmField + var noConsole: Boolean = false + + @JvmField + var noAnsi: Boolean = false + + @JvmField + var noConsoleSafeReading: Boolean = false + + @JvmField + var noConsoleReadingReplacement: String = "" +} \ No newline at end of file diff --git a/frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/ConsoleThread.kt b/frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/ConsoleThread.kt index ef82f3c65..276c6ed18 100644 --- a/frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/ConsoleThread.kt +++ b/frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/ConsoleThread.kt @@ -27,8 +27,10 @@ import org.jline.reader.UserInterruptException val consoleLogger by lazy { DefaultLogger("console") } -@OptIn(ConsoleInternalApi::class) +@OptIn(ConsoleInternalApi::class, ConsolePureExperimentalApi::class) internal fun startupConsoleThread() { + if (ConsolePureSettings.noConsole) return + MiraiConsole.launch(CoroutineName("Input")) { while (true) { try { diff --git a/frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/MiraiConsoleImplementationPure.kt b/frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/MiraiConsoleImplementationPure.kt index 14ec86af7..471c02e7f 100644 --- a/frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/MiraiConsoleImplementationPure.kt +++ b/frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/MiraiConsoleImplementationPure.kt @@ -17,7 +17,7 @@ "INVISIBLE_ABSTRACT_MEMBER_FROM_SUPER_WARNING", "EXPOSED_SUPER_CLASS" ) -@file:OptIn(ConsoleInternalApi::class, ConsoleFrontEndImplementation::class) +@file:OptIn(ConsoleInternalApi::class, ConsoleFrontEndImplementation::class, ConsolePureExperimentalApi::class) package net.mamoe.mirai.console.pure @@ -34,6 +34,8 @@ import net.mamoe.mirai.console.plugin.jvm.JvmPluginLoader import net.mamoe.mirai.console.plugin.loader.PluginLoader import net.mamoe.mirai.console.pure.ConsoleInputImpl.requestInput import net.mamoe.mirai.console.util.ConsoleExperimentalApi +import net.mamoe.mirai.console.pure.noconsole.AllEmptyLineReader +import net.mamoe.mirai.console.pure.noconsole.NoConsole import net.mamoe.mirai.console.util.ConsoleInput import net.mamoe.mirai.console.util.ConsoleInternalApi import net.mamoe.mirai.console.util.NamedSupervisorJob @@ -115,6 +117,8 @@ private object ConsoleInputImpl : ConsoleInput { } val lineReader: LineReader by lazy { + if (ConsolePureSettings.noConsole) return@lazy AllEmptyLineReader + LineReaderBuilder.builder() .terminal(terminal) .completer(NullCompleter()) @@ -122,6 +126,8 @@ val lineReader: LineReader by lazy { } val terminal: Terminal = run { + if (ConsolePureSettings.noConsole) return@run NoConsole + val dumb = System.getProperty("java.class.path") .contains("idea_rt.jar") || System.getProperty("mirai.idea") !== null || System.getenv("mirai.idea") !== null diff --git a/frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/MiraiConsolePureLoader.kt b/frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/MiraiConsolePureLoader.kt index e1bc7d3f2..0fa6abe1f 100644 --- a/frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/MiraiConsolePureLoader.kt +++ b/frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/MiraiConsolePureLoader.kt @@ -15,7 +15,7 @@ "INVISIBLE_GETTER", "INVISIBLE_ABSTRACT_MEMBER_FROM_SUPER", ) -@file:OptIn(ConsoleInternalApi::class) +@file:OptIn(ConsoleInternalApi::class, ConsolePureExperimentalApi::class) package net.mamoe.mirai.console.pure @@ -26,6 +26,7 @@ import net.mamoe.mirai.console.MiraiConsole import net.mamoe.mirai.console.MiraiConsoleImplementation import net.mamoe.mirai.console.MiraiConsoleImplementation.Companion.start import net.mamoe.mirai.console.data.AutoSavePluginDataHolder +import net.mamoe.mirai.console.pure.noconsole.SystemOutputPrintStream import net.mamoe.mirai.console.util.ConsoleExperimentalApi import net.mamoe.mirai.console.util.ConsoleInternalApi import net.mamoe.mirai.console.util.CoroutineScopeUtils.childScope @@ -33,6 +34,7 @@ import net.mamoe.mirai.message.data.Message import net.mamoe.mirai.utils.DefaultLogger import net.mamoe.mirai.utils.minutesToMillis import java.io.PrintStream +import kotlin.system.exitProcess /** * mirai-console-pure CLI 入口点 @@ -40,6 +42,7 @@ import java.io.PrintStream object MiraiConsolePureLoader { @JvmStatic fun main(args: Array) { + parse(args, exitProcess = true) startAsDaemon() try { runBlocking { @@ -50,6 +53,97 @@ object MiraiConsolePureLoader { } } + @ConsolePureExperimentalApi + fun printHelpMessage() { + val help = listOf( + "" to "Mirai-Console[Pure FrontEnd] v" + kotlin.runCatching { + net.mamoe.mirai.console.internal.MiraiConsoleBuildConstants.version + }.getOrElse { "" }, + "" to "", + "--help" to "显示此帮助", + "" to "", + "--no-console" to "使用无终端操作环境", + "--dont-setup-terminal-ansi" to + "[NoConsole] [Windows Only] 不进行ansi console初始化工作", + "--no-ansi" to "[NoConsole] 禁用 ansi", + "--safe-reading" to + "[NoConsole] 如果启动此选项, console在获取用户输入的时候会获得一个安全的替换符\n" + + " 如果不启动, 将会直接 error", + "--reading-replacement " to + "[NoConsole] Console尝试读取命令的替换符, 默认是空字符串\n" + + " 使用此选项会自动开启 --safe-reading", + ) + val prefixPlaceholder = String(CharArray( + help.maxOfOrNull { it.first.length }!! + 3 + ) { ' ' }) + + fun printOption(optionName: String, value: String) { + if (optionName == "") { + println(value) + return + } + print(optionName) + print(prefixPlaceholder.substring(optionName.length)) + val lines = value.split('\n').iterator() + if (lines.hasNext()) println(lines.next()) + lines.forEach { line -> + print(prefixPlaceholder) + println(line) + } + } + help.forEach { (optionName, value) -> + printOption(optionName, value) + } + } + + @ConsolePureExperimentalApi + fun parse(args: Array, exitProcess: Boolean = false) { + val iterator = args.iterator() + while (iterator.hasNext()) { + when (val option = iterator.next()) { + "--help" -> { + printHelpMessage() + if (exitProcess) exitProcess(0) + return + } + "--no-console" -> { + ConsolePureSettings.noConsole = true + } + "--dont-setup-terminal-ansi" -> { + ConsolePureSettings.setupAnsi = false + } + "--no-ansi" -> { + ConsolePureSettings.noAnsi = true + ConsolePureSettings.setupAnsi = false + } + "--reading-replacement" -> { + ConsolePureSettings.noConsoleSafeReading = true + if (iterator.hasNext()) { + ConsolePureSettings.noConsoleReadingReplacement = iterator.next() + } else { + println("Bad option `--reading-replacement`") + println("Usage: --reading-replacement ") + if (exitProcess) + exitProcess(1) + return + } + } + "--safe-reading" -> { + ConsolePureSettings.noConsoleSafeReading = true + } + else -> { + println("Unknown option `$option`") + printHelpMessage() + if (exitProcess) + exitProcess(1) + return + } + } + } + if (ConsolePureSettings.noConsole) + SystemOutputPrintStream // Setup Output Channel + } + @Suppress("MemberVisibilityCanBePrivate") @ConsoleExperimentalApi fun startAsDaemon(instance: MiraiConsoleImplementationPure = MiraiConsoleImplementationPure()) { diff --git a/frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/noconsole/NoConsole.kt b/frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/noconsole/NoConsole.kt new file mode 100644 index 000000000..d04a69470 --- /dev/null +++ b/frontend/mirai-console-pure/src/main/kotlin/net/mamoe/mirai/console/pure/noconsole/NoConsole.kt @@ -0,0 +1,187 @@ +/* + * Copyright 2019-2020 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AFFERO GENERAL PUBLIC LICENSE version 3 license that can be found via the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + +/* + * @author Karlatemp + */ +@file:OptIn(ConsolePureExperimentalApi::class) + +package net.mamoe.mirai.console.pure.noconsole + +import net.mamoe.mirai.console.pure.ConsolePureExperimentalApi +import net.mamoe.mirai.console.pure.ConsolePureSettings +import org.jline.keymap.KeyMap +import org.jline.reader.* +import org.jline.terminal.Attributes +import org.jline.terminal.MouseEvent +import org.jline.terminal.Size +import org.jline.terminal.Terminal +import org.jline.terminal.impl.AbstractTerminal +import org.jline.utils.AttributedString +import org.jline.utils.NonBlockingReader +import java.io.File +import java.io.InputStream +import java.io.OutputStream +import java.io.PrintWriter + +private const val LN_INT = '\n'.toInt() +private const val LN_BYTE = '\n'.toByte() + +internal object NoConsoleNonBlockingReader : NonBlockingReader() { + override fun read(timeout: Long, isPeek: Boolean): Int { + return LN_INT + } + + override fun close() {} + + override fun readBuffered(b: CharArray?): Int { + return 0 + } +} + +internal object AllNextLineInputStream : InputStream() { + override fun read(): Int = LN_INT + + override fun available(): Int = 1 + + override fun read(b: ByteArray, off: Int, len: Int): Int { + for (i in off until (off + len)) { + b[i] = LN_BYTE + } + return len + } + + override fun close() {} +} + +internal object AllIgnoredOutputStream : OutputStream() { + override fun close() {} + override fun write(b: ByteArray, off: Int, len: Int) {} + override fun write(b: ByteArray) {} + override fun write(b: Int) {} + override fun flush() {} +} + +internal val SystemOutputPrintStream by lazy { + @OptIn(ConsolePureExperimentalApi::class) + if (ConsolePureSettings.setupAnsi) { + org.fusesource.jansi.AnsiConsole.systemInstall() + } + System.out +} +private val ANSI_REGEX = """\u001b\[[0-9a-zA-Z;]*?m""".toRegex() + +internal object AllEmptyLineReader : LineReader { + + override fun printAbove(str: String?) { + if (str == null) return + @OptIn(ConsolePureExperimentalApi::class) + if (ConsolePureSettings.noAnsi) { + SystemOutputPrintStream.println(ANSI_REGEX.replace(str, "")) + } else SystemOutputPrintStream.println(str) + } + + @OptIn(ConsolePureExperimentalApi::class) + override fun readLine(): String = + if (ConsolePureSettings.noConsoleSafeReading) ConsolePureSettings.noConsoleReadingReplacement + else error("Unsupported Reading line when console front-end closed.") + + // region + private fun ignored(): T = error("Ignored") + override fun readLine(mask: Char?): String = readLine() + override fun readLine(prompt: String?): String = readLine() + override fun readLine(prompt: String?, mask: Char?): String = readLine() + override fun readLine(prompt: String?, mask: Char?, buffer: String?): String = readLine() + override fun readLine(prompt: String?, rightPrompt: String?, mask: Char?, buffer: String?): String = readLine() + override fun readLine( + prompt: String?, + rightPrompt: String?, + maskingCallback: MaskingCallback?, + buffer: String? + ): String = readLine() + + override fun printAbove(str: AttributedString?) { + str?.let { printAbove(it.toAnsi()) } + } + + override fun defaultKeyMaps(): MutableMap> = ignored() + override fun isReading(): Boolean = false + override fun variable(name: String?, value: Any?) = this + override fun option(option: LineReader.Option?, value: Boolean) = this + override fun callWidget(name: String?) {} + override fun getVariables(): MutableMap = ignored() + override fun getVariable(name: String?): Any = ignored() + override fun setVariable(name: String?, value: Any?) {} + override fun isSet(option: LineReader.Option?): Boolean = ignored() + override fun setOpt(option: LineReader.Option?) {} + override fun unsetOpt(option: LineReader.Option?) {} + override fun getTerminal(): Terminal = NoConsole + override fun getWidgets(): MutableMap = ignored() + override fun getBuiltinWidgets(): MutableMap = ignored() + override fun getBuffer(): Buffer = ignored() + override fun getAppName(): String = "Mirai Console" + override fun runMacro(macro: String?) {} + override fun readMouseEvent(): MouseEvent = ignored() + override fun getHistory(): History = ignored() + override fun getParser(): Parser = ignored() + override fun getHighlighter(): Highlighter = ignored() + override fun getExpander(): Expander = ignored() + override fun getKeyMaps(): MutableMap> = ignored() + override fun getKeyMap(): String = ignored() + override fun setKeyMap(name: String?): Boolean = ignored() + override fun getKeys(): KeyMap = ignored() + override fun getParsedLine(): ParsedLine = ignored() + override fun getSearchTerm(): String = ignored() + override fun getRegionActive(): LineReader.RegionType = ignored() + override fun getRegionMark(): Int = ignored() + override fun addCommandsInBuffer(commands: MutableCollection?) {} + override fun editAndAddInBuffer(file: File?) {} + override fun getLastBinding(): String = ignored() + override fun getTailTip(): String = ignored() + override fun setTailTip(tailTip: String?) {} + override fun setAutosuggestion(type: LineReader.SuggestionType?) {} + override fun getAutosuggestion(): LineReader.SuggestionType = ignored() + // endregion +} + +internal object NoConsole : AbstractTerminal( + "No Console", "No Console" +) { + override fun reader(): NonBlockingReader = NoConsoleNonBlockingReader + + private val AllIgnoredPrintWriter = object : PrintWriter(AllIgnoredOutputStream) { + override fun close() {} + override fun flush() {} + } + + // We don't need it. Mirai-Console using LineReader to print messages. + override fun writer(): PrintWriter = AllIgnoredPrintWriter + + override fun input(): InputStream = AllNextLineInputStream + + override fun output(): OutputStream = AllIgnoredOutputStream + + private val attributes0 = Attributes() + override fun getAttributes(): Attributes { + return Attributes(attributes0) + } + + override fun setAttributes(attr: Attributes?) { + attr?.let { attributes0.copy(it) } + } + + private val size0 = Size(189, 53) + override fun getSize(): Size { + return Size().also { it.copy(size0) } + } + + override fun setSize(size: Size?) { + size?.let { size0.copy(it) } + } +} \ No newline at end of file