From 85d81efc4a8da5ce08a628732b84e51e3451d13f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=BE=AE=E8=8E=B9=C2=B7=E7=BA=A4=E7=BB=AB?=
 <kar@kasukusakura.com>
Date: Sat, 20 Aug 2022 15:58:03 +0800
Subject: [PATCH] [console] Console artifacts downloading progress (#2140)

* Console artifacts downloading progress

* Add missing code

* Update kdoc & naming

* make `terminalDisplay` internal

* improve performance

* Update DownloadingProgress.kt

[skip ci]

* Rename DownloadingProgress to ProcessProgress

* make ProcessProgress stable
---
 .../compatibility-validation/jvm/api/jvm.api  |  11 ++
 .../backend/mirai-console/src/MiraiConsole.kt |  20 +++
 .../src/MiraiConsoleImplementation.kt         |   8 +-
 .../src/command/BuiltInCommands.kt            |  52 +-----
 .../fontend/DefaultLoggingProcessProgress.kt  |  71 ++++++++
 .../src/fontend/ProcessProgress.kt            |  70 ++++++++
 .../plugin/JvmPluginDependencyDownload.kt     |  50 ++++++
 .../mirai-console/src/util/MemoryFormat.kt    |  63 +++++++
 .../src/ConsoleInputImpl.kt                   |   2 +
 .../src/MiraiConsoleImplementationTerminal.kt | 166 ++++++++++++++++++
 .../src/MiraiConsoleTerminalLoader.kt         |   2 +
 .../src/TerminalProcessProgress.kt            | 164 +++++++++++++++++
 12 files changed, 627 insertions(+), 52 deletions(-)
 create mode 100644 mirai-console/backend/mirai-console/src/fontend/DefaultLoggingProcessProgress.kt
 create mode 100644 mirai-console/backend/mirai-console/src/fontend/ProcessProgress.kt
 create mode 100644 mirai-console/backend/mirai-console/src/util/MemoryFormat.kt
 create mode 100644 mirai-console/frontend/mirai-console-terminal/src/TerminalProcessProgress.kt

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