mirror of
https://github.com/slhaf/Partner.git
synced 2026-05-12 08:43:02 +08:00
feat(partnerctl): add path prompt with completion and validation
This commit is contained in:
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user