From d5c221898467eeb1c51027ab437f9f0760a57d21 Mon Sep 17 00:00:00 2001 From: Jacob Persson <7156+typfel@users.noreply.github.com> Date: Wed, 2 Oct 2024 18:01:06 +0200 Subject: [PATCH] feat: support interactive command on all platforms --- cli/build.gradle.kts | 2 + .../wire/kalium/cli/commands/ActionFlow.kt | 111 ------------ .../com/wire/kalium/cli/commands/InputFlow.kt | 168 +++++++++--------- .../com/wire/kalium/cli/commands/RawMode.kt | 70 ++++---- .../cli/commands/interactive/ActionFlow.kt | 114 ++++++++++++ .../cli/commands/interactive}/Command.kt | 4 +- .../cli/commands/interactive}/InputAction.kt | 2 +- .../interactive}/InteractiveCommand.kt | 55 +----- .../cli/commands/interactive}/Widgets.kt | 26 ++- .../kotlin/com/wire/kalium/cli/main.kt | 4 +- gradle/libs.versions.toml | 3 +- 11 files changed, 275 insertions(+), 284 deletions(-) delete mode 100644 cli/src/appleMain/kotlin/com/wire/kalium/cli/commands/ActionFlow.kt create mode 100644 cli/src/commonMain/kotlin/com/wire/kalium/cli/commands/interactive/ActionFlow.kt rename cli/src/{appleMain/kotlin/com/wire/kalium/cli/commands => commonMain/kotlin/com/wire/kalium/cli/commands/interactive}/Command.kt (97%) rename cli/src/{appleMain/kotlin/com/wire/kalium/cli/commands => commonMain/kotlin/com/wire/kalium/cli/commands/interactive}/InputAction.kt (95%) rename cli/src/{appleMain/kotlin/com/wire/kalium/cli/commands => commonMain/kotlin/com/wire/kalium/cli/commands/interactive}/InteractiveCommand.kt (81%) rename cli/src/{appleMain/kotlin/com/wire/kalium/cli/commands => commonMain/kotlin/com/wire/kalium/cli/commands/interactive}/Widgets.kt (81%) diff --git a/cli/build.gradle.kts b/cli/build.gradle.kts index 244c656e822..ff2f9c44009 100644 --- a/cli/build.gradle.kts +++ b/cli/build.gradle.kts @@ -44,6 +44,7 @@ tasks.jar { kotlin { applyDefaultHierarchyTemplate() val jvmTarget = jvm { + withJava() commonJvmConfig(includeNativeInterop = false) tasks.named("run", JavaExec::class) { isIgnoreExitValue = true @@ -75,6 +76,7 @@ kotlin { implementation(libs.coroutines.core) implementation(libs.ktxDateTime) implementation(libs.mordant) + implementation(libs.mordant.coroutines) implementation(libs.ktxSerialization) implementation(libs.ktxIO) } diff --git a/cli/src/appleMain/kotlin/com/wire/kalium/cli/commands/ActionFlow.kt b/cli/src/appleMain/kotlin/com/wire/kalium/cli/commands/ActionFlow.kt deleted file mode 100644 index a6992e9f200..00000000000 --- a/cli/src/appleMain/kotlin/com/wire/kalium/cli/commands/ActionFlow.kt +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Wire - * Copyright (C) 2024 Wire Swiss GmbH - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see http://www.gnu.org/licenses/. - */ -package com.wire.kalium.cli.commands - -import com.wire.kalium.logic.feature.UserSessionScope -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.mapNotNull -import kotlinx.coroutines.flow.merge - -@Suppress("ComplexMethod", "LongMethod") -internal fun actionFlow(userSession: UserSessionScope): Flow { - var command: Command? = null - val draftBuffer = StringBuilder() - var cursorPosition = 0 - - return merge( - flowOf(InputAction.UpdateDraft(draftBuffer.toString(), cursorPosition)), - inputFlow().mapNotNull { - if (it == Input.Character('q')) { - InputAction.Quit - } else { - when (it) { - Input.Character('\n') -> { - val message = draftBuffer.toString() - draftBuffer.clear() - cursorPosition = 0 - - command?.let { - InputAction.RunCommand(it) - } ?: run { - InputAction.SendText(message) - } - } - - Input.Character('\t') -> { - command?.nextResult() - InputAction.UpdateDraft(draftBuffer.toString(), cursorPosition, command?.resultDescription()) - } - - is Input.Character -> { - draftBuffer.insert(cursorPosition, it.char) - val message = draftBuffer.toString() - - if (message.startsWith("/")) { - val components = message.split(" ", limit = 2) - if (components.size == 2) { - val action = components[0].drop(1) - val query = components[1] - - command = command?.let { command -> - if (command.name == action) { - command.also { command.query = query } - } else { - null - } - } ?: run { - Command.find(action, query, userSession) - } - } - } - - InputAction.UpdateDraft(draftBuffer.toString(), ++cursorPosition, command?.resultDescription()) - } - - is Input.ArrowLeft -> { - if (cursorPosition > 0) { - InputAction.UpdateDraft(draftBuffer.toString(), --cursorPosition) - } else { - null - } - } - - is Input.ArrowRight -> { - if (cursorPosition < draftBuffer.length) { - InputAction.UpdateDraft(draftBuffer.toString(), ++cursorPosition) - } else { - null - } - } - - is Input.Backspace, is Input.DeleteKey -> { - if (!draftBuffer.isEmpty()) { - draftBuffer.deleteAt(--cursorPosition) - InputAction.UpdateDraft(draftBuffer.toString(), cursorPosition) - } else { - null - } - } - - else -> null - } - } - } - ) -} diff --git a/cli/src/appleMain/kotlin/com/wire/kalium/cli/commands/InputFlow.kt b/cli/src/appleMain/kotlin/com/wire/kalium/cli/commands/InputFlow.kt index 7187838b3b6..9872e04b635 100644 --- a/cli/src/appleMain/kotlin/com/wire/kalium/cli/commands/InputFlow.kt +++ b/cli/src/appleMain/kotlin/com/wire/kalium/cli/commands/InputFlow.kt @@ -17,88 +17,88 @@ */ package com.wire.kalium.cli.commands -import kotlinx.cinterop.ByteVar -import kotlinx.cinterop.addressOf -import kotlinx.cinterop.alloc -import kotlinx.cinterop.memScoped -import kotlinx.cinterop.ptr -import kotlinx.cinterop.usePinned -import kotlinx.cinterop.value -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.flowOn -import platform.posix.STDIN_FILENO -import platform.posix.read -import platform.posix.ssize_t +// import kotlinx.cinterop.ByteVar +// import kotlinx.cinterop.addressOf +// import kotlinx.cinterop.alloc +// import kotlinx.cinterop.memScoped +// import kotlinx.cinterop.ptr +// import kotlinx.cinterop.usePinned +// import kotlinx.cinterop.value +// import kotlinx.coroutines.Dispatchers +// import kotlinx.coroutines.delay +// import kotlinx.coroutines.flow.Flow +// import kotlinx.coroutines.flow.flow +// import kotlinx.coroutines.flow.flowOn +// import platform.posix.STDIN_FILENO +// import platform.posix.read +// import platform.posix.ssize_t -@Suppress("MagicNumber") -fun inputFlow(): Flow = flow { - while (true) { - emit(readChar()) - delay(100) // TODO jacob avoid this hack by enabling read timeout - } -}.flowOn(Dispatchers.Default) - -@Suppress("ComplexMethod", "TooGenericExceptionThrown", "MagicNumber") -fun readChar(): Input = - memScoped { - val byte = alloc() - var numBytesRead: ssize_t - while (read(STDIN_FILENO, byte.ptr, 1).also { numBytesRead = it } != 1L) { - if (numBytesRead == -1L) { throw RuntimeException("Failed to read input") } - } - - val char = byte.value.toInt().toChar() - if (char == '\u001b') { - val sequence = ByteArray(3) - sequence.usePinned { - if (read(STDIN_FILENO, it.addressOf(0), 1) != 1L) { return Input.Character('\u001b') } - if (read(STDIN_FILENO, it.addressOf(1), 1) != 1L) { return Input.Character('\u001b') } - } - - if (sequence[0].toInt().toChar() == '[') { - when (sequence[1].toInt().toChar()) { - in '0'..'9' -> { - sequence.usePinned { - if (read(STDIN_FILENO, it.addressOf(2), 1) != 1L) { return Input.Character('\u001b') } - } - if (sequence[2].toInt().toChar() == '~') { - when (sequence[1].toInt().toChar()) { - '1', '7' -> return Input.HomeKey - '3', '8' -> return Input.DeleteKey - '4' -> return Input.EndKey - '5' -> return Input.PageUp - '6' -> return Input.PageDown - } - } - } - 'A' -> return Input.ArrowUp - 'B' -> return Input.ArrowDown - 'C' -> return Input.ArrowRight - 'D' -> return Input.ArrowLeft - } - } - } - - @Suppress("MagicNumber") - when (char.code) { - 127 -> return Input.Backspace - else -> return Input.Character(char) - } - } - -sealed class Input { - data class Character(val char: Char) : Input() - data object ArrowUp : Input() - data object ArrowDown : Input() - data object ArrowLeft : Input() - data object ArrowRight : Input() - data object HomeKey : Input() - data object EndKey : Input() - data object DeleteKey : Input() - data object PageUp : Input() - data object PageDown : Input() - data object Backspace : Input() -} +// @Suppress("MagicNumber") +// fun inputFlow(): Flow = flow { +// while (true) { +// emit(readChar()) +// delay(100) // TODO jacob avoid this hack by enabling read timeout +// } +// }.flowOn(Dispatchers.Default) +// +// @Suppress("ComplexMethod", "TooGenericExceptionThrown", "MagicNumber") +// fun readChar(): Input = +// memScoped { +// val byte = alloc() +// var numBytesRead: ssize_t +// while (read(STDIN_FILENO, byte.ptr, 1).also { numBytesRead = it } != 1L) { +// if (numBytesRead == -1L) { throw RuntimeException("Failed to read input") } +// } +// +// val char = byte.value.toInt().toChar() +// if (char == '\u001b') { +// val sequence = ByteArray(3) +// sequence.usePinned { +// if (read(STDIN_FILENO, it.addressOf(0), 1) != 1L) { return Input.Character('\u001b') } +// if (read(STDIN_FILENO, it.addressOf(1), 1) != 1L) { return Input.Character('\u001b') } +// } +// +// if (sequence[0].toInt().toChar() == '[') { +// when (sequence[1].toInt().toChar()) { +// in '0'..'9' -> { +// sequence.usePinned { +// if (read(STDIN_FILENO, it.addressOf(2), 1) != 1L) { return Input.Character('\u001b') } +// } +// if (sequence[2].toInt().toChar() == '~') { +// when (sequence[1].toInt().toChar()) { +// '1', '7' -> return Input.HomeKey +// '3', '8' -> return Input.DeleteKey +// '4' -> return Input.EndKey +// '5' -> return Input.PageUp +// '6' -> return Input.PageDown +// } +// } +// } +// 'A' -> return Input.ArrowUp +// 'B' -> return Input.ArrowDown +// 'C' -> return Input.ArrowRight +// 'D' -> return Input.ArrowLeft +// } +// } +// } +// +// @Suppress("MagicNumber") +// when (char.code) { +// 127 -> return Input.Backspace +// else -> return Input.Character(char) +// } +// } +// +// sealed class Input { +// data class Character(val char: Char) : Input() +// data object ArrowUp : Input() +// data object ArrowDown : Input() +// data object ArrowLeft : Input() +// data object ArrowRight : Input() +// data object HomeKey : Input() +// data object EndKey : Input() +// data object DeleteKey : Input() +// data object PageUp : Input() +// data object PageDown : Input() +// data object Backspace : Input() +// } diff --git a/cli/src/appleMain/kotlin/com/wire/kalium/cli/commands/RawMode.kt b/cli/src/appleMain/kotlin/com/wire/kalium/cli/commands/RawMode.kt index 8c5016bfef7..2734730565b 100644 --- a/cli/src/appleMain/kotlin/com/wire/kalium/cli/commands/RawMode.kt +++ b/cli/src/appleMain/kotlin/com/wire/kalium/cli/commands/RawMode.kt @@ -17,39 +17,39 @@ */ package com.wire.kalium.cli.commands -import com.github.ajalt.mordant.terminal.Terminal -import kotlinx.cinterop.alloc -import kotlinx.cinterop.convert -import kotlinx.cinterop.memScoped -import kotlinx.cinterop.ptr -import platform.posix.ECHO -import platform.posix.ICANON -import platform.posix.STDOUT_FILENO -import platform.posix.TCSAFLUSH -import platform.posix.tcgetattr -import platform.posix.tcsetattr -import platform.posix.termios +// import com.github.ajalt.mordant.terminal.Terminal +// import kotlinx.cinterop.alloc +// import kotlinx.cinterop.convert +// import kotlinx.cinterop.memScoped +// import kotlinx.cinterop.ptr +// import platform.posix.ECHO +// import platform.posix.ICANON +// import platform.posix.STDOUT_FILENO +// import platform.posix.TCSAFLUSH +// import platform.posix.tcgetattr +// import platform.posix.tcsetattr +// import platform.posix.termios -fun Terminal.setRawMode(enabled: Boolean) = memScoped { - val termios = alloc() - if (tcgetattr(STDOUT_FILENO, termios.ptr) != 0) { - return@memScoped - } - - if (enabled) { - termios.c_lflag = termios.c_lflag and ICANON.inv().convert() - termios.c_lflag = termios.c_lflag and ECHO.inv().convert() - } else { - termios.c_lflag = termios.c_lflag or ICANON.convert() - termios.c_lflag = termios.c_lflag or ECHO.convert() - } - - tcsetattr(0, TCSAFLUSH, termios.ptr) -} - -inline fun Terminal.withRawMode(block: () -> T): T { - setRawMode(true) - val result = block() - setRawMode(false) - return result -} +// fun Terminal.setRawMode(enabled: Boolean) = memScoped { +// val termios = alloc() +// if (tcgetattr(STDOUT_FILENO, termios.ptr) != 0) { +// return@memScoped +// } +// +// if (enabled) { +// termios.c_lflag = termios.c_lflag and ICANON.inv().convert() +// termios.c_lflag = termios.c_lflag and ECHO.inv().convert() +// } else { +// termios.c_lflag = termios.c_lflag or ICANON.convert() +// termios.c_lflag = termios.c_lflag or ECHO.convert() +// } +// +// tcsetattr(0, TCSAFLUSH, termios.ptr) +// } +// +// inline fun Terminal.withRawMode(block: () -> T): T { +// setRawMode(true) +// val result = block() +// setRawMode(false) +// return result +// } diff --git a/cli/src/commonMain/kotlin/com/wire/kalium/cli/commands/interactive/ActionFlow.kt b/cli/src/commonMain/kotlin/com/wire/kalium/cli/commands/interactive/ActionFlow.kt new file mode 100644 index 00000000000..b10fdf1f629 --- /dev/null +++ b/cli/src/commonMain/kotlin/com/wire/kalium/cli/commands/interactive/ActionFlow.kt @@ -0,0 +1,114 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.cli.commands.interactive + +import com.github.ajalt.mordant.input.coroutines.receiveKeyEventsFlow +import com.github.ajalt.mordant.input.isCtrlC +import com.github.ajalt.mordant.terminal.Terminal +import com.wire.kalium.logic.feature.UserSessionScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.merge + +@Suppress("ComplexMethod", "LongMethod") +internal fun actionFlow(userSession: UserSessionScope, terminal: Terminal): Flow { + var command: Command? = null + val draftBuffer = StringBuilder() + var cursorPosition = 0 + + return merge( + flowOf(InputAction.UpdateDraft(draftBuffer.toString(), cursorPosition)), + terminal.receiveKeyEventsFlow() + .mapNotNull { event -> + if (event.isCtrlC) { + return@mapNotNull InputAction.Quit + } + + when (event.key) { + "Enter" -> { + val message = draftBuffer.toString() + draftBuffer.clear() + cursorPosition = 0 + + command?.let { + InputAction.RunCommand(it) + } ?: run { + InputAction.SendText(message) + } + } + + "Tab" -> { + command?.nextResult() + InputAction.UpdateDraft(draftBuffer.toString(), cursorPosition, command?.resultDescription()) + } + + "ArrowLeft" -> { + if (cursorPosition > 0) { + InputAction.UpdateDraft(draftBuffer.toString(), --cursorPosition) + } else { + null + } + } + + "ArrowRight" -> { + if (cursorPosition < draftBuffer.length) { + InputAction.UpdateDraft(draftBuffer.toString(), ++cursorPosition) + } else { + null + } + } + + "Backspace", "Delete" -> { + if (!draftBuffer.isEmpty()) { + draftBuffer.deleteAt(--cursorPosition) + InputAction.UpdateDraft(draftBuffer.toString(), cursorPosition) + } else { + null + } + } + + else -> { + draftBuffer.insert(cursorPosition, event.key) + val message = draftBuffer.toString() + + if (message.startsWith("/")) { + val components = message.split(" ", limit = 2) + if (components.size == 2) { + val action = components[0].drop(1) + val query = components[1] + + command = command?.let { command -> + if (command.name == action) { + command.also { command.query = query } + } else { + null + } + } ?: run { + Command.find(action, query, userSession) + } + } + } + + InputAction.UpdateDraft(draftBuffer.toString(), ++cursorPosition, command?.resultDescription()) + } + + } + } + ) +} diff --git a/cli/src/appleMain/kotlin/com/wire/kalium/cli/commands/Command.kt b/cli/src/commonMain/kotlin/com/wire/kalium/cli/commands/interactive/Command.kt similarity index 97% rename from cli/src/appleMain/kotlin/com/wire/kalium/cli/commands/Command.kt rename to cli/src/commonMain/kotlin/com/wire/kalium/cli/commands/interactive/Command.kt index 4fa42ddfb86..7ec769b73e1 100644 --- a/cli/src/appleMain/kotlin/com/wire/kalium/cli/commands/Command.kt +++ b/cli/src/commonMain/kotlin/com/wire/kalium/cli/commands/interactive/Command.kt @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see http://www.gnu.org/licenses/. */ -package com.wire.kalium.cli.commands +package com.wire.kalium.cli.commands.interactive import com.wire.kalium.logic.data.conversation.ConversationDetails import com.wire.kalium.logic.data.message.UnreadEventType @@ -45,7 +45,7 @@ sealed class Command( } suspend fun prepare() { - conversations = userSession.conversations.observeConversationListDetails(includeArchived = true).first() + conversations = userSession.conversations.observeConversationListDetails(fromArchive = false).first() updateFilter() } diff --git a/cli/src/appleMain/kotlin/com/wire/kalium/cli/commands/InputAction.kt b/cli/src/commonMain/kotlin/com/wire/kalium/cli/commands/interactive/InputAction.kt similarity index 95% rename from cli/src/appleMain/kotlin/com/wire/kalium/cli/commands/InputAction.kt rename to cli/src/commonMain/kotlin/com/wire/kalium/cli/commands/interactive/InputAction.kt index 864f31ea4fd..c86eb7c2a22 100644 --- a/cli/src/appleMain/kotlin/com/wire/kalium/cli/commands/InputAction.kt +++ b/cli/src/commonMain/kotlin/com/wire/kalium/cli/commands/interactive/InputAction.kt @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see http://www.gnu.org/licenses/. */ -package com.wire.kalium.cli.commands +package com.wire.kalium.cli.commands.interactive sealed class InputAction { data class UpdateDraft( diff --git a/cli/src/appleMain/kotlin/com/wire/kalium/cli/commands/InteractiveCommand.kt b/cli/src/commonMain/kotlin/com/wire/kalium/cli/commands/interactive/InteractiveCommand.kt similarity index 81% rename from cli/src/appleMain/kotlin/com/wire/kalium/cli/commands/InteractiveCommand.kt rename to cli/src/commonMain/kotlin/com/wire/kalium/cli/commands/interactive/InteractiveCommand.kt index c3de92e8da2..eed64a3bc20 100644 --- a/cli/src/appleMain/kotlin/com/wire/kalium/cli/commands/InteractiveCommand.kt +++ b/cli/src/commonMain/kotlin/com/wire/kalium/cli/commands/interactive/InteractiveCommand.kt @@ -15,28 +15,24 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see http://www.gnu.org/licenses/. */ -package com.wire.kalium.cli.commands +package com.wire.kalium.cli.commands.interactive import com.github.ajalt.clikt.core.CliktCommand import com.github.ajalt.clikt.core.PrintMessage import com.github.ajalt.clikt.core.requireObject +import com.github.ajalt.mordant.input.enterRawMode import com.github.ajalt.mordant.terminal.Terminal +import com.github.ajalt.mordant.terminal.prompt import com.wire.kalium.cli.listConversations import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.message.Message import com.wire.kalium.logic.feature.UserSessionScope import com.wire.kalium.logic.feature.conversation.GetConversationUseCase -import kotlinx.cinterop.alloc -import kotlinx.cinterop.memScoped -import kotlinx.cinterop.ptr -import kotlinx.cinterop.set -import kotlinx.cinterop.staticCFunction import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.cancel -import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filterIsInstance @@ -45,21 +41,6 @@ import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking -import platform.posix.SIGWINCH -import platform.posix.STDIN_FILENO -import platform.posix.TCSAFLUSH -import platform.posix.VMIN -import platform.posix.VTIME -import platform.posix.signal -import platform.posix.tcgetattr -import platform.posix.tcsetattr -import platform.posix.termios - -sealed class PosixSignal { - data object WindowChanged : PosixSignal() -} - -private val signalFlow = MutableSharedFlow() data class ViewState( val title: String, @@ -91,7 +72,7 @@ class InteractiveCommand : CliktCommand(name = "interactive") { viewState.inputInfo, viewState.title, viewState.messages, - terminal.info.height + terminal.size.height ) ) ) @@ -106,15 +87,6 @@ class InteractiveCommand : CliktCommand(name = "interactive") { } override fun run() = runBlocking { - signal( - SIGWINCH, - staticCFunction { - runBlocking { - signalFlow.emit(PosixSignal.WindowChanged) - } - } - ) - while (!finished) { displayConversation(currentConversationId ?: userSession.selectConversation().id) } @@ -130,9 +102,9 @@ class InteractiveCommand : CliktCommand(name = "interactive") { return conversations[selectedConversationIndex] } - @OptIn(DelicateCoroutinesApi::class) + @OptIn(DelicateCoroutinesApi::class, ExperimentalStdlibApi::class) private suspend fun displayConversation(conversationId: ConversationId) { - terminal.withRawMode { + terminal.enterRawMode().use { _ -> terminal.cursor.move { setPosition(0, 0) clearScreen() @@ -143,7 +115,7 @@ class InteractiveCommand : CliktCommand(name = "interactive") { userSession.messages.getRecentMessages(conversationId, limit = 100), userSession.conversations.getConversationDetails(conversationId) .mapNotNull { if (it is GetConversationUseCase.Result.Success) it.conversation.name else null }, - actionFlow(userSession) + actionFlow(userSession, terminal) .onEach { when (it) { is InputAction.Quit -> { @@ -183,17 +155,4 @@ class InteractiveCommand : CliktCommand(name = "interactive") { }.join() } } - - @Suppress("UnusedPrivateMember") - private fun enableReadTimeout() = memScoped { - val termios = alloc() - if (tcgetattr(STDIN_FILENO, termios.ptr) != 0) { - return@memScoped - } - - termios.c_cc[VMIN] = 0u - termios.c_cc[VTIME] = 1u - - tcsetattr(STDIN_FILENO, TCSAFLUSH, termios.ptr) - } } diff --git a/cli/src/appleMain/kotlin/com/wire/kalium/cli/commands/Widgets.kt b/cli/src/commonMain/kotlin/com/wire/kalium/cli/commands/interactive/Widgets.kt similarity index 81% rename from cli/src/appleMain/kotlin/com/wire/kalium/cli/commands/Widgets.kt rename to cli/src/commonMain/kotlin/com/wire/kalium/cli/commands/interactive/Widgets.kt index 2556c3cd665..be7d570c237 100644 --- a/cli/src/appleMain/kotlin/com/wire/kalium/cli/commands/Widgets.kt +++ b/cli/src/commonMain/kotlin/com/wire/kalium/cli/commands/interactive/Widgets.kt @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see http://www.gnu.org/licenses/. */ -package com.wire.kalium.cli.commands +package com.wire.kalium.cli.commands.interactive import com.github.ajalt.mordant.rendering.Lines import com.github.ajalt.mordant.rendering.OverflowWrap @@ -126,6 +126,8 @@ private fun regularContent(message: Message.Regular) = is MessageContent.Knock -> textMessage(message.senderUserName, "") is MessageContent.RestrictedAsset -> textMessage(message.senderUserName, "Shared an asset") is MessageContent.Unknown -> systemMessage(message.senderUserName, "Unknown message") + is MessageContent.Composite -> textMessage(message.senderUserName, "") + is MessageContent.Location -> textMessage(message.senderUserName, "Shared an location: ${content.name}") } private fun systemContent(message: Message.System) = @@ -151,6 +153,28 @@ private fun systemContent(message: Message.System) = systemMessage(null, "Read receipts are ${if (content.receiptMode) "enabled" else "disabled" }") is MessageContent.TeamMemberRemoved -> systemMessage(null, "${content.userName} was removed from the team") + + MessageContent.ConversationCreated -> TODO() + MessageContent.ConversationDegradedMLS -> TODO() + MessageContent.ConversationDegradedProteus -> TODO() + is MessageContent.ConversationMessageTimerChanged -> TODO() + is MessageContent.ConversationProtocolChanged -> TODO() + MessageContent.ConversationProtocolChangedDuringACall -> TODO() + MessageContent.ConversationStartedUnverifiedWarning -> TODO() + MessageContent.ConversationVerifiedMLS -> TODO() + MessageContent.ConversationVerifiedProteus -> TODO() + is MessageContent.FederationStopped.ConnectionRemoved -> TODO() + is MessageContent.FederationStopped.Removed -> TODO() + MessageContent.HistoryLostProtocolChanged -> TODO() + MessageContent.LegalHold.ForConversation.Disabled -> TODO() + MessageContent.LegalHold.ForConversation.Enabled -> TODO() + is MessageContent.LegalHold.ForMembers.Disabled -> TODO() + is MessageContent.LegalHold.ForMembers.Enabled -> TODO() + MessageContent.MLSWrongEpochWarning -> TODO() + is MessageContent.MemberChange.CreationAdded -> TODO() + is MessageContent.MemberChange.FailedToAdd -> TODO() + is MessageContent.MemberChange.FederationRemoved -> TODO() + is MessageContent.MemberChange.RemovedFromTeam -> TODO() } @Suppress("MagicNumber") diff --git a/cli/src/jvmMain/kotlin/com/wire/kalium/cli/main.kt b/cli/src/jvmMain/kotlin/com/wire/kalium/cli/main.kt index 54c196bf100..7b5b11d60fd 100644 --- a/cli/src/jvmMain/kotlin/com/wire/kalium/cli/main.kt +++ b/cli/src/jvmMain/kotlin/com/wire/kalium/cli/main.kt @@ -27,6 +27,7 @@ import com.wire.kalium.cli.commands.LoginCommand import com.wire.kalium.cli.commands.MarkAsReadCommand import com.wire.kalium.cli.commands.ConsoleCommand import com.wire.kalium.cli.commands.GenerateEventsCommand +import com.wire.kalium.cli.commands.interactive.InteractiveCommand import com.wire.kalium.cli.commands.RefillKeyPackagesCommand import com.wire.kalium.cli.commands.RemoveMemberFromGroupCommand import com.wire.kalium.cli.commands.UpdateSupportedProtocolsCommand @@ -42,6 +43,7 @@ fun main(args: Array) = CLIApplication().subcommands( RefillKeyPackagesCommand(), MarkAsReadCommand(), UpdateSupportedProtocolsCommand(), - GenerateEventsCommand() + GenerateEventsCommand(), + InteractiveCommand() ) ).main(args) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7597114ff62..a31289e3f51 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -51,7 +51,7 @@ carthage = "0.0.1" libsodiumBindings = "0.8.7" protobufCodegen = "0.9.4" annotation = "1.7.1" -mordant = "2.0.0-beta13" +mordant = "3.0.0" apache-tika = "2.9.2" mockk = "1.13.10" faker = "1.16.0" @@ -148,6 +148,7 @@ libsodiumBindingsMP = { module = "com.ionspin.kotlin:multiplatform-crypto-libsod cliKt = { module = "com.github.ajalt.clikt:clikt", version.ref = "cli-kt" } okhttp-loggingInterceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "ok-http" } mordant = { module = "com.github.ajalt.mordant:mordant", version.ref = "mordant" } +mordant-coroutines = { module = "com.github.ajalt.mordant:mordant-coroutines", version.ref = "mordant" } # ktor ktor-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }