diff --git a/.github/workflows/dokka.yml b/.github/workflows/dokka.yml index 016ffe6..90f96b6 100644 --- a/.github/workflows/dokka.yml +++ b/.github/workflows/dokka.yml @@ -27,7 +27,7 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@af1da67850ed9a4cedd57bfd976089dd991e2582 # v4.0.0 - name: Run Dokka - run: ./gradlew :lib:dokkaHtml :test:dokkaHtml + run: ./gradlew :lib:dokkaGenerate :test:dokkaGenerate - name: Generate tar file run: | echo ::group::prepare files for archival diff --git a/.gitignore b/.gitignore index df2c9f8..beb2ac6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .gradle/ **/build/ .kotlin/ +.idea/ diff --git a/gradle.properties b/gradle.properties index 7457cd5..4e80d42 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,6 +3,7 @@ commonsValidatorVersion=1.9.0 org.gradle.parallel=true org.gradle.caching=true kotestVersion=5.9.1 -kotlinVersion=2.1.10 +kotlinVersion=2.3.0 kotlinXDateTimeVersion=0.6.1 mockkVersion=1.13.16 +org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 22b7f64..b1a46a6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,4 +10,4 @@ commons-math3 = { module = "org.apache.commons:commons-math3", version.ref = "co guava = { module = "com.google.guava:guava", version.ref = "guava" } [plugins] -kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version = "2.0.20" } +kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version = "2.3.0" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e18bc25..bad7c24 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.0-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts index 2e65799..796b73b 100644 --- a/lib/build.gradle.kts +++ b/lib/build.gradle.kts @@ -24,7 +24,7 @@ TODO plugins { alias(libs.plugins.kotlin.jvm) - id("org.jetbrains.dokka") version "2.0.0" + id("org.jetbrains.dokka") version "2.1.0" `maven-publish` `java-library` } diff --git a/lib/src/main/kotlin/bps/console/menu/BaseScrollingSelectionMenu.kt b/lib/src/main/kotlin/bps/console/menu/BaseScrollingSelectionMenu.kt new file mode 100644 index 0000000..731f86e --- /dev/null +++ b/lib/src/main/kotlin/bps/console/menu/BaseScrollingSelectionMenu.kt @@ -0,0 +1,92 @@ +package bps.console.menu + +import kotlin.math.min + +/** + * @param T the type of the selected item + */ +abstract class BaseScrollingSelectionMenu( + override val header: () -> String?, + override val prompt: () -> String = { "Enter selection: " }, + val limit: Int = 30, + val offset: Int = 0, + protected val itemListGenerator: (Int, Int) -> List, + protected open val extraItems: List = emptyList(), + protected open val labelGenerator: T.() -> String = { toString() }, +) : Menu { + + override val shortcutMap: MutableMap = mutableMapOf() + + constructor( + header: () -> String?, + prompt: () -> String = { "Enter selection: " }, + limit: Int = 30, + offset: Int = 0, + baseList: List, + extraItems: List = emptyList(), + labelGenerator: T.() -> String = { toString() }, + ) : this( + header = header, + prompt = prompt, + limit = limit, + offset = offset, + itemListGenerator = { lim, off -> baseList.subList(off, min(baseList.size, off + lim)) }, + extraItems = extraItems, + labelGenerator = labelGenerator, + ) + + init { + require(limit > 0) { "limit must be > 0" } + require(offset >= 0) { "offset must be >= 0" } + } + + protected open fun MutableList.incorporateItem(menuItem: MenuItem) { + add(menuItem) + if (menuItem.shortcut !== null) + shortcutMap[menuItem.shortcut!!] = menuItem + } + + abstract fun generateBaseMenuItemList(): MutableList + + override var itemsGenerator: () -> List = { + generateBaseMenuItemList() + .also { menuItems: MutableList -> + addNextAndPreviousItems(menuItems) + addExtraItems(menuItems) + addBackAndQuitItems(menuItems) + } + } + + private fun addExtraItems(menuItems: MutableList) { + extraItems.forEach { menuItems.incorporateItem(it) } + } + + private fun addBackAndQuitItems(menuItems: MutableList) { + menuItems.incorporateItem(backItem) + menuItems.incorporateItem(quitItem) + } + + protected fun addNextAndPreviousItems(menuItems: MutableList) { + if (menuItems.size == limit) { + menuItems.incorporateItem( + item({ "Next Items" }, "n") { menuSession -> + menuSession.pop() + menuSession.push(nextPageMenuProducer()) + }, + ) + } + if (offset > 0) { + menuItems.incorporateItem( + item({ "Previous Items" }, "p") { menuSession -> + menuSession.pop() + menuSession.push(previousPageMenuProducer()) + }, + ) + } + } + + abstract fun previousPageMenuProducer(): BaseScrollingSelectionMenu + + abstract fun nextPageMenuProducer(): BaseScrollingSelectionMenu + +} diff --git a/lib/src/main/kotlin/bps/console/menu/ScrollingMultiSelectionMenu.kt b/lib/src/main/kotlin/bps/console/menu/ScrollingMultiSelectionMenu.kt new file mode 100644 index 0000000..3dbdf91 --- /dev/null +++ b/lib/src/main/kotlin/bps/console/menu/ScrollingMultiSelectionMenu.kt @@ -0,0 +1,105 @@ +package bps.console.menu + +import bps.console.app.MenuSession +import kotlin.math.max +import kotlin.math.min + +/** + * @param T the type of the selected item + */ +open class ScrollingMultiSelectionMenu private constructor( + limit: Int = 30, + offset: Int = 0, + itemListGenerator: (Int, Int) -> List, + extraItems: List = emptyList(), + protected val actionLabel: () -> String = { "Take Action" }, + protected val actOnSelectedItems: (MenuSession, List) -> Unit, +) : BaseScrollingSelectionMenu( + // NOTE need to pass this in but I'm overriding it below... see if this works + header = { null }, + limit = limit, + offset = offset, + itemListGenerator = itemListGenerator, +) { + + private val selectedItems: MutableList = mutableListOf() + override val header: () -> String? = { String.format("Selected Items: (%d)", selectedItems.size) } + override val labelGenerator: T.() -> String = { + "[${ + if (this in selectedItems) + "x" + else + " " + }] $this" + } + override val shortcutMap: MutableMap = mutableMapOf() + override val extraItems: List = + listOf( + // FIXME add "deselect all", "select all", and "take action" + item( + label = actionLabel, + action = { actOnSelectedItems(it, selectedItems) }, + ), + item( + label = { "Select All" }, + shortcut = "s", + action = { selectedItems.addAll(itemListGenerator(Int.MAX_VALUE, 0)) }, + ), + item( + label = { "Deselect All" }, + shortcut = "d", + action = { selectedItems.clear() }, + ), + ) + + extraItems + + constructor( + limit: Int = 30, + offset: Int = 0, + baseList: List, + extraItems: List = emptyList(), + actOnSelectedItems: (MenuSession, List) -> Unit, + ) : this( + limit = limit, + offset = offset, + itemListGenerator = { lim, off -> baseList.subList(off, min(baseList.size, off + lim)) }, + extraItems = extraItems, + actOnSelectedItems = actOnSelectedItems, + ) + + init { + require(limit > 0) { "limit must be > 0" } + require(offset >= 0) { "offset must be >= 0" } + } + + override fun previousPageMenuProducer(): ScrollingMultiSelectionMenu = + ScrollingMultiSelectionMenu( + limit = limit, + offset = max(offset - limit, 0), + extraItems = extraItems, + itemListGenerator = itemListGenerator, + actOnSelectedItems = actOnSelectedItems, + ) + + override fun nextPageMenuProducer(): ScrollingMultiSelectionMenu = + ScrollingMultiSelectionMenu( + limit = limit, + offset = offset + limit, + itemListGenerator = itemListGenerator, + extraItems = extraItems, + actOnSelectedItems = actOnSelectedItems, + ) + + override fun generateBaseMenuItemList() = + itemListGenerator(limit, offset) + .mapTo(mutableListOf()) { item: T -> + item({ item.labelGenerator() }) { + if (item in selectedItems) { + selectedItems.remove(item) + } else { + selectedItems.add(item) + } + } + } + +} diff --git a/lib/src/main/kotlin/bps/console/menu/ScrollingSelectionMenu.kt b/lib/src/main/kotlin/bps/console/menu/ScrollingSelectionMenu.kt index 51fcd93..f155902 100644 --- a/lib/src/main/kotlin/bps/console/menu/ScrollingSelectionMenu.kt +++ b/lib/src/main/kotlin/bps/console/menu/ScrollingSelectionMenu.kt @@ -8,15 +8,15 @@ import kotlin.math.min * @param T the type of the selected item */ open class ScrollingSelectionMenu( - override val header: () -> String?, - override val prompt: () -> String = { "Enter selection: " }, - val limit: Int = 30, - val offset: Int = 0, - protected val itemListGenerator: (Int, Int) -> List, - protected val extraItems: List = emptyList(), - protected val labelGenerator: T.() -> String = { toString() }, + header: () -> String?, + prompt: () -> String = { "Enter selection: " }, + limit: Int = 30, + offset: Int = 0, + itemListGenerator: (Int, Int) -> List, + extraItems: List = emptyList(), + labelGenerator: T.() -> String = { toString() }, protected val actOnSelectedItem: (MenuSession, T) -> Unit, -) : Menu { +) : BaseScrollingSelectionMenu(header, prompt, limit, offset, itemListGenerator, extraItems, labelGenerator) { override val shortcutMap: MutableMap = mutableMapOf() @@ -45,38 +45,7 @@ open class ScrollingSelectionMenu( require(offset >= 0) { "offset must be >= 0" } } - protected open fun MutableList.incorporateItem(menuItem: MenuItem) { - add(menuItem) - if (menuItem.shortcut !== null) - shortcutMap[menuItem.shortcut!!] = menuItem - } - - final override var itemsGenerator: () -> List = { - generateBaseMenuItemList() - .also { menuItems: MutableList -> - if (menuItems.size == limit) { - menuItems.incorporateItem( - item({ "Next Items" }, "n") { menuSession -> - menuSession.pop() - menuSession.push(nextPageMenuProducer()) - }, - ) - } - if (offset > 0) { - menuItems.incorporateItem( - item({ "Previous Items" }, "p") { menuSession -> - menuSession.pop() - menuSession.push(previousPageMenuProducer()) - }, - ) - } - extraItems.forEach { menuItems.incorporateItem(it) } - menuItems.incorporateItem(backItem) - menuItems.incorporateItem(quitItem) - } - } - - protected open fun previousPageMenuProducer(): ScrollingSelectionMenu = + override fun previousPageMenuProducer(): ScrollingSelectionMenu = ScrollingSelectionMenu( header, prompt, @@ -88,7 +57,7 @@ open class ScrollingSelectionMenu( actOnSelectedItem, ) - protected open fun nextPageMenuProducer(): ScrollingSelectionMenu = + override fun nextPageMenuProducer(): ScrollingSelectionMenu = ScrollingSelectionMenu( header, prompt, @@ -100,7 +69,7 @@ open class ScrollingSelectionMenu( actOnSelectedItem, ) - protected open fun generateBaseMenuItemList() = + override fun generateBaseMenuItemList() = itemListGenerator(limit, offset) .mapTo(mutableListOf()) { item -> item({ item.labelGenerator() }) { menuSession: MenuSession -> diff --git a/lib/src/test/kotlin/bps/console/TempTest.kt b/lib/src/test/kotlin/bps/console/TempTest.kt new file mode 100644 index 0000000..9fcb1da --- /dev/null +++ b/lib/src/test/kotlin/bps/console/TempTest.kt @@ -0,0 +1,69 @@ +package bps.console + +import bps.console.app.MenuApplicationWithQuit +import bps.console.menu.Menu +import bps.console.menu.ScrollingMultiSelectionMenu +import bps.console.menu.pushMenu +import bps.console.menu.quitItem +import io.kotest.core.spec.style.FreeSpec +import io.kotest.matchers.shouldBe +import kotlin.math.pow +import kotlin.math.roundToLong + +class TempTest : FreeSpec() { + + private fun format(double: Double, scale: Int): String { + val longString = (double * 10.0.pow(scale)).roundToLong().toString() + return buildString { + append(longString.substring(0, longString.length - scale)) + append(".") + append(longString.substring(longString.length - scale)) + } + } + + + init { + "test" { + format(12345.12345, 2) shouldBe "12345.12" + format(12345.12345, 3) shouldBe "12345.123" + format(9876.9876, 2) shouldBe "9876.99" + } + } +} + +fun main() { + MenuApplicationWithQuit( + topLevelMenu = Menu( + items = { + add( + pushMenu( + label = { "print selected" }, + to = { + ScrollingMultiSelectionMenu( + baseList = listOf("a", "b", "c"), + ) { session, items -> + session.pop() + println(items) + } + }, + ), + ) + add( + pushMenu( + label = { "print reverse selected" }, + to = { + ScrollingMultiSelectionMenu( + baseList = listOf("a", "b", "c"), + ) { session, items -> + session.pop() + println(items.reversed()) + } + }, + ), + ) + add(quitItem("blarg!")) + }, + ), + ) + .runApplication() +} diff --git a/test/build.gradle.kts b/test/build.gradle.kts index 82322bc..56880e8 100644 --- a/test/build.gradle.kts +++ b/test/build.gradle.kts @@ -2,7 +2,7 @@ val kotestVersion: String by project plugins { alias(libs.plugins.kotlin.jvm) - id("org.jetbrains.dokka") version "2.0.0" + id("org.jetbrains.dokka") version "2.1.0" `maven-publish` `java-library` } diff --git a/test/src/main/kotlin/bps/console/ComplexConsoleIoTestFixture.kt b/test/src/main/kotlin/bps/console/ComplexConsoleIoTestFixture.kt index 757bd7b..0f5a6b9 100644 --- a/test/src/main/kotlin/bps/console/ComplexConsoleIoTestFixture.kt +++ b/test/src/main/kotlin/bps/console/ComplexConsoleIoTestFixture.kt @@ -399,7 +399,6 @@ interface ComplexConsoleIoTestFixture : SimpleConsoleIoTestFixture { withClue("application thread should have terminated within $awaitMillis milliseconds") { applicationThread.state shouldBe Thread.State.TERMINATED } - println("Application stopped") } }