diff --git a/sharingan/src/commonMain/kotlin/dev/sharingan/SharinganExport.kt b/sharingan/src/commonMain/kotlin/dev/sharingan/SharinganExport.kt index 6c54b22..01980e0 100644 --- a/sharingan/src/commonMain/kotlin/dev/sharingan/SharinganExport.kt +++ b/sharingan/src/commonMain/kotlin/dev/sharingan/SharinganExport.kt @@ -2,6 +2,7 @@ package dev.sharingan import dev.sharingan.internal.formatClockTime import dev.sharingan.ui.descriptorOf +import dev.sharingan.ui.protocolCountsLine /** * Turns captured events into shareable text. @@ -78,12 +79,8 @@ public object SharinganExport { // ── session assembly ───────────────────────────────────────── - private fun countsLine(events: List): String { - val http = events.count { it is HttpEvent } - val mqtt = events.count { it is MqttEvent } - val ble = events.count { it is BleEvent } - return "${events.size} events · HTTP $http · MQTT $mqtt · BLE $ble" - } + private fun countsLine(events: List): String = + "${events.size} events · ${protocolCountsLine(events)}" private fun eventJson(event: SharinganEvent, indent: String): String { val fields = mutableListOf>() diff --git a/sharingan/src/commonMain/kotlin/dev/sharingan/internal/NotificationContent.kt b/sharingan/src/commonMain/kotlin/dev/sharingan/internal/NotificationContent.kt index 27e8f65..071d2b6 100644 --- a/sharingan/src/commonMain/kotlin/dev/sharingan/internal/NotificationContent.kt +++ b/sharingan/src/commonMain/kotlin/dev/sharingan/internal/NotificationContent.kt @@ -1,9 +1,8 @@ package dev.sharingan.internal import dev.sharingan.SharinganEvent -import dev.sharingan.ui.Protocol import dev.sharingan.ui.descriptorOf -import dev.sharingan.ui.protocolOf +import dev.sharingan.ui.protocolCountsLine /** * What the capture notification should say — the platform-free half of the @@ -29,9 +28,7 @@ internal fun notificationContentOf( ): NotificationContent? { if (events.isEmpty()) return null val stateLabel = if (recording) "Capturing" else "Paused" - val countsLine = Protocol.entries.joinToString(" · ") { protocol -> - "${protocol.name} ${events.count { protocolOf(it) == protocol }}" - } + val countsLine = protocolCountsLine(events) val ticker = events.takeLast(3).reversed().joinToString("\n") { descriptorOf(it).tickerLine(it) } return NotificationContent( title = "Sharingan — $stateLabel · ${events.size} events", diff --git a/sharingan/src/commonMain/kotlin/dev/sharingan/ui/BleDescriptor.kt b/sharingan/src/commonMain/kotlin/dev/sharingan/ui/BleDescriptor.kt index 5877b5d..31ff376 100644 --- a/sharingan/src/commonMain/kotlin/dev/sharingan/ui/BleDescriptor.kt +++ b/sharingan/src/commonMain/kotlin/dev/sharingan/ui/BleDescriptor.kt @@ -1,6 +1,7 @@ package dev.sharingan.ui import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.vector.ImageVector import dev.sharingan.BleEvent import dev.sharingan.BleOperation import dev.sharingan.formatBytes @@ -11,6 +12,8 @@ internal object BleDescriptor : ProtocolDescriptor() { override val protocol: Protocol = Protocol.BLE override val eventNoun: String = "operations" + override val tabIcon: ImageVector = SharinganIcons.Bluetooth + override val searchPlaceholder: String = "Filter characteristic, device…" override val chips: List = listOf( FilterChipSpec("all", "All"), diff --git a/sharingan/src/commonMain/kotlin/dev/sharingan/ui/EventFilter.kt b/sharingan/src/commonMain/kotlin/dev/sharingan/ui/EventFilter.kt index a533e52..37a13f9 100644 --- a/sharingan/src/commonMain/kotlin/dev/sharingan/ui/EventFilter.kt +++ b/sharingan/src/commonMain/kotlin/dev/sharingan/ui/EventFilter.kt @@ -18,6 +18,14 @@ internal data class FilterChipSpec(val key: String, val label: String) internal fun protocolOf(event: SharinganEvent): Protocol = descriptorOf(event).protocol +/** `HTTP n · MQTT n · BLE n` — one segment per protocol, in tab order. The + * single home for the cross-protocol counts line shared by the export header + * and the capture notification. */ +internal fun protocolCountsLine(events: List): String = + Protocol.entries.joinToString(" · ") { protocol -> + "${protocol.name} ${events.count { protocolOf(it) == protocol }}" + } + /** The design's per-protocol chip rows. */ internal fun chipsFor(protocol: Protocol): List = descriptorOf(protocol).chips diff --git a/sharingan/src/commonMain/kotlin/dev/sharingan/ui/HomeScreen.kt b/sharingan/src/commonMain/kotlin/dev/sharingan/ui/HomeScreen.kt index aff292f..e49984a 100644 --- a/sharingan/src/commonMain/kotlin/dev/sharingan/ui/HomeScreen.kt +++ b/sharingan/src/commonMain/kotlin/dev/sharingan/ui/HomeScreen.kt @@ -190,11 +190,7 @@ private fun TabBar( Row(Modifier.fillMaxWidth()) { Protocol.entries.forEach { protocol -> val on = protocol == selected - val icon = when (protocol) { - Protocol.HTTP -> SharinganIcons.Globe - Protocol.MQTT -> SharinganIcons.Waves - Protocol.BLE -> SharinganIcons.Bluetooth - } + val icon = descriptorOf(protocol).tabIcon Column( Modifier.weight(1f).clickable { onSelect(protocol) }, horizontalAlignment = Alignment.CenterHorizontally, @@ -255,11 +251,7 @@ private fun SearchField( modifier: Modifier = Modifier, ) { val colors = LocalSharinganColors.current - val placeholder = when (protocol) { - Protocol.HTTP -> "Filter path, status, header…" - Protocol.MQTT -> "Filter topic, payload…" - Protocol.BLE -> "Filter characteristic, device…" - } + val placeholder = descriptorOf(protocol).searchPlaceholder Row( modifier .fillMaxWidth() diff --git a/sharingan/src/commonMain/kotlin/dev/sharingan/ui/HttpDescriptor.kt b/sharingan/src/commonMain/kotlin/dev/sharingan/ui/HttpDescriptor.kt index e20d787..1b9a2fb 100644 --- a/sharingan/src/commonMain/kotlin/dev/sharingan/ui/HttpDescriptor.kt +++ b/sharingan/src/commonMain/kotlin/dev/sharingan/ui/HttpDescriptor.kt @@ -19,6 +19,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -32,6 +33,8 @@ internal object HttpDescriptor : ProtocolDescriptor() { override val protocol: Protocol = Protocol.HTTP override val eventNoun: String = "requests" + override val tabIcon: ImageVector = SharinganIcons.Globe + override val searchPlaceholder: String = "Filter path, status, header…" override val chips: List = listOf( FilterChipSpec("all", "All"), diff --git a/sharingan/src/commonMain/kotlin/dev/sharingan/ui/MqttDescriptor.kt b/sharingan/src/commonMain/kotlin/dev/sharingan/ui/MqttDescriptor.kt index 471311b..079fd95 100644 --- a/sharingan/src/commonMain/kotlin/dev/sharingan/ui/MqttDescriptor.kt +++ b/sharingan/src/commonMain/kotlin/dev/sharingan/ui/MqttDescriptor.kt @@ -1,6 +1,7 @@ package dev.sharingan.ui import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.vector.ImageVector import dev.sharingan.MqttDirection import dev.sharingan.MqttEvent import dev.sharingan.formatBytes @@ -12,6 +13,8 @@ internal object MqttDescriptor : ProtocolDescriptor() { override val protocol: Protocol = Protocol.MQTT override val eventNoun: String = "messages" + override val tabIcon: ImageVector = SharinganIcons.Waves + override val searchPlaceholder: String = "Filter topic, payload…" override val chips: List = listOf( FilterChipSpec("all", "All"), diff --git a/sharingan/src/commonMain/kotlin/dev/sharingan/ui/ProtocolDescriptor.kt b/sharingan/src/commonMain/kotlin/dev/sharingan/ui/ProtocolDescriptor.kt index 42a2f32..a49acc8 100644 --- a/sharingan/src/commonMain/kotlin/dev/sharingan/ui/ProtocolDescriptor.kt +++ b/sharingan/src/commonMain/kotlin/dev/sharingan/ui/ProtocolDescriptor.kt @@ -1,6 +1,7 @@ package dev.sharingan.ui import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.vector.ImageVector import dev.sharingan.BleEvent import dev.sharingan.HttpEvent import dev.sharingan.MqttEvent @@ -34,6 +35,12 @@ internal abstract class ProtocolDescriptor { /** Quick-filter chips below the search field, in design order. */ abstract val chips: List + /** Tab-bar icon for this protocol's tab. */ + abstract val tabIcon: ImageVector + + /** Placeholder hint shown in the search field on this protocol's tab. */ + abstract val searchPlaceholder: String + /** Chip matching; `"all"` is handled by the caller before dispatch. */ protected abstract fun chipMatches(event: E, chipKey: String): Boolean diff --git a/sharingan/src/commonTest/kotlin/dev/sharingan/ui/EventFilterTest.kt b/sharingan/src/commonTest/kotlin/dev/sharingan/ui/EventFilterTest.kt index f85e2bd..4cc799e 100644 --- a/sharingan/src/commonTest/kotlin/dev/sharingan/ui/EventFilterTest.kt +++ b/sharingan/src/commonTest/kotlin/dev/sharingan/ui/EventFilterTest.kt @@ -5,6 +5,7 @@ import dev.sharingan.BleOperation import dev.sharingan.HttpEvent import dev.sharingan.MqttDirection import dev.sharingan.MqttEvent +import dev.sharingan.SharinganEvent import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse @@ -87,6 +88,24 @@ internal class EventFilterTest { assertTrue(matchesQuery(http(), " ")) } + @Test + fun `Given a mix of events When the counts line is built Then it shows per-protocol counts`() { + val events: List = + listOf(http(), http(), mqtt(MqttDirection.PUBLISH), ble(BleOperation.NOTIFY)) + assertEquals("HTTP 2 · MQTT 1 · BLE 1", protocolCountsLine(events)) + } + + @Test + fun `When the counts line is built Then it has one segment for every protocol`() { + val events: List = + listOf(http(), mqtt(MqttDirection.PUBLISH), ble(BleOperation.NOTIFY)) + val line = protocolCountsLine(events) + assertEquals(Protocol.entries.size, line.split(" · ").size) + for (p in Protocol.entries) { + assertTrue(line.contains(p.name)) + } + } + @Test fun `When chips for a protocol are requested Then they match the design`() { assertEquals(listOf("all", "err", "2xx", "get", "post"), chipsFor(Protocol.HTTP).map { it.key })