Skip to content

Thernal/console

Repository files navigation

Console

A gesture-triggered debug overlay for Kotlin Multiplatform apps.
Drop it in, swipe to open, and inspect logs, HTTP traffic, and session state — without touching production code.

Maven Central License: MIT Kotlin Compose Multiplatform Platform


Features at a glance

📋 Log viewer Filterable log list — levels, tags, full-text search
🎨 Custom log types Define your own Log subtypes with their own UI renderers
🔍 Search & filter Real-time full-text search across message, tag, and level
🔗 Log grouping Link related logs with a shared groupId
🌐 Network inspector OkHttp + Ktor interceptors with headers, body, duration, status
🗂️ Details panel Live key/value sidebar for session info, flags, build metadata
⏸️ Stepper Pause log processing and replay events one-by-one
🖥️ Custom tabs Add your own full screens to the Console navigation bar
👆 Custom triggers Swap the default swipe for any gesture — double-tap, shake, etc.
🚫 Zero prod cost Noop stubs with identical APIs — no UI, no overhead in release builds

Quick start

1. Apply the settings plugin

Adds the JetBrains Compose repository required to resolve Compose Multiplatform artifacts.

// settings.gradle.kts
plugins {
    id("io.github.thernal.console") version "<version>"
}

2. Add dependencies

// build.gradle.kts
dependencies {
    debugImplementation("io.github.thernal:console-ui:<version>")
    debugImplementation("io.github.thernal:console-logging-ui:<version>")
    releaseImplementation("io.github.thernal:console-ui-noop:<version>")
}

3. Wrap your root composable

@Composable
fun App() {
    ConsoleProvider {
        YourAppContent()
    }
}

4. Open the console

Swipe ↑ ↓ ← → anywhere on screen (default gesture).


Logging

Basic logs

// Fire-and-forget — thread-safe, non-blocking
Console.notify {
    Log(message = "Payment initiated", tag = "Payments", level = LogLevel.Info)
}

// Suspending — preserves ordering when called sequentially inside a coroutine
Console.asyncNotify {
    Log(message = "Token refreshed", tag = "Auth", level = LogLevel.Success)
}

Log levels

Level Typical use
None No level indicator
Verbose Trace-level detail
Debug Development information
Info General flow events
Success Completed operations
Warning Recoverable issues
Error Failures that were handled
Fatal Unrecoverable crashes

Log grouping

Logs with the same groupId are visually linked — useful for correlating events like a network request and its response.

val id = Uuid.random().toString()

Console.notify {
    Log(message = "→ POST /auth/login", tag = "Network", level = LogLevel.Debug, groupId = id)
}

// ... later ...

Console.notify {
    Log(message = "← 200 OK (143ms)", tag = "Network", level = LogLevel.Success, groupId = id)
}

Custom log types

Implement Log to carry structured data through the pipeline.

data class AnalyticsLog(
    override val message: String,
    override val level: LogLevel = LogLevel.Debug,
    val eventName: String,
    val params: Map<String, Any> = emptyMap(),
    override val id: String = Uuid.random().toString(),
    override val tag: String? = "Analytics",
    override val groupId: String? = null,
    override val timestamp: Instant = Clock.System.now(),
) : Log

Send it like any other log:

Console.notify {
    AnalyticsLog(
        message = "screen_view",
        eventName = "screen_view",
        params = mapOf("screen_name" to "Checkout"),
    )
}

Custom log renderer

Implement LogRenderer so AnalyticsLog entries have their own item and detail screens, then register it for the type:

@file:OptIn(ConsoleInternalApi::class)  // LogRendererRegistry.register is a first-party API

object AnalyticsLogRenderer : LogRenderer {
    @Composable
    override fun Item(log: Log, modifier: Modifier) {
        if (log !is AnalyticsLog) return
        AnalyticsLogItem(log, modifier)
    }

    @Composable
    override fun Detail(log: Log) {
        if (log !is AnalyticsLog) return
        AnalyticsLogDetail(log)
    }
}

object AnalyticsAddon : ConsoleAddon {
    override fun onInstall() {
        LogRendererRegistry.register<AnalyticsLog>(AnalyticsLogRenderer)
    }
}

// Call once at startup
AnalyticsAddon.install()

Search & filter

The log list filters in real time as you type. The query matches against message, tag, and level name.


Triggers

Default trigger

The built-in gesture is a swipe sequence: ↑ ↓ ← →

ConsoleProvider { YourAppContent() }  // default trigger, no configuration needed

Custom swipe sequence

ConsoleProvider(
    trigger = ConsoleTrigger.swipeSequence(Swipe.UP, Swipe.DOWN)
) {
    YourAppContent()
}

Fully custom trigger

ConsoleTrigger is a fun interface — any Modifier extension that calls onDetected() qualifies:

// Double-tap anywhere on screen
val doubleTapTrigger = ConsoleTrigger { onDetected ->
    pointerInput(Unit) {
        detectTapGestures(onDoubleTap = { onDetected() })
    }
}

ConsoleProvider(trigger = doubleTapTrigger) {
    YourAppContent()
}
// Long-press trigger
val longPressTrigger = ConsoleTrigger { onDetected ->
    pointerInput(Unit) {
        detectTapGestures(onLongPress = { onDetected() })
    }
}

Network inspector

Captures HTTP traffic and renders it in the log list with method, status code, URL, headers, body, and round-trip duration. Tap any entry to see the full request/response detail.

OkHttp

// build.gradle.kts
dependencies {
    debugImplementation("io.github.thernal:console-network-core:<version>")
    debugImplementation("io.github.thernal:console-network-okhttp:<version>")
    debugImplementation("io.github.thernal:console-network-ui:<version>")
}
val client = OkHttpClient.Builder()
    .addInterceptor(ConsoleNetworkOkHttpInterceptor())
    .build()

Ktor

// build.gradle.kts
dependencies {
    debugImplementation("io.github.thernal:console-network-core:<version>")
    debugImplementation("io.github.thernal:console-network-ktor:<version>")
    debugImplementation("io.github.thernal:console-network-ui:<version>")
}
val client = HttpClient {
    install(ConsoleNetworkKtorPlugin)
}

Sensitive headers

Authorization, Cookie, Set-Cookie, X-Api-Key, and Proxy-Authorization are masked with *** by default (SensitiveHeaders.DEFAULT).

// Custom names and mask string
ConsoleNetworkOkHttpInterceptor(
    sensitiveHeaders = SensitiveHeaders(
        names = setOf("authorization", "x-session-token"),
        mask = "[redacted]",
    )
)

// Disable masking entirely
ConsoleNetworkOkHttpInterceptor(sensitiveHeaders = SensitiveHeaders.NONE)

// Same API for Ktor
HttpClient {
    install(ConsoleNetworkKtorPlugin) {
        sensitiveHeaders = SensitiveHeaders.NONE
    }
}

Details panel

A live key/value sidebar for session info, feature flags, user context, or any ambient state — visible at a glance without scrolling through logs.

// build.gradle.kts
dependencies {
    debugImplementation("io.github.thernal:console-details-ui:<version>")
    releaseImplementation("io.github.thernal:console-details-core-noop:<version>")
}
// Upsert — updates in place if the key already exists
ConsoleDetails.put("User" to "alice@example.com")
ConsoleDetails.put("Environment" to "staging")
ConsoleDetails.put("Feature:NewCheckout" to "enabled")

// Remove
ConsoleDetails.remove("Environment")

Stepper

Pauses log processing and lets you replay events one-by-one — useful for stepping through complex async flows that would otherwise scroll past instantly.

// build.gradle.kts
dependencies {
    debugImplementation("io.github.thernal:console-stepper-ui:<version>")
}

No code required. Once the module is on the classpath, the stepper control appears automatically as a floating overlay inside the console. Tap Pause to freeze the pipeline, Step to advance one event at a time, and Resume to return to live mode.


Custom tabs

Add your own full-screen view to the Console navigation bar by implementing ConsoleAddon.

// 1. Define the tab
object MetricsTab : ConsoleTab {
    override val title = "Metrics"
    override val icon = Icons.Default.BarChart
    override val order = 10  // lower = further left in the nav bar

    @Composable
    override fun Content() {
        MetricsScreen()
    }
}

// 2. Expose it via an addon
object MetricsAddon : ConsoleAddon {
    override fun tab(): ConsoleTab = MetricsTab
}

// 3. Install once at app startup
MetricsAddon.install()

Beyond tabs, ConsoleAddon also supports:

  • navGraph() — register a full navigation sub-graph behind your tab
  • overlay() — inject a floating composable on top of the console UI

Modules

Core

Artifact Description
io.github.thernal:console-core:<version> Foundation — Log, LogLevel, LogObserver, LogProcessor, ConsoleScope (no UI, no pipeline)
io.github.thernal:console-runtime:<version> Console singleton + log pipeline (depends on console-core)
io.github.thernal:console-api:<version> Addon contracts — ConsoleAddon, ConsoleTab, LogRenderer, LogRendererRegistry (depends on console-core, not runtime)
io.github.thernal:console-ui:<version> Compose UI shell — ConsoleProvider, navigation, overlay
io.github.thernal:console-ui-noop:<version> No-op stub for production builds

Addons

Artifact Description
io.github.thernal:console-logging-ui:<version> Log list, log detail screen, BasicLog renderer
io.github.thernal:console-details-ui:<version> Live key/value Details panel
io.github.thernal:console-details-core-noop:<version> No-op stub for production builds
io.github.thernal:console-network-core:<version> Shared network log types
io.github.thernal:console-network-okhttp:<version> OkHttp interceptor
io.github.thernal:console-network-ktor:<version> Ktor plugin
io.github.thernal:console-network-ui:<version> Network log UI renderer
io.github.thernal:console-stepper-ui:<version> Pause-and-step log replay

License

MIT — see LICENSE.

About

A Kotlin Multiplatform in-app debug console with addon support.

Resources

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors