mirror of
https://github.com/slhaf/Partner.git
synced 2026-05-12 08:43:02 +08:00
feat(partnerctl): add interactive single select
This commit is contained in:
@@ -96,6 +96,63 @@ class Prompt private constructor(
|
|||||||
require(choices.isNotEmpty()) { "choices must not be empty" }
|
require(choices.isNotEmpty()) { "choices must not be empty" }
|
||||||
require(defaultIndex in choices.indices) { "defaultIndex must be within choices indices" }
|
require(defaultIndex in choices.indices) { "defaultIndex must be within choices indices" }
|
||||||
|
|
||||||
|
return if (terminal.type == "dumb") {
|
||||||
|
fallbackSelect(label, choices, defaultIndex)
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
interactiveSelect(label, choices, defaultIndex)
|
||||||
|
} catch (e: PromptCancelledException) {
|
||||||
|
throw e
|
||||||
|
} catch (_: Throwable) {
|
||||||
|
fallbackSelect(label, choices, defaultIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <T> interactiveSelect(
|
||||||
|
label: String,
|
||||||
|
choices: List<Choice<T>>,
|
||||||
|
defaultIndex: Int,
|
||||||
|
): T {
|
||||||
|
var cursor = when {
|
||||||
|
choices[defaultIndex].enabled -> defaultIndex
|
||||||
|
else -> choices.indexOfFirst { it.enabled }.takeIf { it >= 0 } ?: defaultIndex
|
||||||
|
}
|
||||||
|
var renderedLines = 0
|
||||||
|
val oldAttributes = terminal.enterRawMode()
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
renderedLines = renderSelect(label, choices, cursor, renderedLines)
|
||||||
|
|
||||||
|
when (readKey()) {
|
||||||
|
PromptKey.UP -> cursor = moveCursor(choices, cursor, -1)
|
||||||
|
PromptKey.DOWN -> cursor = moveCursor(choices, cursor, 1)
|
||||||
|
PromptKey.ENTER -> {
|
||||||
|
val choice = choices[cursor]
|
||||||
|
if (choice.enabled) {
|
||||||
|
terminal.attributes = oldAttributes
|
||||||
|
println()
|
||||||
|
return choice.value
|
||||||
|
}
|
||||||
|
beep()
|
||||||
|
}
|
||||||
|
|
||||||
|
PromptKey.CANCEL -> throw PromptCancelledException()
|
||||||
|
PromptKey.SPACE,
|
||||||
|
PromptKey.OTHER -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
terminal.attributes = oldAttributes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <T> fallbackSelect(
|
||||||
|
label: String,
|
||||||
|
choices: List<Choice<T>>,
|
||||||
|
defaultIndex: Int,
|
||||||
|
): T {
|
||||||
println(label)
|
println(label)
|
||||||
choices.forEachIndexed { index, choice ->
|
choices.forEachIndexed { index, choice ->
|
||||||
val disabled = if (choice.enabled) "" else " (unavailable)"
|
val disabled = if (choice.enabled) "" else " (unavailable)"
|
||||||
@@ -118,6 +175,33 @@ class Prompt private constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun <T> renderSelect(
|
||||||
|
label: String,
|
||||||
|
choices: List<Choice<T>>,
|
||||||
|
cursor: Int,
|
||||||
|
previousLines: Int,
|
||||||
|
): Int {
|
||||||
|
if (previousLines > 0) {
|
||||||
|
terminal.writer().print("\u001B[${previousLines}A")
|
||||||
|
terminal.writer().print("\u001B[J")
|
||||||
|
}
|
||||||
|
|
||||||
|
val lines = buildList {
|
||||||
|
add(label)
|
||||||
|
add(" ↑/↓ move, Enter confirm")
|
||||||
|
choices.forEachIndexed { index, choice ->
|
||||||
|
val pointer = if (index == cursor) "❯" else " "
|
||||||
|
val disabled = if (choice.enabled) "" else " (unavailable)"
|
||||||
|
val description = choice.description?.let { " - $it" } ?: ""
|
||||||
|
add("$pointer ${choice.label}$disabled$description")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.forEach { terminal.writer().println(it) }
|
||||||
|
terminal.writer().flush()
|
||||||
|
return lines.size
|
||||||
|
}
|
||||||
|
|
||||||
fun <T> multiSelect(
|
fun <T> multiSelect(
|
||||||
label: String,
|
label: String,
|
||||||
choices: List<Choice<T>>,
|
choices: List<Choice<T>>,
|
||||||
|
|||||||
Reference in New Issue
Block a user