diff --git a/mirai-console/backend/mirai-console/compatibility-validation/jvm/api/jvm.api b/mirai-console/backend/mirai-console/compatibility-validation/jvm/api/jvm.api index 6ef85562f..417758581 100644 --- a/mirai-console/backend/mirai-console/compatibility-validation/jvm/api/jvm.api +++ b/mirai-console/backend/mirai-console/compatibility-validation/jvm/api/jvm.api @@ -1719,6 +1719,17 @@ public final class net/mamoe/mirai/console/extensions/SingletonExtensionSelector public fun toString ()Ljava/lang/String; } +public abstract interface class net/mamoe/mirai/console/fontend/ProcessProgress : java/io/Closeable { + public abstract fun close ()V + public abstract fun markFailed ()V + public abstract fun rerender ()V + public abstract fun setTotalSize (J)V + public abstract fun update (J)V + public abstract fun update (JJ)V + public fun updateText (Ljava/lang/CharSequence;)V + public abstract fun updateText (Ljava/lang/String;)V +} + public final class net/mamoe/mirai/console/logging/AbstractLoggerController$LogPriority$Companion { public final fun by (Lnet/mamoe/mirai/utils/SimpleLogger$LogPriority;)Lnet/mamoe/mirai/console/logging/AbstractLoggerController$LogPriority; } diff --git a/mirai-console/backend/mirai-console/src/MiraiConsole.kt b/mirai-console/backend/mirai-console/src/MiraiConsole.kt index ff4f3e11c..091198e3a 100644 --- a/mirai-console/backend/mirai-console/src/MiraiConsole.kt +++ b/mirai-console/backend/mirai-console/src/MiraiConsole.kt @@ -19,6 +19,7 @@ import net.mamoe.mirai.BotFactory import net.mamoe.mirai.console.MiraiConsole.INSTANCE import net.mamoe.mirai.console.MiraiConsoleImplementation.Companion.start import net.mamoe.mirai.console.extensions.BotConfigurationAlterer +import net.mamoe.mirai.console.fontend.ProcessProgress import net.mamoe.mirai.console.internal.MiraiConsoleImplementationBridge import net.mamoe.mirai.console.internal.extension.GlobalComponentStorage import net.mamoe.mirai.console.plugin.PluginManager @@ -254,6 +255,25 @@ public interface MiraiConsole : CoroutineScope { MiraiConsoleImplementation.shutdown() } } + + /** + * 创建一个新的处理进度, 此进度将会在前端显示, 并且此进度需要[手动关闭][ProcessProgress.close] + * + * 注: 此 API 应该只在以下情况使用 + * + * - 插件初始化 (包括 onLoad, onEnable) + * - 命令执行中 (控制台) + * + * 在其他情况使用可能会导致意外的情况 + * + * // implementation note: + * 在 Terminal 前端中, 有处理进度存在时会停止命令输入 (即停止命令执行) + */ + @ConsoleExperimentalApi + @JvmStatic + public fun newProcessProgress(): ProcessProgress { + return MiraiConsoleImplementation.getInstance().createNewProcessProgress() + } } diff --git a/mirai-console/backend/mirai-console/src/MiraiConsoleImplementation.kt b/mirai-console/backend/mirai-console/src/MiraiConsoleImplementation.kt index 2f547d246..4ac891081 100644 --- a/mirai-console/backend/mirai-console/src/MiraiConsoleImplementation.kt +++ b/mirai-console/backend/mirai-console/src/MiraiConsoleImplementation.kt @@ -22,13 +22,14 @@ import net.mamoe.mirai.console.data.PluginConfig import net.mamoe.mirai.console.data.PluginData import net.mamoe.mirai.console.data.PluginDataStorage import net.mamoe.mirai.console.extension.ComponentStorage +import net.mamoe.mirai.console.fontend.DefaultLoggingProcessProgress import net.mamoe.mirai.console.internal.MiraiConsoleImplementationBridge import net.mamoe.mirai.console.internal.command.CommandManagerImpl import net.mamoe.mirai.console.internal.data.builtins.ConsoleDataScopeImpl +import net.mamoe.mirai.console.fontend.ProcessProgress import net.mamoe.mirai.console.internal.logging.LoggerControllerImpl import net.mamoe.mirai.console.internal.plugin.BuiltInJvmPluginLoaderImpl import net.mamoe.mirai.console.internal.pluginManagerImpl -import net.mamoe.mirai.console.internal.shutdown.ShutdownDaemon import net.mamoe.mirai.console.logging.LoggerController import net.mamoe.mirai.console.plugin.Plugin import net.mamoe.mirai.console.plugin.jvm.JvmPluginLoader @@ -220,6 +221,11 @@ public interface MiraiConsoleImplementation : CoroutineScope { */ public fun createLogger(identity: String?): MiraiLogger + /** @see [MiraiConsole.newProcessProgress] */ + public fun createNewProcessProgress(): ProcessProgress { + return DefaultLoggingProcessProgress() + } + /** * 该前端是否支持使用 Ansi 输出彩色信息 * diff --git a/mirai-console/backend/mirai-console/src/command/BuiltInCommands.kt b/mirai-console/backend/mirai-console/src/command/BuiltInCommands.kt index 6c5c31697..5122aa0df 100644 --- a/mirai-console/backend/mirai-console/src/command/BuiltInCommands.kt +++ b/mirai-console/backend/mirai-console/src/command/BuiltInCommands.kt @@ -47,10 +47,7 @@ import net.mamoe.mirai.console.permission.PermissionService.Companion.permit import net.mamoe.mirai.console.permission.PermitteeId import net.mamoe.mirai.console.plugin.name import net.mamoe.mirai.console.plugin.version -import net.mamoe.mirai.console.util.AnsiMessageBuilder -import net.mamoe.mirai.console.util.ConsoleExperimentalApi -import net.mamoe.mirai.console.util.ConsoleInternalApi -import net.mamoe.mirai.console.util.sendAnsiMessage +import net.mamoe.mirai.console.util.* import net.mamoe.mirai.event.events.EventCancelledException import net.mamoe.mirai.utils.BotConfiguration import java.lang.management.ManagementFactory @@ -58,7 +55,6 @@ import java.lang.management.MemoryMXBean import java.lang.management.MemoryUsage import java.time.ZoneId import java.time.format.DateTimeFormatter -import kotlin.math.floor import kotlin.system.exitProcess @@ -687,35 +683,6 @@ public object BuiltInCommands { } } - private const val MEM_B = 1024L - private const val MEM_KB = 1024L shl 10 - private const val MEM_MB = 1024L shl 20 - private const val MEM_GB = 1024L shl 30 - - @Suppress("NOTHING_TO_INLINE") - private inline fun StringBuilder.appendDouble(number: Double): StringBuilder = - append(floor(number * 100) / 100) - - private fun renderMemoryUsageNumber(num: Long) = buildString { - when { - num == -1L -> { - append(num) - } - num < MEM_B -> { - append(num).append("B") - } - num < MEM_KB -> { - appendDouble(num / 1024.0).append("KB") - } - num < MEM_MB -> { - appendDouble((num ushr 10) / 1024.0).append("MB") - } - else -> { - appendDouble((num ushr 20) / 1024.0).append("GB") - } - } - } - private fun AnsiMessageBuilder.renderMemoryUsage(usage: MUsage) = arrayOf( renderMemoryUsageNumber(usage.committed), renderMemoryUsageNumber(usage.init), @@ -728,23 +695,6 @@ public object BuiltInCommands { usage.max, ) - private var emptyLine = " ".repeat(10) - private fun Appendable.emptyLine(size: Int) { - if (emptyLine.length <= size) { - emptyLine = String(CharArray(size) { ' ' }) - } - append(emptyLine, 0, size) - } - - private inline fun AnsiMessageBuilder.renderMUNum(size: Int, contentLength: Int, code: () -> Unit) { - val s = size - contentLength - val left = s / 2 - val right = s - left - emptyLine(left) - code() - emptyLine(right) - } - private fun calculateMax( vararg lines: Array<String> ): IntArray = IntArray(lines[0].size) { r -> diff --git a/mirai-console/backend/mirai-console/src/fontend/DefaultLoggingProcessProgress.kt b/mirai-console/backend/mirai-console/src/fontend/DefaultLoggingProcessProgress.kt new file mode 100644 index 000000000..32fc7af77 --- /dev/null +++ b/mirai-console/backend/mirai-console/src/fontend/DefaultLoggingProcessProgress.kt @@ -0,0 +1,71 @@ +/* + * Copyright 2019-2022 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/dev/LICENSE + */ + +package net.mamoe.mirai.console.fontend + +import net.mamoe.mirai.console.MiraiConsole + +/** + * [ProcessProgress] 的简单实现, 前端应该自行实现 [ProcessProgress] + * + * 此类为前端未实现 [ProcessProgress] 时的缺省实现 + */ +internal class DefaultLoggingProcessProgress : ProcessProgress { + private var message: String = "" + private var lastDisplay = 0L + private var changed: Boolean = false + private var failed: Boolean = false + + private companion object { + private val logger by lazy { MiraiConsole.createLogger("ProcessProgress") } + } + + override fun updateText(txt: String) { + this.message = txt + changed = true + } + + override fun setTotalSize(totalSize: Long) { + } + + override fun update(processed: Long) { + } + + override fun update(processed: Long, totalSize: Long) { + } + + override fun markFailed() { + failed = true + } + + override fun rerender() { + if (!changed) return + changed = false + val crtTime = System.currentTimeMillis() + if (crtTime - lastDisplay < 1000) { + return + } + lastDisplay = crtTime + if (failed) { + logger.error(message) + } else { + logger.info(message) + } + } + + override fun close() { + if (failed) { + logger.error(message) + } else { + logger.info(message) + } + changed = false + message = "" + } +} \ No newline at end of file diff --git a/mirai-console/backend/mirai-console/src/fontend/ProcessProgress.kt b/mirai-console/backend/mirai-console/src/fontend/ProcessProgress.kt new file mode 100644 index 000000000..ffa91cf06 --- /dev/null +++ b/mirai-console/backend/mirai-console/src/fontend/ProcessProgress.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2019-2022 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/dev/LICENSE + */ + +package net.mamoe.mirai.console.fontend + +import net.mamoe.mirai.console.MiraiConsole +import net.mamoe.mirai.console.util.ConsoleExperimentalApi +import java.io.Closeable + +/** + * 一个下载进度 + * + * @see MiraiConsole.newProcessProgress + */ +// @ConsoleFrontEndImplementation +public interface ProcessProgress : Closeable { + /** + * 更新当前下载进度的文本 + */ + public fun updateText(txt: String) + + /** + * 更新当前下载进度的文本 + */ + public fun updateText(txt: CharSequence) { + updateText(txt.toString()) + } + + /** + * 设置此进度的最终大小 + */ + public fun setTotalSize(totalSize: Long) + + /** + * 更新下载进度的进度 + * + * 在更新进度后需要[刷新显示][rerender] + */ + public fun update(processed: Long) + + /** + * 更新下载进度的进度 + * + * 在更新进度后需要[刷新显示][rerender] + */ + public fun update(processed: Long, totalSize: Long) + + /** + * 将该进度标记为 已失败 / 出错 + * + * 注: 即使此进度被标记为失败, 也需要[手动释放][close] + */ + public fun markFailed() + + /** + * 立即重新渲染此进度 + */ + public fun rerender() + + /** + * 释放此进度, 相关资源和 UI 将会更新 + */ + override fun close() +} diff --git a/mirai-console/backend/mirai-console/src/internal/plugin/JvmPluginDependencyDownload.kt b/mirai-console/backend/mirai-console/src/internal/plugin/JvmPluginDependencyDownload.kt index ed0bbdcc5..c54e724e1 100644 --- a/mirai-console/backend/mirai-console/src/internal/plugin/JvmPluginDependencyDownload.kt +++ b/mirai-console/backend/mirai-console/src/internal/plugin/JvmPluginDependencyDownload.kt @@ -9,11 +9,14 @@ package net.mamoe.mirai.console.internal.plugin +import net.mamoe.mirai.console.MiraiConsoleImplementation import net.mamoe.mirai.console.MiraiConsoleImplementation.ConsoleDataScope.Companion.get +import net.mamoe.mirai.console.fontend.ProcessProgress import net.mamoe.mirai.console.internal.MiraiConsoleBuildDependencies import net.mamoe.mirai.console.internal.data.builtins.DataScope import net.mamoe.mirai.console.internal.data.builtins.PluginDependenciesConfig import net.mamoe.mirai.console.plugin.PluginManager +import net.mamoe.mirai.console.util.renderMemoryUsageNumber import net.mamoe.mirai.utils.MiraiLogger import net.mamoe.mirai.utils.debug import net.mamoe.mirai.utils.verbose @@ -41,6 +44,7 @@ import org.eclipse.aether.transfer.AbstractTransferListener import org.eclipse.aether.transfer.TransferEvent import org.eclipse.aether.transport.http.HttpTransporterFactory import java.io.File +import java.util.concurrent.ConcurrentHashMap @Suppress("DEPRECATION", "MemberVisibilityCanBePrivate") @@ -96,14 +100,60 @@ internal class JvmPluginDependencyDownloader( session, LocalRepository(PluginManager.pluginLibrariesFolder) ) session.transferListener = object : AbstractTransferListener() { + private val dwnProgresses: MutableMap<File, ProcessProgress> = ConcurrentHashMap() + override fun transferStarted(event: TransferEvent) { logger.verbose { "Downloading ${event.resource?.repositoryUrl}${event.resource?.resourceName}" } + val nw = MiraiConsoleImplementation.getInstance().createNewProcessProgress() + dwnProgresses.put( + event.resource.file, nw + )?.close() + nw.setTotalSize(event.resource.contentLength) + nw.updateText("Downloading ${event.resource.resourceName}....") + } + + override fun transferSucceeded(event: TransferEvent) { + dwnProgresses.remove(event.resource.file)?.let { dp -> + dp.updateText(buildString { + append("Downloaded ") + append(event.resource.resourceName) + append(" (") + renderMemoryUsageNumber(this@buildString, event.resource.contentLength) + append(")") + }) + dp.close() + } + } + + override fun transferProgressed(event: TransferEvent) { + dwnProgresses[event.resource.file]?.let { pg -> + pg.update(event.transferredBytes) + pg.updateText(buildString bs@{ + append("Downloading ") + append(event.resource.resourceName) + append(" (") + val sz = this@bs.length + renderMemoryUsageNumber(this@bs, event.transferredBytes) + repeat(kotlin.math.max(0, 7 - (this@bs.length - sz))) { + append(' ') + } + + append(" / ") + renderMemoryUsageNumber(this@bs, event.resource.contentLength) + append(")") + }) + pg.rerender() + } } override fun transferFailed(event: TransferEvent) { logger.warning(event.exception) + dwnProgresses.remove(event.resource.file)?.let { + it.markFailed() + it.close() + } } } val userHome = System.getProperty("user.home") diff --git a/mirai-console/backend/mirai-console/src/util/MemoryFormat.kt b/mirai-console/backend/mirai-console/src/util/MemoryFormat.kt new file mode 100644 index 000000000..9875779cd --- /dev/null +++ b/mirai-console/backend/mirai-console/src/util/MemoryFormat.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2019-2022 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/dev/LICENSE + */ + +package net.mamoe.mirai.console.util + +import kotlin.math.floor + + +private const val MEM_B = 1024L +private const val MEM_KB = 1024L shl 10 +private const val MEM_MB = 1024L shl 20 +private const val MEM_GB = 1024L shl 30 + +@Suppress("NOTHING_TO_INLINE") +private inline fun StringBuilder.appendDouble(number: Double): StringBuilder = + append(floor(number * 100) / 100) + +internal fun renderMemoryUsageNumber(num: Long) = buildString { + renderMemoryUsageNumber(this, num) +} + +internal fun renderMemoryUsageNumber(builder: StringBuilder, num: Long) { + when { + num == -1L -> { + builder.append(num) + } + num < MEM_B -> { + builder.append(num).append("B") + } + num < MEM_KB -> { + builder.appendDouble(num / 1024.0).append("KB") + } + num < MEM_MB -> { + builder.appendDouble((num ushr 10) / 1024.0).append("MB") + } + else -> { + builder.appendDouble((num ushr 20) / 1024.0).append("GB") + } + } +} + +private var emptyLine = " ".repeat(10) +internal fun Appendable.emptyLine(size: Int) { + if (emptyLine.length <= size) { + emptyLine = String(CharArray(size) { ' ' }) + } + append(emptyLine, 0, size) +} + +internal inline fun AnsiMessageBuilder.renderMUNum(size: Int, contentLength: Int, code: () -> Unit) { + val s = size - contentLength + val left = s / 2 + val right = s - left + emptyLine(left) + code() + emptyLine(right) +} diff --git a/mirai-console/frontend/mirai-console-terminal/src/ConsoleInputImpl.kt b/mirai-console/frontend/mirai-console-terminal/src/ConsoleInputImpl.kt index 3bfe74c36..858f449d8 100644 --- a/mirai-console/frontend/mirai-console-terminal/src/ConsoleInputImpl.kt +++ b/mirai-console/frontend/mirai-console-terminal/src/ConsoleInputImpl.kt @@ -42,8 +42,10 @@ internal object ConsoleInputImpl : ConsoleInput { kotlin.runCatching { thread.submit { kotlin.runCatching { + waitDownloadingProgressEmpty() lineReader.readLine( if (hint.isNotEmpty()) { + prePrintNewLog() lineReader.printAbove( Ansi.ansi() .fgCyan() diff --git a/mirai-console/frontend/mirai-console-terminal/src/MiraiConsoleImplementationTerminal.kt b/mirai-console/frontend/mirai-console-terminal/src/MiraiConsoleImplementationTerminal.kt index 48265f24b..5e22ce841 100644 --- a/mirai-console/frontend/mirai-console-terminal/src/MiraiConsoleImplementationTerminal.kt +++ b/mirai-console/frontend/mirai-console-terminal/src/MiraiConsoleImplementationTerminal.kt @@ -30,6 +30,7 @@ import net.mamoe.mirai.console.MiraiConsoleImplementation import net.mamoe.mirai.console.command.CommandManager import net.mamoe.mirai.console.data.MultiFilePluginDataStorage import net.mamoe.mirai.console.data.PluginDataStorage +import net.mamoe.mirai.console.fontend.ProcessProgress import net.mamoe.mirai.console.plugin.jvm.JvmPluginLoader import net.mamoe.mirai.console.plugin.loader.PluginLoader import net.mamoe.mirai.console.terminal.ConsoleInputImpl.requestInput @@ -43,12 +44,18 @@ import net.mamoe.mirai.utils.* import org.fusesource.jansi.Ansi import org.jline.reader.LineReader import org.jline.reader.LineReaderBuilder +import org.jline.reader.impl.LineReaderImpl import org.jline.reader.impl.completer.NullCompleter import org.jline.terminal.Terminal import org.jline.terminal.TerminalBuilder import org.jline.terminal.impl.AbstractWindowsTerminal +import org.jline.utils.AttributedString +import org.jline.utils.Display import java.nio.file.Path import java.nio.file.Paths +import kotlin.concurrent.withLock +import kotlin.coroutines.Continuation +import kotlin.reflect.KProperty /** * mirai-console-terminal 后端实现 @@ -86,6 +93,7 @@ open class MiraiConsoleImplementationTerminal configStorageForBuiltIns ) } + // used in test internal val logService: LoggingService @@ -97,7 +105,9 @@ open class MiraiConsoleImplementationTerminal override fun createLogger(identity: String?): MiraiLogger { return PlatformLogger(identity = identity, output = { line -> val text = line + ANSI_RESET + prePrintNewLog() lineReader.printAbove(text) + postPrintNewLog() logService.pushLine(text) }) } @@ -122,6 +132,21 @@ open class MiraiConsoleImplementationTerminal override fun preStart() { registerSignalHandler() overrideSTD(this) + launch(CoroutineName("Mirai Console Terminal Downloading Progress Bar Updater")) { + while (isActive) { + downloadingProgressDaemonStub() + } + } + } + + override fun createNewProcessProgress(): ProcessProgress { + if (terminal is NoConsole) return super.createNewProcessProgress() + + containDownloadingProgress = true + kotlin.runCatching { + downloadingProgressCoroutine?.resumeWith(Result.success(Unit)) + } + return TerminalProcessProgress(lineReader).also { terminalDownloadingProgresses.add(it) } } } @@ -135,6 +160,147 @@ val lineReader: LineReader by lazy { .build() } +internal val terminalDisplay: Display by object : kotlin.properties.ReadOnlyProperty<Any?, Display> { + val delegate: () -> Display by lazy { + val terminal = terminal + if (terminal is NoConsole) { + val display = Display(terminal, false) + return@lazy { display } + } + + val lr = lineReader + val field = LineReaderImpl::class.java.declaredFields.first { it.type == Display::class.java } + field.isAccessible = true + return@lazy { field[lr] as Display } + } + + override fun getValue(thisRef: Any?, property: KProperty<*>): Display { + return delegate() + } +} + +internal val terminalExecuteLock: java.util.concurrent.locks.Lock by lazy { + val terminal = terminal + if (terminal is NoConsole) return@lazy java.util.concurrent.locks.ReentrantLock() + val lr = lineReader + val field = LineReaderImpl::class.java.declaredFields.first { + java.util.concurrent.locks.Lock::class.java.isAssignableFrom(it.type) + } + field.isAccessible = true + field[lr].cast() +} +private val terminalDownloadingProgressesNoticer = Object() +private var containDownloadingProgress: Boolean = false + get() = field || terminalDownloadingProgresses.isNotEmpty() + +internal val terminalDownloadingProgresses = mutableListOf<TerminalProcessProgress>() +private var downloadingProgressCoroutine: Continuation<Unit>? = null +private suspend fun downloadingProgressDaemonStub() { + delay(500L) + if (containDownloadingProgress) { + updateTerminalDownloadingProgresses() + } else { + suspendCancellableCoroutine<Unit> { cp -> + downloadingProgressCoroutine = cp + } + downloadingProgressCoroutine = null + } +} + +internal fun updateTerminalDownloadingProgresses() { + if (!containDownloadingProgress) return + + runCatching { downloadingProgressCoroutine?.resumeWith(Result.success(Unit)) } + + terminalExecuteLock.withLock { + if (terminalDownloadingProgresses.isNotEmpty()) { + val wid = terminal.width + if (wid == 0) { // Run in idea + if (terminalDownloadingProgresses.removeIf { it.pendingErase }) { + updateTerminalDownloadingProgresses() + return + } + terminalDisplay.update(listOf(AttributedString.EMPTY), 0, false) + // Error in idea when more than one bar displaying + terminalDisplay.update(listOf(terminalDownloadingProgresses[0].let { + it.updateTxt(0); it.ansiMsg + }), 0) + } else { + if (terminalDownloadingProgresses.size > 4) { + // to mush. delete some completed status + var allowToDelete = terminalDownloadingProgresses.size - 4 + terminalDownloadingProgresses.removeIf { pg -> + if (allowToDelete == 0) { + return@removeIf false + } + if (pg.pendingErase) { + allowToDelete-- + return@removeIf true + } + return@removeIf false + } + } + terminalDisplay.update(terminalDownloadingProgresses.map { + it.updateTxt(wid); it.ansiMsg + }, 0) + cleanupErase() + } + } else { + terminalDisplay.update(emptyList(), 0) + (lineReader as LineReaderImpl).let { lr -> + if (lr.isReading) { + lr.redisplay() + } + } + noticeDownloadingProgressEmpty() + } + } +} + +internal fun prePrintNewLog() { + if (!containDownloadingProgress) return + if (terminalDownloadingProgresses.isNotEmpty()) { + terminalExecuteLock.withLock { + terminalDisplay.update(emptyList(), 0) + } + } +} + +internal fun cleanupErase() { + val now = currentTimeMillis() + terminalDownloadingProgresses.removeIf { pg -> + if (!pg.pendingErase) return@removeIf false + if (now > pg.eraseTimestamp) { + pg.ansiMsg = AttributedString.EMPTY + return@removeIf true + } + return@removeIf false + } +} + +internal fun postPrintNewLog() { + if (!containDownloadingProgress) return + updateTerminalDownloadingProgresses() + cleanupErase() +} + +private fun noticeDownloadingProgressEmpty() { + synchronized(terminalDownloadingProgressesNoticer) { + containDownloadingProgress = false + if (terminalDownloadingProgresses.isEmpty()) { + terminalDownloadingProgressesNoticer.notifyAll() + } + } +} + +internal fun waitDownloadingProgressEmpty() { + synchronized(terminalDownloadingProgressesNoticer) { + if (containDownloadingProgress) { + terminalDownloadingProgressesNoticer.wait() + } + } +} + val terminal: Terminal = run { if (ConsoleTerminalSettings.noConsole) return@run NoConsole diff --git a/mirai-console/frontend/mirai-console-terminal/src/MiraiConsoleTerminalLoader.kt b/mirai-console/frontend/mirai-console-terminal/src/MiraiConsoleTerminalLoader.kt index 32444fbdb..5606cc20b 100644 --- a/mirai-console/frontend/mirai-console-terminal/src/MiraiConsoleTerminalLoader.kt +++ b/mirai-console/frontend/mirai-console-terminal/src/MiraiConsoleTerminalLoader.kt @@ -308,7 +308,9 @@ internal fun overrideSTD(terminal: MiraiConsoleImplementation) { internal object ConsoleCommandSenderImplTerminal : MiraiConsoleImplementation.ConsoleCommandSenderImpl { override suspend fun sendMessage(message: String) { kotlin.runCatching { + prePrintNewLog() lineReader.printAbove(message + ANSI_RESET) + postPrintNewLog() }.onFailure { exception -> // If failed. It means JLine Terminal not working... PrintStream(FileOutputStream(FileDescriptor.err)).use { diff --git a/mirai-console/frontend/mirai-console-terminal/src/TerminalProcessProgress.kt b/mirai-console/frontend/mirai-console-terminal/src/TerminalProcessProgress.kt new file mode 100644 index 000000000..89ecb864f --- /dev/null +++ b/mirai-console/frontend/mirai-console-terminal/src/TerminalProcessProgress.kt @@ -0,0 +1,164 @@ +/* + * Copyright 2019-2022 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/dev/LICENSE + */ + +package net.mamoe.mirai.console.terminal + +import net.mamoe.mirai.console.fontend.ProcessProgress +import org.jline.utils.AttributedString +import org.jline.utils.AttributedStringBuilder +import org.jline.utils.AttributedStyle + +internal class TerminalProcessProgress( + private val reader: org.jline.reader.LineReader, +) : ProcessProgress { + private var totalSize: Long = 1 + private var processed: Long = 0 + private val txt: StringBuilder = StringBuilder() + private val renderedTxt: StringBuilder = StringBuilder() + private var failed: Boolean = false + private var disposed: Boolean = false + + @JvmField + var pendingErase: Boolean = false + + @JvmField + var eraseTimestamp: Long = 0 + + @JvmField + var ansiMsg: AttributedString = AttributedString.EMPTY + + private var lastTerminalWidth = 0 + private var needRerender: Boolean = true + private var needUpdateTxt: Boolean = true + + override fun updateText(txt: CharSequence) { + this.txt.setLength(0) + this.txt.append(txt) + needUpdateTxt = true + needRerender = true + } + + override fun updateText(txt: String) { + updateText(txt as CharSequence) + } + + override fun setTotalSize(totalSize: Long) { + this.totalSize = totalSize + needRerender = true + } + + override fun update(processed: Long) { + this.processed = processed + needRerender = true + } + + override fun update(processed: Long, totalSize: Long) { + this.processed = processed + this.totalSize = totalSize + needRerender = true + } + + override fun markFailed() { + failed = true + needRerender = true + } + + internal fun updateTxt(terminalWidth: Int) { + + // region check need to update + if (needUpdateTxt || lastTerminalWidth != terminalWidth) { + // <text changed / screen width changed> + lastTerminalWidth = terminalWidth + synchronized(renderedTxt) { + renderedTxt.setLength(0) + renderedTxt.append(txt) + // paddings + if (renderedTxt.length < terminalWidth) { + repeat(terminalWidth - renderedTxt.length) { + renderedTxt.append(' ') + } + } + } + } else if (!needRerender) { + // nothing changed + return + } /* else { <api require rerender> } */ + + lastTerminalWidth = terminalWidth + // endregion + + val renderedTextWidth = when (terminalWidth) { + 0 -> renderedTxt.length + else -> terminalWidth + } + + val finalAnsiLineBuilder = AttributedStringBuilder() + + if (failed) { + finalAnsiLineBuilder.style( + AttributedStyle.DEFAULT + .background(AttributedStyle.RED) + .foreground(AttributedStyle.BLACK) + ) + finalAnsiLineBuilder.append(renderedTxt, 0, renderedTextWidth) + } else { + val downpcent = (renderedTextWidth * processed / totalSize).toInt() + if (downpcent > 0) { + finalAnsiLineBuilder.style( + AttributedStyle.DEFAULT + .background(AttributedStyle.GREEN) + .foreground(AttributedStyle.BLACK) + ) + finalAnsiLineBuilder.append(renderedTxt, 0, downpcent) + } + if (downpcent < renderedTextWidth) { + finalAnsiLineBuilder.style( + AttributedStyle.DEFAULT + .background(AttributedStyle.WHITE) + .foreground(AttributedStyle.BLACK) + ) + finalAnsiLineBuilder.append(renderedTxt, downpcent, renderedTextWidth) + } + } + ansiMsg = finalAnsiLineBuilder.toAttributedString() + needUpdateTxt = false + needRerender = false + } + + override fun rerender() { + updateTerminalDownloadingProgresses() + } + + override fun close() { + if (disposed) return + disposed = true + + totalSize = 1 + processed = 1 + needUpdateTxt = true + updateTxt(reader.terminal.width) + if (failed) { + terminalDownloadingProgresses.remove(this) + prePrintNewLog() + reader.printAbove(ansiMsg) + ansiMsg = AttributedString.EMPTY + postPrintNewLog() + return + } + // terminalDownloadingProgresses.remove(this) + pendingErase = true + eraseTimestamp = System.currentTimeMillis() + 1500L + + updateTerminalDownloadingProgresses() + + // prePrintNewLog() + // reader.printAbove(ansiMsg) + // ansiMsg = AttributedString.EMPTY + } +} \ No newline at end of file