mirror of
https://github.com/slhaf/Partner.git
synced 2026-05-12 08:43:02 +08:00
refactor(partnerctl): extract TerminalText styling utility and reuse it in prompt/chat rendering
This commit is contained in:
@@ -4,25 +4,72 @@ import work.slhaf.partner.api.InteractionEvent
|
||||
import work.slhaf.partner.api.ModuleEvent
|
||||
import work.slhaf.partner.api.ReplyEvent
|
||||
import work.slhaf.partner.api.SystemEvent
|
||||
import work.slhaf.partner.ctl.support.PromptPart
|
||||
import work.slhaf.partner.ctl.support.PromptStyle
|
||||
import work.slhaf.partner.ctl.support.TerminalText
|
||||
|
||||
internal class ChatEventRenderer {
|
||||
fun renderCommittedUserInput(content: String): String = "you: $content"
|
||||
fun renderCommittedUserInput(content: String): String {
|
||||
return TerminalText.render(
|
||||
listOf(
|
||||
PromptPart("you", PromptStyle.CYAN),
|
||||
PromptPart(": "),
|
||||
PromptPart(content),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun renderActiveReply(content: String): String {
|
||||
return if (content.isBlank()) {
|
||||
"assistant:"
|
||||
TerminalText.render(
|
||||
listOf(
|
||||
PromptPart("assistant", PromptStyle.GREEN),
|
||||
PromptPart(":"),
|
||||
)
|
||||
)
|
||||
} else {
|
||||
"assistant: $content"
|
||||
TerminalText.render(
|
||||
listOf(
|
||||
PromptPart("assistant", PromptStyle.GREEN),
|
||||
PromptPart(": "),
|
||||
PromptPart(content),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun renderEventMessage(event: InteractionEvent): String? {
|
||||
return when (event) {
|
||||
is ReplyEvent -> null
|
||||
is SystemEvent -> "system: ${event.title}: ${event.content}"
|
||||
is ModuleEvent -> "module:${event.data.module}: ${event.data.content}"
|
||||
is SystemEvent -> TerminalText.render(
|
||||
listOf(
|
||||
PromptPart("system", PromptStyle.YELLOW),
|
||||
PromptPart(": "),
|
||||
PromptPart(event.title),
|
||||
PromptPart(": "),
|
||||
PromptPart(event.content),
|
||||
)
|
||||
)
|
||||
|
||||
is ModuleEvent -> TerminalText.render(
|
||||
listOf(
|
||||
PromptPart("module", PromptStyle.BLUE),
|
||||
PromptPart(":"),
|
||||
PromptPart(event.data.module),
|
||||
PromptPart(": "),
|
||||
PromptPart(event.data.content),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun renderSendFailure(message: String): String = "send failed: $message"
|
||||
fun renderSendFailure(message: String): String {
|
||||
return TerminalText.render(
|
||||
listOf(
|
||||
PromptPart("send failed", PromptStyle.RED),
|
||||
PromptPart(": "),
|
||||
PromptPart(message),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import org.jline.terminal.TerminalBuilder
|
||||
import work.slhaf.partner.api.InteractionEvent
|
||||
import work.slhaf.partner.api.InteractionEvent.EventStatus
|
||||
import work.slhaf.partner.api.ReplyEvent
|
||||
import work.slhaf.partner.ctl.support.TerminalText
|
||||
import java.util.concurrent.BlockingQueue
|
||||
import java.util.concurrent.LinkedBlockingQueue
|
||||
import kotlin.math.ceil
|
||||
@@ -196,7 +197,7 @@ internal class ChatScreen(
|
||||
|
||||
private fun measureDisplayRows(text: String): Int {
|
||||
val width = terminal.width.takeIf { it > 0 } ?: DEFAULT_TERMINAL_WIDTH
|
||||
return text.split('\n').sumOf { line ->
|
||||
return TerminalText.stripAnsi(text).split('\n').sumOf { line ->
|
||||
ceil(displayWidth(line).coerceAtLeast(1).toDouble() / width.toDouble())
|
||||
.toInt()
|
||||
.coerceAtLeast(1)
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package work.slhaf.partner.ctl.support
|
||||
|
||||
data class PromptPart(
|
||||
val text: String,
|
||||
val style: PromptStyle = PromptStyle.PLAIN,
|
||||
)
|
||||
|
||||
enum class PromptStyle {
|
||||
PLAIN,
|
||||
DIM,
|
||||
BOLD,
|
||||
CYAN,
|
||||
GREEN,
|
||||
YELLOW,
|
||||
RED,
|
||||
BLUE,
|
||||
}
|
||||
|
||||
object TerminalText {
|
||||
val colorEnabled: Boolean
|
||||
get() = System.getenv("NO_COLOR") == null &&
|
||||
!(System.getenv("TERM") ?: "").equals("dumb", ignoreCase = true)
|
||||
|
||||
fun render(parts: List<PromptPart>, colorEnabled: Boolean = this.colorEnabled): String {
|
||||
return parts.joinToString(separator = "") { render(it, colorEnabled) }
|
||||
}
|
||||
|
||||
fun render(part: PromptPart, colorEnabled: Boolean = this.colorEnabled): String {
|
||||
return when (part.style) {
|
||||
PromptStyle.PLAIN -> part.text
|
||||
PromptStyle.DIM -> ansi("2", part.text, colorEnabled)
|
||||
PromptStyle.BOLD -> ansi("1", part.text, colorEnabled)
|
||||
PromptStyle.CYAN -> ansi("36", part.text, colorEnabled)
|
||||
PromptStyle.GREEN -> ansi("32", part.text, colorEnabled)
|
||||
PromptStyle.YELLOW -> ansi("33", part.text, colorEnabled)
|
||||
PromptStyle.RED -> ansi("31", part.text, colorEnabled)
|
||||
PromptStyle.BLUE -> ansi("34", part.text, colorEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
fun ansi(code: String, text: String, colorEnabled: Boolean = this.colorEnabled): String {
|
||||
if (!colorEnabled) return text
|
||||
val escape = 27.toChar()
|
||||
return "$escape[${code}m$text$escape[0m"
|
||||
}
|
||||
|
||||
fun stripAnsi(text: String): String {
|
||||
val escape = 27.toChar()
|
||||
val result = StringBuilder(text.length)
|
||||
var index = 0
|
||||
|
||||
while (index < text.length) {
|
||||
if (text[index] == escape && index + 1 < text.length && text[index + 1] == '[') {
|
||||
index += 2
|
||||
while (index < text.length && text[index] !in '@'..'~') {
|
||||
index++
|
||||
}
|
||||
if (index < text.length) {
|
||||
index++
|
||||
}
|
||||
} else {
|
||||
result.append(text[index])
|
||||
index++
|
||||
}
|
||||
}
|
||||
|
||||
return result.toString()
|
||||
}
|
||||
}
|
||||
@@ -8,26 +8,13 @@ import org.jline.reader.LineReaderBuilder
|
||||
import org.jline.reader.UserInterruptException
|
||||
import org.jline.terminal.Terminal
|
||||
import org.jline.terminal.TerminalBuilder
|
||||
import work.slhaf.partner.ctl.support.PromptPart
|
||||
import work.slhaf.partner.ctl.support.PromptStyle
|
||||
import work.slhaf.partner.ctl.support.TerminalText
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
|
||||
data class PromptPart(
|
||||
val text: String,
|
||||
val style: PromptStyle = PromptStyle.PLAIN,
|
||||
)
|
||||
|
||||
enum class PromptStyle {
|
||||
PLAIN,
|
||||
DIM,
|
||||
BOLD,
|
||||
CYAN,
|
||||
GREEN,
|
||||
YELLOW,
|
||||
RED,
|
||||
BLUE,
|
||||
}
|
||||
|
||||
class Prompt private constructor(
|
||||
private val terminal: Terminal,
|
||||
private val reader: LineReader,
|
||||
@@ -46,28 +33,25 @@ class Prompt private constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private val colorEnabled: Boolean =
|
||||
System.getenv("NO_COLOR") == null &&
|
||||
!(System.getenv("TERM") ?: "").equals("dumb", ignoreCase = true)
|
||||
private val colorEnabled: Boolean = TerminalText.colorEnabled
|
||||
|
||||
private fun ansi(code: String, text: String): String {
|
||||
val escape = 27.toChar()
|
||||
return if (colorEnabled) "$escape[${code}m$text$escape[0m" else text
|
||||
return TerminalText.ansi(code, text, colorEnabled)
|
||||
}
|
||||
|
||||
private fun bold(text: String) = ansi("1", text)
|
||||
private fun bold(text: String) = TerminalText.render(PromptPart(text, PromptStyle.BOLD), colorEnabled)
|
||||
|
||||
private fun dim(text: String) = ansi("2", text)
|
||||
private fun dim(text: String) = TerminalText.render(PromptPart(text, PromptStyle.DIM), colorEnabled)
|
||||
|
||||
private fun cyan(text: String) = ansi("36", text)
|
||||
private fun cyan(text: String) = TerminalText.render(PromptPart(text, PromptStyle.CYAN), colorEnabled)
|
||||
|
||||
private fun green(text: String) = ansi("32", text)
|
||||
private fun green(text: String) = TerminalText.render(PromptPart(text, PromptStyle.GREEN), colorEnabled)
|
||||
|
||||
private fun yellow(text: String) = ansi("33", text)
|
||||
private fun yellow(text: String) = TerminalText.render(PromptPart(text, PromptStyle.YELLOW), colorEnabled)
|
||||
|
||||
private fun red(text: String) = ansi("31", text)
|
||||
private fun red(text: String) = TerminalText.render(PromptPart(text, PromptStyle.RED), colorEnabled)
|
||||
|
||||
private fun blue(text: String) = ansi("34", text)
|
||||
private fun blue(text: String) = TerminalText.render(PromptPart(text, PromptStyle.BLUE), colorEnabled)
|
||||
|
||||
private fun questionPrefix() = cyan("?")
|
||||
|
||||
@@ -85,18 +69,7 @@ class Prompt private constructor(
|
||||
}
|
||||
|
||||
private fun renderPrompt(parts: List<PromptPart>): String {
|
||||
return parts.joinToString(separator = "") { part ->
|
||||
when (part.style) {
|
||||
PromptStyle.PLAIN -> part.text
|
||||
PromptStyle.DIM -> dim(part.text)
|
||||
PromptStyle.BOLD -> bold(part.text)
|
||||
PromptStyle.CYAN -> cyan(part.text)
|
||||
PromptStyle.GREEN -> green(part.text)
|
||||
PromptStyle.YELLOW -> yellow(part.text)
|
||||
PromptStyle.RED -> red(part.text)
|
||||
PromptStyle.BLUE -> blue(part.text)
|
||||
}
|
||||
}
|
||||
return TerminalText.render(parts, colorEnabled)
|
||||
}
|
||||
|
||||
fun print(message: String) {
|
||||
|
||||
Reference in New Issue
Block a user