Jline added. UX fixed

This commit is contained in:
2026-04-15 10:42:52 +03:00
parent f088d05731
commit 73970c192a
3 changed files with 59 additions and 53 deletions
+1 -1
View File
@@ -23,7 +23,7 @@ shadowJar {
dependencies { dependencies {
testImplementation 'org.jetbrains.kotlin:kotlin-test' testImplementation 'org.jetbrains.kotlin:kotlin-test'
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.10.0' implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.10.0'
implementation "io.ktor:ktor-network:3.4.2" implementation "org.jline:jline:3.30.6"
implementation project(':common') implementation project(':common')
} }
+56 -52
View File
@@ -1,21 +1,63 @@
package input package input
import org.jline.reader.EndOfFileException
import org.jline.reader.LineReader
import org.jline.reader.LineReaderBuilder
import org.jline.reader.UserInterruptException
import org.jline.reader.impl.history.DefaultHistory
import org.jline.terminal.TerminalBuilder
import org.jline.reader.impl.completer.StringsCompleter
import java.io.File import java.io.File
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.util.ArrayDeque import java.util.ArrayDeque
import java.util.Scanner import java.util.Scanner
import runner.CommandValidator
/** class IOManager {
* Реализация [input.IOManager] для чтения потока ввода/файла. val isInteractive: Boolean = true
* Поддерживает корректную обработку Ctrl+C и Ctrl+D. val commandValidator: CommandValidator = CommandValidator()
*/
class IOManager() {
val isInteractive = true
val inputQueue = ArrayDeque<Scanner>()
val fileHistory = ArrayDeque<String>()
init { private val terminal = TerminalBuilder.builder()
inputQueue.push(Scanner(System.`in`)) .system(true)
.build()
private val history = DefaultHistory()
private val lineReader: LineReader = LineReaderBuilder.builder()
.terminal(terminal)
.history(history)
.completer(StringsCompleter(commandValidator.getKnownCommands()))
.option(LineReader.Option.HISTORY_IGNORE_DUPS, true)
.option(LineReader.Option.DISABLE_EVENT_EXPANSION, true)
.build()
private val scriptQueue = ArrayDeque<Scanner>()
private val fileHistory = ArrayDeque<String>()
/**
* Читает строку.
* Если есть активный скрипт — читает из него.
* Иначе — интерактивный ввод через JLine.
*/
fun readLine(prompt: String): String? {
// Режим скрипта
while (scriptQueue.isNotEmpty()) {
val scanner = scriptQueue.peek()
if (scanner.hasNextLine()) {
return scanner.nextLine()
}
scanner.close()
scriptQueue.pop()
fileHistory.pop()
}
return try {
lineReader.readLine(prompt)
} catch (e: UserInterruptException) {
""
} catch (e: EndOfFileException) {
null
}
} }
fun addScriptScanner(filePath: String) { fun addScriptScanner(filePath: String) {
@@ -23,53 +65,15 @@ class IOManager() {
printErrConsole("Скрипт уже был выполнен: $filePath") printErrConsole("Скрипт уже был выполнен: $filePath")
return return
} }
try { try {
fileHistory.push(filePath) fileHistory.push(filePath)
inputQueue.push(Scanner(File(filePath), Charsets.UTF_8)) scriptQueue.push(Scanner(File(filePath), Charsets.UTF_8))
} catch (e: FileNotFoundException) { } catch (e: FileNotFoundException) {
printErrConsole("Файл не найден: $filePath") printErrConsole("Файл не найден: $filePath")
} }
} }
// сделать основной Scanner fun print(text: String) = println(text)
// при инициализации стандартный сканер добавляется
// стек input, один (console) открыт постоянно
// если файл - в стек кидается Scanner
/** fun printErrConsole(text: String) = System.err.println(text)
* Читает строку с промптом ☭. }
*/
fun readLine(prompt: String): String? {
while (inputQueue.isNotEmpty()) {
if (inputQueue.size == 1) kotlin.io.print(prompt)
val topInDeque = inputQueue.peek()
if (topInDeque.hasNextLine()) {
return topInDeque.nextLine().toString()
}
if (inputQueue.size > 1) {
topInDeque.close()
inputQueue.pop()
fileHistory.pop()
// snap back to reality
continue
}
inputQueue.pop()
print("Ввод закончен.")
break
}
return null
}
fun print(text: String) {
println(text)
}
fun printErrConsole(text: String) {
System.err.println(text)
}
}
@@ -18,6 +18,8 @@ class CommandValidator {
"exit" "exit"
) )
fun getKnownCommands(): Set<String> {return knownCommands}
fun validate(command: String, args: List<String>?): Pair<Boolean, String?> { fun validate(command: String, args: List<String>?): Pair<Boolean, String?> {
if (command !in knownCommands){ if (command !in knownCommands){
return Pair(false, "Неизвестная команда: '$command'. Введите 'help' для справки.") return Pair(false, "Неизвестная команда: '$command'. Введите 'help' для справки.")