Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/dokka.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.gradle/
**/build/
.kotlin/
.idea/
3 changes: 2 additions & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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`
}
Expand Down
92 changes: 92 additions & 0 deletions lib/src/main/kotlin/bps/console/menu/BaseScrollingSelectionMenu.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package bps.console.menu

import kotlin.math.min

/**
* @param T the type of the selected item
*/
abstract class BaseScrollingSelectionMenu<T>(
override val header: () -> String?,
override val prompt: () -> String = { "Enter selection: " },
val limit: Int = 30,
val offset: Int = 0,
protected val itemListGenerator: (Int, Int) -> List<T>,
protected open val extraItems: List<MenuItem> = emptyList(),
protected open val labelGenerator: T.() -> String = { toString() },
) : Menu {

override val shortcutMap: MutableMap<String, MenuItem> = mutableMapOf()

constructor(
header: () -> String?,
prompt: () -> String = { "Enter selection: " },
limit: Int = 30,
offset: Int = 0,
baseList: List<T>,
extraItems: List<MenuItem> = 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<MenuItem>.incorporateItem(menuItem: MenuItem) {
add(menuItem)
if (menuItem.shortcut !== null)
shortcutMap[menuItem.shortcut!!] = menuItem
}

abstract fun generateBaseMenuItemList(): MutableList<MenuItem>

override var itemsGenerator: () -> List<MenuItem> = {
generateBaseMenuItemList()
.also { menuItems: MutableList<MenuItem> ->
addNextAndPreviousItems(menuItems)
addExtraItems(menuItems)
addBackAndQuitItems(menuItems)
}
}

private fun addExtraItems(menuItems: MutableList<MenuItem>) {
extraItems.forEach { menuItems.incorporateItem(it) }
}

private fun addBackAndQuitItems(menuItems: MutableList<MenuItem>) {
menuItems.incorporateItem(backItem)
menuItems.incorporateItem(quitItem)
}

protected fun addNextAndPreviousItems(menuItems: MutableList<MenuItem>) {
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<T>

abstract fun nextPageMenuProducer(): BaseScrollingSelectionMenu<T>

}
105 changes: 105 additions & 0 deletions lib/src/main/kotlin/bps/console/menu/ScrollingMultiSelectionMenu.kt
Original file line number Diff line number Diff line change
@@ -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<T> private constructor(
limit: Int = 30,
offset: Int = 0,
itemListGenerator: (Int, Int) -> List<T>,
extraItems: List<MenuItem> = emptyList(),
protected val actionLabel: () -> String = { "Take Action" },
protected val actOnSelectedItems: (MenuSession, List<T>) -> Unit,
) : BaseScrollingSelectionMenu<T>(
// 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<T> = 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<String, MenuItem> = mutableMapOf()
override val extraItems: List<MenuItem> =
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<T>,
extraItems: List<MenuItem> = emptyList(),
actOnSelectedItems: (MenuSession, List<T>) -> 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<T> =
ScrollingMultiSelectionMenu(
limit = limit,
offset = max(offset - limit, 0),
extraItems = extraItems,
itemListGenerator = itemListGenerator,
actOnSelectedItems = actOnSelectedItems,
)

override fun nextPageMenuProducer(): ScrollingMultiSelectionMenu<T> =
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)
}
}
}

}
53 changes: 11 additions & 42 deletions lib/src/main/kotlin/bps/console/menu/ScrollingSelectionMenu.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ import kotlin.math.min
* @param T the type of the selected item
*/
open class ScrollingSelectionMenu<T>(
override val header: () -> String?,
override val prompt: () -> String = { "Enter selection: " },
val limit: Int = 30,
val offset: Int = 0,
protected val itemListGenerator: (Int, Int) -> List<T>,
protected val extraItems: List<MenuItem> = emptyList(),
protected val labelGenerator: T.() -> String = { toString() },
header: () -> String?,
prompt: () -> String = { "Enter selection: " },
limit: Int = 30,
offset: Int = 0,
itemListGenerator: (Int, Int) -> List<T>,
extraItems: List<MenuItem> = emptyList(),
labelGenerator: T.() -> String = { toString() },
protected val actOnSelectedItem: (MenuSession, T) -> Unit,
) : Menu {
) : BaseScrollingSelectionMenu<T>(header, prompt, limit, offset, itemListGenerator, extraItems, labelGenerator) {

override val shortcutMap: MutableMap<String, MenuItem> = mutableMapOf()

Expand Down Expand Up @@ -45,38 +45,7 @@ open class ScrollingSelectionMenu<T>(
require(offset >= 0) { "offset must be >= 0" }
}

protected open fun MutableList<MenuItem>.incorporateItem(menuItem: MenuItem) {
add(menuItem)
if (menuItem.shortcut !== null)
shortcutMap[menuItem.shortcut!!] = menuItem
}

final override var itemsGenerator: () -> List<MenuItem> = {
generateBaseMenuItemList()
.also { menuItems: MutableList<MenuItem> ->
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<T> =
override fun previousPageMenuProducer(): ScrollingSelectionMenu<T> =
ScrollingSelectionMenu(
header,
prompt,
Expand All @@ -88,7 +57,7 @@ open class ScrollingSelectionMenu<T>(
actOnSelectedItem,
)

protected open fun nextPageMenuProducer(): ScrollingSelectionMenu<T> =
override fun nextPageMenuProducer(): ScrollingSelectionMenu<T> =
ScrollingSelectionMenu(
header,
prompt,
Expand All @@ -100,7 +69,7 @@ open class ScrollingSelectionMenu<T>(
actOnSelectedItem,
)

protected open fun generateBaseMenuItemList() =
override fun generateBaseMenuItemList() =
itemListGenerator(limit, offset)
.mapTo(mutableListOf()) { item ->
item({ item.labelGenerator() }) { menuSession: MenuSession ->
Expand Down
69 changes: 69 additions & 0 deletions lib/src/test/kotlin/bps/console/TempTest.kt
Original file line number Diff line number Diff line change
@@ -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()
}
Loading
Loading