mirror of
https://github.com/slhaf/Partner.git
synced 2026-05-12 16:53:04 +08:00
feat(partnerctl): add ANSI-styled interactive prompt output and labels
This commit is contained in:
@@ -30,6 +30,36 @@ class Prompt private constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val colorEnabled: Boolean =
|
||||||
|
System.getenv("NO_COLOR") == null &&
|
||||||
|
!(System.getenv("TERM") ?: "").equals("dumb", ignoreCase = true)
|
||||||
|
|
||||||
|
private fun ansi(code: String, text: String): String {
|
||||||
|
val escape = 27.toChar()
|
||||||
|
return if (colorEnabled) "$escape[${code}m$text$escape[0m" else text
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bold(text: String) = ansi("1", text)
|
||||||
|
|
||||||
|
private fun dim(text: String) = ansi("2", text)
|
||||||
|
|
||||||
|
private fun cyan(text: String) = ansi("36", text)
|
||||||
|
|
||||||
|
private fun green(text: String) = ansi("32", text)
|
||||||
|
|
||||||
|
private fun yellow(text: String) = ansi("33", text)
|
||||||
|
|
||||||
|
private fun red(text: String) = ansi("31", text)
|
||||||
|
|
||||||
|
private fun blue(text: String) = ansi("34", text)
|
||||||
|
|
||||||
|
private fun questionPrefix() = cyan("?")
|
||||||
|
|
||||||
|
private fun promptLabel(label: String, defaultValue: String? = null): String {
|
||||||
|
val suffix = if (defaultValue != null) " ${dim("[$defaultValue]")}" else ""
|
||||||
|
return "${questionPrefix()} $label$suffix: "
|
||||||
|
}
|
||||||
|
|
||||||
fun print(message: String) {
|
fun print(message: String) {
|
||||||
terminal.writer().print(message)
|
terminal.writer().print(message)
|
||||||
terminal.writer().flush()
|
terminal.writer().flush()
|
||||||
@@ -44,28 +74,31 @@ class Prompt private constructor(
|
|||||||
|
|
||||||
fun section(title: String) {
|
fun section(title: String) {
|
||||||
blank()
|
blank()
|
||||||
println("❯ $title")
|
println(cyan(bold("◆ $title")))
|
||||||
|
println(dim("─".repeat((title.length + 2).coerceAtLeast(12))))
|
||||||
|
blank()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun info(message: String) = println("[info] $message")
|
fun info(message: String) = println("${blue("ℹ")} $message")
|
||||||
|
|
||||||
fun details(title: String? = null, items: List<Pair<String, String>>) {
|
fun details(title: String? = null, items: List<Pair<String, String>>) {
|
||||||
if (items.isEmpty()) return
|
if (items.isEmpty()) return
|
||||||
|
|
||||||
if (!title.isNullOrBlank()) {
|
if (!title.isNullOrBlank()) {
|
||||||
println(title)
|
println(bold(title))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val width = items.maxOfOrNull { it.first.length } ?: 0
|
||||||
items.forEach { (key, value) ->
|
items.forEach { (key, value) ->
|
||||||
println(" $key: $value")
|
println(" ${dim(key.padEnd(width))} $value")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun success(message: String) = println("[ok] $message")
|
fun success(message: String) = println("${green("✓")} $message")
|
||||||
|
|
||||||
fun warn(message: String) = println("[warn] $message")
|
fun warn(message: String) = println("${yellow("⚠")} $message")
|
||||||
|
|
||||||
fun error(message: String) = println("[error] $message")
|
fun error(message: String) = println("${red("✗")} $message")
|
||||||
|
|
||||||
fun ask(
|
fun ask(
|
||||||
label: String,
|
label: String,
|
||||||
@@ -73,10 +106,8 @@ class Prompt private constructor(
|
|||||||
required: Boolean = true,
|
required: Boolean = true,
|
||||||
validator: ((String) -> String?)? = null,
|
validator: ((String) -> String?)? = null,
|
||||||
): String {
|
): String {
|
||||||
val suffix = if (defaultValue != null) " [$defaultValue]" else ""
|
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
val input = readLine("$label$suffix: ").trim()
|
val input = readLine(promptLabel(label, defaultValue)).trim()
|
||||||
val value = when {
|
val value = when {
|
||||||
input.isNotEmpty() -> input
|
input.isNotEmpty() -> input
|
||||||
defaultValue != null -> defaultValue
|
defaultValue != null -> defaultValue
|
||||||
@@ -96,7 +127,7 @@ class Prompt private constructor(
|
|||||||
val suffix = if (defaultValue) "[Y/n]" else "[y/N]"
|
val suffix = if (defaultValue) "[Y/n]" else "[y/N]"
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
when (readLine("$label$suffix ").trim().lowercase()) {
|
when (readLine("${questionPrefix()} $label ${dim(suffix)} ").trim().lowercase()) {
|
||||||
"" -> return defaultValue
|
"" -> return defaultValue
|
||||||
"y", "yes" -> return true
|
"y", "yes" -> return true
|
||||||
"n", "no" -> return false
|
"n", "no" -> return false
|
||||||
@@ -127,10 +158,9 @@ class Prompt private constructor(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
val suffix = defaultValue?.let { " [$it]" } ?: ""
|
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
val input = readLine(pathReader, "$label$suffix: ").trim()
|
val input = readLine(pathReader, promptLabel(label, defaultValue?.toString())).trim()
|
||||||
val rawValue = when {
|
val rawValue = when {
|
||||||
input.isNotEmpty() -> input
|
input.isNotEmpty() -> input
|
||||||
defaultValue != null -> defaultValue.toString()
|
defaultValue != null -> defaultValue.toString()
|
||||||
@@ -216,15 +246,16 @@ class Prompt private constructor(
|
|||||||
choices: List<Choice<T>>,
|
choices: List<Choice<T>>,
|
||||||
defaultIndex: Int,
|
defaultIndex: Int,
|
||||||
): T {
|
): T {
|
||||||
println(label)
|
println("${questionPrefix()} $label")
|
||||||
|
println()
|
||||||
choices.forEachIndexed { index, choice ->
|
choices.forEachIndexed { index, choice ->
|
||||||
val disabled = if (choice.enabled) "" else " (unavailable)"
|
val disabled = if (choice.enabled) "" else " ${dim("(unavailable)")}"
|
||||||
val description = choice.description?.let { " - $it" } ?: ""
|
val description = choice.description?.let { dim(" - $it") } ?: ""
|
||||||
println(" ${index + 1}. ${choice.label}$disabled$description")
|
println(" ${index + 1}. ${choice.label}$disabled$description")
|
||||||
}
|
}
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
val input = readLine("Choose [${defaultIndex + 1}]: ").trim()
|
val input = readLine(promptLabel("Choose", (defaultIndex + 1).toString())).trim()
|
||||||
val selectedIndex = if (input.isEmpty()) defaultIndex else input.toIntOrNull()?.minus(1)
|
val selectedIndex = if (input.isEmpty()) defaultIndex else input.toIntOrNull()?.minus(1)
|
||||||
|
|
||||||
if (selectedIndex != null && selectedIndex in choices.indices) {
|
if (selectedIndex != null && selectedIndex in choices.indices) {
|
||||||
@@ -250,13 +281,15 @@ class Prompt private constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val lines = buildList {
|
val lines = buildList {
|
||||||
add(label)
|
add("${questionPrefix()} $label")
|
||||||
add(" ↑/↓ move, Enter confirm")
|
add(" ${dim("↑/↓ move, Enter confirm")}")
|
||||||
|
add("")
|
||||||
choices.forEachIndexed { index, choice ->
|
choices.forEachIndexed { index, choice ->
|
||||||
val pointer = if (index == cursor) "❯" else " "
|
val pointer = if (index == cursor) cyan("❯") else " "
|
||||||
val disabled = if (choice.enabled) "" else " (unavailable)"
|
val disabled = if (choice.enabled) "" else " ${dim("(unavailable)")}"
|
||||||
val description = choice.description?.let { " - $it" } ?: ""
|
val description = choice.description?.let { dim(" - $it") } ?: ""
|
||||||
add("$pointer ${choice.label}$disabled$description")
|
val line = " $pointer ${choice.label}$disabled$description"
|
||||||
|
add(if (choice.enabled) line else dim(line))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -333,17 +366,18 @@ class Prompt private constructor(
|
|||||||
choices: List<Choice<T>>,
|
choices: List<Choice<T>>,
|
||||||
defaultSelected: Set<Int>,
|
defaultSelected: Set<Int>,
|
||||||
): List<T> {
|
): List<T> {
|
||||||
println(label)
|
println("${questionPrefix()} $label")
|
||||||
|
println()
|
||||||
choices.forEachIndexed { index, choice ->
|
choices.forEachIndexed { index, choice ->
|
||||||
val selected = if (index in defaultSelected) "x" else " "
|
val selected = if (index in defaultSelected) green("[x]") else dim("[ ]")
|
||||||
val disabled = if (choice.enabled) "" else " (unavailable)"
|
val disabled = if (choice.enabled) "" else " ${dim("(unavailable)")}"
|
||||||
val description = choice.description?.let { " - $it" } ?: ""
|
val description = choice.description?.let { dim(" - $it") } ?: ""
|
||||||
println(" ${index + 1}. [$selected] ${choice.label}$disabled$description")
|
println(" ${index + 1}. $selected ${choice.label}$disabled$description")
|
||||||
}
|
}
|
||||||
println("Enter numbers separated by comma. Leave empty to use defaults.")
|
println(" ${dim("Enter numbers separated by comma. Leave empty to use defaults.")}")
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
val input = readLine("Select: ").trim()
|
val input = readLine(promptLabel("Select")).trim()
|
||||||
val selectedIndices = if (input.isEmpty()) {
|
val selectedIndices = if (input.isEmpty()) {
|
||||||
defaultSelected
|
defaultSelected
|
||||||
} else {
|
} else {
|
||||||
@@ -378,14 +412,16 @@ class Prompt private constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val lines = buildList {
|
val lines = buildList {
|
||||||
add(label)
|
add("${questionPrefix()} $label")
|
||||||
add(" ↑/↓ move, Space toggle, Enter confirm")
|
add(" ${dim("↑/↓ move, Space toggle, Enter confirm")}")
|
||||||
|
add("")
|
||||||
choices.forEachIndexed { index, choice ->
|
choices.forEachIndexed { index, choice ->
|
||||||
val pointer = if (index == cursor) "❯" else " "
|
val pointer = if (index == cursor) cyan("❯") else " "
|
||||||
val checked = if (index in selected) "x" else " "
|
val checked = if (index in selected) green("[x]") else dim("[ ]")
|
||||||
val disabled = if (choice.enabled) "" else " (unavailable)"
|
val disabled = if (choice.enabled) "" else " ${dim("(unavailable)")}"
|
||||||
val description = choice.description?.let { " - $it" } ?: ""
|
val description = choice.description?.let { dim(" - $it") } ?: ""
|
||||||
add("$pointer [$checked] ${choice.label}$disabled$description")
|
val line = " $pointer $checked ${choice.label}$disabled$description"
|
||||||
|
add(if (choice.enabled) line else dim(line))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user