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.ModuleEvent
|
||||||
import work.slhaf.partner.api.ReplyEvent
|
import work.slhaf.partner.api.ReplyEvent
|
||||||
import work.slhaf.partner.api.SystemEvent
|
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 {
|
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 {
|
fun renderActiveReply(content: String): String {
|
||||||
return if (content.isBlank()) {
|
return if (content.isBlank()) {
|
||||||
"assistant:"
|
TerminalText.render(
|
||||||
|
listOf(
|
||||||
|
PromptPart("assistant", PromptStyle.GREEN),
|
||||||
|
PromptPart(":"),
|
||||||
|
)
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
"assistant: $content"
|
TerminalText.render(
|
||||||
|
listOf(
|
||||||
|
PromptPart("assistant", PromptStyle.GREEN),
|
||||||
|
PromptPart(": "),
|
||||||
|
PromptPart(content),
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun renderEventMessage(event: InteractionEvent): String? {
|
fun renderEventMessage(event: InteractionEvent): String? {
|
||||||
return when (event) {
|
return when (event) {
|
||||||
is ReplyEvent -> null
|
is ReplyEvent -> null
|
||||||
is SystemEvent -> "system: ${event.title}: ${event.content}"
|
is SystemEvent -> TerminalText.render(
|
||||||
is ModuleEvent -> "module:${event.data.module}: ${event.data.content}"
|
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
|
||||||
import work.slhaf.partner.api.InteractionEvent.EventStatus
|
import work.slhaf.partner.api.InteractionEvent.EventStatus
|
||||||
import work.slhaf.partner.api.ReplyEvent
|
import work.slhaf.partner.api.ReplyEvent
|
||||||
|
import work.slhaf.partner.ctl.support.TerminalText
|
||||||
import java.util.concurrent.BlockingQueue
|
import java.util.concurrent.BlockingQueue
|
||||||
import java.util.concurrent.LinkedBlockingQueue
|
import java.util.concurrent.LinkedBlockingQueue
|
||||||
import kotlin.math.ceil
|
import kotlin.math.ceil
|
||||||
@@ -196,7 +197,7 @@ internal class ChatScreen(
|
|||||||
|
|
||||||
private fun measureDisplayRows(text: String): Int {
|
private fun measureDisplayRows(text: String): Int {
|
||||||
val width = terminal.width.takeIf { it > 0 } ?: DEFAULT_TERMINAL_WIDTH
|
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())
|
ceil(displayWidth(line).coerceAtLeast(1).toDouble() / width.toDouble())
|
||||||
.toInt()
|
.toInt()
|
||||||
.coerceAtLeast(1)
|
.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.reader.UserInterruptException
|
||||||
import org.jline.terminal.Terminal
|
import org.jline.terminal.Terminal
|
||||||
import org.jline.terminal.TerminalBuilder
|
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.Files
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.nio.file.Paths
|
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(
|
class Prompt private constructor(
|
||||||
private val terminal: Terminal,
|
private val terminal: Terminal,
|
||||||
private val reader: LineReader,
|
private val reader: LineReader,
|
||||||
@@ -46,28 +33,25 @@ class Prompt private constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val colorEnabled: Boolean =
|
private val colorEnabled: Boolean = TerminalText.colorEnabled
|
||||||
System.getenv("NO_COLOR") == null &&
|
|
||||||
!(System.getenv("TERM") ?: "").equals("dumb", ignoreCase = true)
|
|
||||||
|
|
||||||
private fun ansi(code: String, text: String): String {
|
private fun ansi(code: String, text: String): String {
|
||||||
val escape = 27.toChar()
|
return TerminalText.ansi(code, text, colorEnabled)
|
||||||
return if (colorEnabled) "$escape[${code}m$text$escape[0m" else text
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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("?")
|
private fun questionPrefix() = cyan("?")
|
||||||
|
|
||||||
@@ -85,18 +69,7 @@ class Prompt private constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun renderPrompt(parts: List<PromptPart>): String {
|
private fun renderPrompt(parts: List<PromptPart>): String {
|
||||||
return parts.joinToString(separator = "") { part ->
|
return TerminalText.render(parts, colorEnabled)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun print(message: String) {
|
fun print(message: String) {
|
||||||
|
|||||||
Reference in New Issue
Block a user