feat(partnerctl): add path prompt with completion and validation

This commit is contained in:
2026-05-03 19:27:07 +08:00
parent 6f48c36f67
commit 729700ceb7
2 changed files with 83 additions and 0 deletions

View File

@@ -1,11 +1,16 @@
package work.slhaf.partner.ctl.ui package work.slhaf.partner.ctl.ui
import org.jline.builtins.Completers.DirectoriesCompleter
import org.jline.builtins.Completers.FilesCompleter
import org.jline.reader.EndOfFileException import org.jline.reader.EndOfFileException
import org.jline.reader.LineReader import org.jline.reader.LineReader
import org.jline.reader.LineReaderBuilder 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 java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
class Prompt private constructor( class Prompt private constructor(
private val terminal: Terminal, private val terminal: Terminal,
@@ -88,6 +93,52 @@ class Prompt private constructor(
} }
} }
fun askPath(
label: String,
defaultValue: Path? = null,
required: Boolean = true,
mustExist: Boolean = false,
directoryOnly: Boolean = false,
fileOnly: Boolean = false,
currentDir: Path = Paths.get(System.getProperty("user.dir") ?: "."),
): Path {
require(!(directoryOnly && fileOnly)) { "directoryOnly and fileOnly cannot both be true" }
val normalizedCurrentDir = currentDir.toAbsolutePath().normalize()
val pathReader = LineReaderBuilder.builder()
.terminal(terminal)
.completer(
if (directoryOnly) {
DirectoriesCompleter(normalizedCurrentDir)
} else {
FilesCompleter(normalizedCurrentDir)
}
)
.build()
val suffix = defaultValue?.let { " [$it]" } ?: ""
while (true) {
val input = readLine(pathReader, "$label$suffix: ").trim()
val rawValue = when {
input.isNotEmpty() -> input
defaultValue != null -> defaultValue.toString()
!required -> ""
else -> {
error("This value is required.")
continue
}
}
if (rawValue.isBlank() && !required) {
return Paths.get("")
}
val path = expandPath(rawValue)
val validationError = validatePath(path, mustExist, directoryOnly, fileOnly) ?: return path
error(validationError)
}
}
fun <T> select( fun <T> select(
label: String, label: String,
choices: List<Choice<T>>, choices: List<Choice<T>>,
@@ -373,6 +424,10 @@ class Prompt private constructor(
} }
private fun readLine(prompt: String): String { private fun readLine(prompt: String): String {
return readLine(reader, prompt)
}
private fun readLine(reader: LineReader, prompt: String): String {
return try { return try {
reader.readLine(prompt) reader.readLine(prompt)
} catch (_: UserInterruptException) { } catch (_: UserInterruptException) {
@@ -382,6 +437,28 @@ class Prompt private constructor(
} }
} }
private fun expandPath(value: String): Path {
return when {
value == "~" -> Paths.get(System.getProperty("user.home") ?: ".")
value.startsWith("~/") -> Paths.get(System.getProperty("user.home") ?: ".", value.substring(2))
else -> Paths.get(value)
}.toAbsolutePath().normalize()
}
private fun validatePath(
path: Path,
mustExist: Boolean,
directoryOnly: Boolean,
fileOnly: Boolean,
): String? {
return when {
mustExist && !Files.exists(path) -> "Path does not exist: $path"
directoryOnly && Files.exists(path) && !Files.isDirectory(path) -> "Path is not a directory: $path"
fileOnly && Files.exists(path) && !Files.isRegularFile(path) -> "Path is not a regular file: $path"
else -> null
}
}
private fun parseIndexList(input: String, size: Int): Set<Int>? { private fun parseIndexList(input: String, size: Int): Set<Int>? {
if (input.isBlank()) return emptySet() if (input.isBlank()) return emptySet()

View File

@@ -111,6 +111,12 @@ fun main() {
) )
prompt.info("Selected modules = ${modules.joinToString()}") prompt.info("Selected modules = ${modules.joinToString()}")
prompt.section("Ask path")
val askPath = prompt.askPath(
label = "Ask path",
)
prompt.info("Ask path = $askPath")
prompt.section("Done") prompt.section("Done")
prompt.success("Prompt demo completed.") prompt.success("Prompt demo completed.")
} catch (_: work.slhaf.partner.ctl.ui.PromptCancelledException) { } catch (_: work.slhaf.partner.ctl.ui.PromptCancelledException) {