Repository: ajalt/mordant (kotlin)

Issue:
Selected value for interactiveSelectList
`Terminal.interactiveSelectList()` returns the visible title of the selected entry. This is not always convenient. It would be useful to specify a (hidden) value in each entry which is returned instead of the title (a bit like HTML &lt;option value="..."&gt;).

This could be added to `SelectList.Entry`:

```kotlin
    data class Entry(
        /** The title of the entry. */
        val title: String,
        /** An optional description of the entry. */
        val description: Widget? = null,
        /** Whether this entry is marked as selected. */
        val selected: Boolean = false,
        /** Return this value instead of title if not null. */
        val value: String? = null,
       )
```

Pull request:
Fixes #245

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 88db1f2a..823aca63 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,8 @@
 # Changelog
 
 ## Unreleased
+### Added
+- Possibility to specify hidden return value in `Terminal.interactiveSelectList`, `Terminal.interactiveMultiSelectList`, and `InteractiveSelectListBuilder`.
 
 ## 3.0.2
 ### Changed
diff --git a/mordant/api/mordant.api b/mordant/api/mordant.api
index a6d5b9a4..971f80c1 100644
--- a/mordant/api/mordant.api
+++ b/mordant/api/mordant.api
@@ -212,10 +212,14 @@ public final class com/github/ajalt/mordant/input/InteractiveSelectListBuilder {
 	public fun <init> (Lcom/github/ajalt/mordant/terminal/Terminal;)V
 	public final fun addEntry (Lcom/github/ajalt/mordant/widgets/SelectList$Entry;)Lcom/github/ajalt/mordant/input/InteractiveSelectListBuilder;
 	public final fun addEntry (Ljava/lang/String;)Lcom/github/ajalt/mordant/input/InteractiveSelectListBuilder;
+	public final fun addEntry (Ljava/lang/String;Lcom/github/ajalt/mordant/rendering/Widget;)Lcom/github/ajalt/mordant/input/InteractiveSelectListBuilder;
 	public final fun addEntry (Ljava/lang/String;Lcom/github/ajalt/mordant/rendering/Widget;Z)Lcom/github/ajalt/mordant/input/InteractiveSelectListBuilder;
+	public final fun addEntry (Ljava/lang/String;Lcom/github/ajalt/mordant/rendering/Widget;ZLjava/lang/String;)Lcom/github/ajalt/mordant/input/InteractiveSelectListBuilder;
+	public final fun addEntry (Ljava/lang/String;Ljava/lang/String;)Lcom/github/ajalt/mordant/input/InteractiveSelectListBuilder;
 	public final fun addEntry (Ljava/lang/String;Ljava/lang/String;Z)Lcom/github/ajalt/mordant/input/InteractiveSelectListBuilder;
-	public static synthetic fun addEntry$default (Lcom/github/ajalt/mordant/input/InteractiveSelectListBuilder;Ljava/lang/String;Lcom/github/ajalt/mordant/rendering/Widget;ZILjava/lang/Object;)Lcom/github/ajalt/mordant/input/InteractiveSelectListBuilder;
-	public static synthetic fun addEntry$default (Lcom/github/ajalt/mordant/input/InteractiveSelectListBuilder;Ljava/lang/String;Ljava/lang/String;ZILjava/lang/Object;)Lcom/github/ajalt/mordant/input/InteractiveSelectListBuilder;
+	public final fun addEntry (Ljava/lang/String;Ljava/lang/String;ZLjava/lang/String;)Lcom/github/ajalt/mordant/input/InteractiveSelectListBuilder;
+	public static synthetic fun addEntry$default (Lcom/github/ajalt/mordant/input/InteractiveSelectListBuilder;Ljava/lang/String;Lcom/github/ajalt/mordant/rendering/Widget;ZLjava/lang/String;ILjava/lang/Object;)Lcom/github/ajalt/mordant/input/InteractiveSelectListBuilder;
+	public static synthetic fun addEntry$default (Lcom/github/ajalt/mordant/input/InteractiveSelectListBuilder;Ljava/lang/String;Ljava/lang/String;ZLjava/lang/String;ILjava/lang/Object;)Lcom/github/ajalt/mordant/input/InteractiveSelectListBuilder;
 	public final fun clearOnExit (Z)Lcom/github/ajalt/mordant/input/InteractiveSelectListBuilder;
 	public final fun createMultiSelectInputAnimation ()Lcom/github/ajalt/mordant/input/InputReceiverAnimation;
 	public final fun createSingleSelectInputAnimation ()Lcom/github/ajalt/mordant/input/InputReceiverAnimation;
@@ -1579,19 +1583,26 @@ public final class com/github/ajalt/mordant/widgets/SelectList : com/github/ajal
 }
 
 public final class com/github/ajalt/mordant/widgets/SelectList$Entry {
+	public fun <init> (Ljava/lang/String;)V
+	public fun <init> (Ljava/lang/String;Lcom/github/ajalt/mordant/rendering/Widget;)V
 	public fun <init> (Ljava/lang/String;Lcom/github/ajalt/mordant/rendering/Widget;Z)V
-	public synthetic fun <init> (Ljava/lang/String;Lcom/github/ajalt/mordant/rendering/Widget;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V
+	public fun <init> (Ljava/lang/String;Lcom/github/ajalt/mordant/rendering/Widget;ZLjava/lang/String;)V
+	public synthetic fun <init> (Ljava/lang/String;Lcom/github/ajalt/mordant/rendering/Widget;ZLjava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+	public fun <init> (Ljava/lang/String;Ljava/lang/String;)V
 	public fun <init> (Ljava/lang/String;Ljava/lang/String;Z)V
-	public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V
+	public fun <init> (Ljava/lang/String;Ljava/lang/String;ZLjava/lang/String;)V
+	public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;ZLjava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
 	public final fun component1 ()Ljava/lang/String;
 	public final fun component2 ()Lcom/github/ajalt/mordant/rendering/Widget;
 	public final fun component3 ()Z
-	public final fun copy (Ljava/lang/String;Lcom/github/ajalt/mordant/rendering/Widget;Z)Lcom/github/ajalt/mordant/widgets/SelectList$Entry;
-	public static synthetic fun copy$default (Lcom/github/ajalt/mordant/widgets/SelectList$Entry;Ljava/lang/String;Lcom/github/ajalt/mordant/rendering/Widget;ZILjava/lang/Object;)Lcom/github/ajalt/mordant/widgets/SelectList$Entry;
+	public final fun component4 ()Ljava/lang/String;
+	public final fun copy (Ljava/lang/String;Lcom/github/ajalt/mordant/rendering/Widget;ZLjava/lang/String;)Lcom/github/ajalt/mordant/widgets/SelectList$Entry;
+	public static synthetic fun copy$default (Lcom/github/ajalt/mordant/widgets/SelectList$Entry;Ljava/lang/String;Lcom/github/ajalt/mordant/rendering/Widget;ZLjava/lang/String;ILjava/lang/Object;)Lcom/github/ajalt/mordant/widgets/SelectList$Entry;
 	public fun equals (Ljava/lang/Object;)Z
 	public final fun getDescription ()Lcom/github/ajalt/mordant/rendering/Widget;
 	public final fun getSelected ()Z
 	public final fun getTitle ()Ljava/lang/String;
+	public final fun getValue ()Ljava/lang/String;
 	public fun hashCode ()I
 	public fun toString ()Ljava/lang/String;
 }
diff --git a/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/input/InteractiveSelectList.kt b/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/input/InteractiveSelectList.kt
index d9c1597f..1699765a 100644
--- a/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/input/InteractiveSelectList.kt
+++ b/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/input/InteractiveSelectList.kt
@@ -7,7 +7,7 @@ import kotlin.jvm.JvmName
 /**
  * Display a list of items and allow the user to select one with the arrow keys and enter.
  *
- * @return the selected item title, or null if the user canceled the selection
+ * @return the selected item value, or title if value is not specified, or null if the user canceled the selection
  */
 inline fun Terminal.interactiveSelectList(
     block: InteractiveSelectListBuilder.() -> Unit,
@@ -41,7 +41,7 @@ fun Terminal.interactiveSelectList(
  *
  * @param entries The list of items to select from
  * @param title The title to display above the list
- * @return the selected item title, or null if the user canceled the selection
+ * @return the selected item value, or title if value is not specified, or null if the user canceled the selection
  */
 @JvmName("interactiveSelectListEntry")
 fun Terminal.interactiveSelectList(
@@ -57,7 +57,7 @@ fun Terminal.interactiveSelectList(
 /**
  * Display a list of items and allow the user to select zero or more with the arrow keys and enter.
  *
- * @return the selected item titles, or null if the user canceled the selection
+ * @return the selected item value, or title if value is not specified, or null if the user canceled the selection
  */
 inline fun Terminal.interactiveMultiSelectList(
     block: InteractiveSelectListBuilder.() -> Unit,
@@ -73,7 +73,7 @@ inline fun Terminal.interactiveMultiSelectList(
  *
  * @param entries The list of items to select from
  * @param title The title to display above the list
- * @return the selected item titles, or null if the user canceled the selection
+ * @return the selected item value, or title if value is not specified, or null if the user canceled the selection
  */
 @JvmName("interactiveMultiSelectListEntry")
 fun Terminal.interactiveMultiSelectList(
diff --git a/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/input/SelectListAnimation.kt b/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/input/SelectListAnimation.kt
index 4fc853ed..9befdc10 100644
--- a/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/input/SelectListAnimation.kt
+++ b/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/input/SelectListAnimation.kt
@@ -12,6 +12,7 @@ import com.github.ajalt.mordant.terminal.Terminal
 import com.github.ajalt.mordant.widgets.SelectList
 import com.github.ajalt.mordant.widgets.Text
 import kotlin.jvm.JvmName
+import kotlin.jvm.JvmOverloads
 
 private class SelectConfig(
     var entries: MutableList<SelectList.Entry> = mutableListOf(),
@@ -77,21 +78,25 @@ class InteractiveSelectListBuilder(private val terminal: Terminal) {
     }
 
     /** Add an item to the list of items to select from */
+    @JvmOverloads
     fun addEntry(
         title: String,
         description: String,
         selected: Boolean = false,
+        value: String? = null,
     ): InteractiveSelectListBuilder = apply {
-        config.entries += SelectList.Entry(title, description, selected)
+        config.entries += SelectList.Entry(title, description, selected, value)
     }
 
     /** Add an item to the list of items to select from */
+    @JvmOverloads
     fun addEntry(
         title: String,
         description: Widget? = null,
         selected: Boolean = false,
+        value: String? = null
     ): InteractiveSelectListBuilder = apply {
-        config.entries += SelectList.Entry(title, description, selected)
+        config.entries += SelectList.Entry(title, description, selected, value)
     }
 
     /** Add an item to the list of items to select from */
@@ -99,11 +104,6 @@ class InteractiveSelectListBuilder(private val terminal: Terminal) {
         config.entries += entry
     }
 
-    /** Add an item to the list of items to select from */
-    fun addEntry(entry: String): InteractiveSelectListBuilder = apply {
-        config.entries += SelectList.Entry(entry)
-    }
-
     /** Set the title to display above the list */
     fun title(title: String): InteractiveSelectListBuilder = apply {
         config.title = when {
@@ -386,10 +386,10 @@ private class SelectInputAnimation(
                     }
 
                     key == keySubmit -> {
-                        if (singleSelect) copy(finished = true, result = listOf(entry.title))
+                        if (singleSelect) copy(finished = true, result = listOf(entry.value ?: entry.title))
                         else copy(
                             finished = true,
-                            result = items.filter { it.selected }.map { it.title })
+                            result = items.filter { it.selected }.map { it.value ?: it.title })
                     }
 
                     filtering && !key.alt && !key.ctrl -> {
diff --git a/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/widgets/SelectList.kt b/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/widgets/SelectList.kt
index e2448d06..b0cae866 100644
--- a/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/widgets/SelectList.kt
+++ b/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/widgets/SelectList.kt
@@ -8,6 +8,7 @@ import com.github.ajalt.mordant.table.Borders
 import com.github.ajalt.mordant.table.table
 import com.github.ajalt.mordant.table.verticalLayout
 import com.github.ajalt.mordant.terminal.Terminal
+import kotlin.jvm.JvmOverloads
 
 /**
  * A list widget with selectable items.
@@ -56,19 +57,23 @@ class SelectList private constructor(
         unselectedMarkerStyle = ThemeStyle.of("select.unselected-marker", unselectedMarkerStyle),
     )
 
-    data class Entry(
+    data class Entry @JvmOverloads constructor(
         /** The title of the entry. */
         val title: String,
         /** An optional description of the entry. */
         val description: Widget? = null,
         /** Whether this entry is marked as selected. */
         val selected: Boolean = false,
+        /** Return this value instead of title if not null. */
+        val value: String? = null,
     ) {
-        constructor(title: String, description: String?, selected: Boolean = false)
+        @JvmOverloads
+        constructor(title: String, description: String?, selected: Boolean = false, value: String? = null)
                 : this(
             title = title,
             description = description?.let { Text(it, whitespace = Whitespace.PRE_WRAP) },
-            selected = selected
+            selected = selected,
+            value = value
         )
     }
 


Test patch:
diff --git a/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/input/InteractiveSelectListTest.kt b/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/input/InteractiveSelectListTest.kt
index fbb92778..2a7a196e 100644
--- a/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/input/InteractiveSelectListTest.kt
+++ b/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/input/InteractiveSelectListTest.kt
@@ -16,28 +16,38 @@ class InteractiveSelectListTest {
     private val t = Terminal(terminalInterface = rec)
 
     @[Test JsName("single_select_strings")]
-    fun `single select strings`() = doSingleSelectTest {
+    fun `single select strings`() = doSingleSelectTest({
         t.interactiveSelectList(listOf("a", "b", "c"))
-    }
+    }, "b")
 
     @[Test JsName("single_select_entries")]
-    fun `single select entries`() = doSingleSelectTest {
+    fun `single select entries`() = doSingleSelectTest({
         t.interactiveSelectList(listOf(Entry("a"), Entry("b"), Entry("c")))
-    }
+    }, "b")
+
+    @[Test JsName("single_select_entries_with_value")]
+    fun `single select entries with value`() = doSingleSelectTest({
+        t.interactiveSelectList(listOf(Entry("a", value = "AA"), Entry("b", value = "BB"), Entry("c", value = "CC")))
+    }, "BB")
 
     @[Test JsName("multi_select_strings")]
-    fun `multi select strings`() = doMultiSelectTest {
+    fun `multi select strings`() = doMultiSelectTest({
         t.interactiveMultiSelectList(listOf("a", "b", "c"))
-    }
+    }, listOf("b", "c"))
 
     @[Test JsName("multi_select_entries")]
-    fun `multi select entries`() = doMultiSelectTest {
+    fun `multi select entries`() = doMultiSelectTest({
         t.interactiveMultiSelectList(listOf(Entry("a"), Entry("b"), Entry("c")))
-    }
+    }, listOf("b", "c"))
+
+    @[Test JsName("multi_select_entries_with_values")]
+    fun `multi select entries with values`() = doMultiSelectTest({
+        t.interactiveMultiSelectList(listOf(Entry("a", value = "AA"), Entry("b", value = "BB"), Entry("c", value = "CC")))
+    }, listOf("BB", "CC"))
 
-    private fun doSingleSelectTest(runList: () -> String?) {
+    private fun doSingleSelectTest(runList: () -> String?, expected: String) {
         rec.inputEvents = mutableListOf(KeyboardEvent("ArrowDown"), KeyboardEvent("Enter"))
-        runList() shouldBe "b"
+        runList() shouldBe expected
         rec.stdout() shouldContain  """
         ░❯ a
         ░  b
@@ -45,7 +55,7 @@ class InteractiveSelectListTest {
         """.trimMargin("░")
     }
 
-    private fun doMultiSelectTest(runList: () -> List<String>?) {
+    private fun doMultiSelectTest(runList: () -> List<String>?, expected: List<String>) {
         rec.inputEvents = mutableListOf(
             KeyboardEvent("ArrowDown"),
             KeyboardEvent("x"),
@@ -53,7 +63,7 @@ class InteractiveSelectListTest {
             KeyboardEvent("x"),
             KeyboardEvent("Enter"),
         )
-        runList() shouldBe listOf("b", "c")
+        runList() shouldBe expected
         rec.stdout() shouldContain  """
         ░❯ • a
         ░  • b