Skip to content
Draft
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 gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ android.r8.strictFullModeForKeepRules=false

#Publishing
wiretap.group=dev.skymansandy
wiretap.version=1.0.0-RC15
wiretap.version=1.0.0-RC16

# KSP Fix
ksp.incremental=false
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File

internal actual fun shareHttpLogs(subject: String, text: String): String? {
internal actual fun shareLogText(subject: String, text: String): String? {
val context = WiretapContextProvider.context
val intent = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
Expand All @@ -29,16 +29,16 @@ internal actual fun shareHttpLogs(subject: String, text: String): String? {
return null
}

internal actual fun shareHttpLogAsFile(content: String) {
internal actual fun shareLogAsFile(content: String, fileName: String): String? {
CoroutineScope(Dispatchers.IO).launch {
try {
val context = WiretapContextProvider.context
val shareDir = File(context.cacheDir, SHARE_DIR_NAME).apply { mkdirs() }
val file = File(shareDir, HTTP_LOG_FILE_NAME)
val file = File(shareDir, fileName)
file.writeText(content, Charsets.UTF_8)

val authority = "${context.packageName}.wiretap.fileprovider"
val uri = "content://$authority/$HTTP_LOG_FILE_NAME".toUri()
val uri = "content://$authority/$fileName".toUri()

withContext(Dispatchers.Main) {
val intent = Intent(Intent.ACTION_SEND).apply {
Expand All @@ -57,4 +57,5 @@ internal actual fun shareHttpLogAsFile(content: String) {
// Silently fail -- never crash the host app
}
}
return null
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,8 @@ package dev.skymansandy.wiretap.helper.util

import dev.skymansandy.wiretap.domain.model.HttpLog

internal const val SHARE_DIR_NAME = "wiretap_share"
internal const val HTTP_LOG_FILE_NAME = "wiretap_http_log.txt"

internal expect fun shareHttpLogs(subject: String, text: String): String?

internal expect fun shareHttpLogAsFile(content: String)

internal fun buildShareText(entry: HttpLog): String = buildString {
appendLine("${entry.method} ${entry.responseCode}")
appendLine(entry.url)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
* Copyright (c) 2026 skymansandy. All rights reserved.
*/

package dev.skymansandy.wiretap.helper.util

internal const val SHARE_DIR_NAME = "wiretap_share"

internal expect fun shareLogText(subject: String, text: String): String?

internal expect fun shareLogAsFile(content: String, fileName: String): String?
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright (c) 2026 skymansandy. All rights reserved.
*/

package dev.skymansandy.wiretap.helper.util

import dev.skymansandy.wiretap.domain.model.SocketConnection
import dev.skymansandy.wiretap.domain.model.SocketContentType
import dev.skymansandy.wiretap.domain.model.SocketMessage
import dev.skymansandy.wiretap.domain.model.SocketMessageType

internal const val SOCKET_LOG_FILE_NAME = "wiretap_socket_log.txt"

internal fun buildSocketShareText(
connection: SocketConnection,
messages: List<SocketMessage>,
): String = buildString {
appendLine("WS ${connection.url}")
appendLine("Status: ${connection.status.name}")
appendLine("Opened: ${formatTime(connection.timestamp)}")
connection.closedAt?.let { appendLine("Closed: ${formatTime(it)}") }
connection.closeCode?.let { appendLine("Close Code: $it") }
connection.closeReason?.let { appendLine("Close Reason: $it") }
connection.failureMessage?.let { appendLine("Error: $it") }
connection.protocol?.let { appendLine("Protocol: $it") }
connection.remoteAddress?.let { appendLine("Remote Address: $it") }
appendLine()
appendLine("--- Request Headers ---")
if (connection.requestHeaders.isEmpty()) {
appendLine("(none)")
} else {
connection.requestHeaders.forEach { (k, v) -> appendLine("$k: $v") }
}
appendLine()
appendLine("--- Messages (${messages.size}) ---")
if (messages.isEmpty()) {
append("(none)")
} else {
messages.forEachIndexed { index, message ->
append(formatMessageLine(message))
if (index < messages.lastIndex) appendLine()
}
}
}

private fun formatMessageLine(message: SocketMessage): String {
val time = formatTime(message.timestamp)
val bytes = formatBytes(message.byteCount)
return when (message.contentType) {
SocketContentType.Text, SocketContentType.Binary -> {
val arrow = if (message.direction == SocketMessageType.Sent) ">>" else "<<"
val tag = if (message.direction == SocketMessageType.Sent) "SENT" else "RECV"
"[$time] $arrow $tag [${message.contentType.name}, $bytes] ${message.content}"
}

SocketContentType.Ping,
SocketContentType.Pong,
SocketContentType.Close,
-> {
val suffix = message.content.takeIf { it.isNotBlank() }?.let { " — $it" } ?: ""
"[$time] -- ${message.contentType.name.uppercase()}$suffix"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright (c) 2026 skymansandy. All rights reserved.
*/

package dev.skymansandy.wiretap.helper.util

import dev.skymansandy.wiretap.domain.model.SseConnection
import dev.skymansandy.wiretap.domain.model.SseEvent

internal const val SSE_LOG_FILE_NAME = "wiretap_sse_log.txt"

internal fun buildSseShareText(
connection: SseConnection,
events: List<SseEvent>,
): String = buildString {
appendLine("SSE ${connection.url}")
appendLine("Status: ${connection.status.name}")
appendLine("Opened: ${formatTime(connection.timestamp)}")
connection.closedAt?.let { appendLine("Closed: ${formatTime(it)}") }
connection.failureMessage?.let { appendLine("Error: $it") }
connection.lastEventId?.let { appendLine("Last Event ID: $it") }
connection.retryMs?.let { appendLine("Retry: ${it}ms") }
appendLine()
appendLine("--- Request Headers ---")
if (connection.requestHeaders.isEmpty()) {
appendLine("(none)")
} else {
connection.requestHeaders.forEach { (k, v) -> appendLine("$k: $v") }
}
appendLine()
appendLine("--- Events (${events.size}) ---")
if (events.isEmpty()) {
append("(none)")
} else {
events.forEachIndexed { index, event ->
append(formatEventBlock(event))
if (index < events.lastIndex) appendLine()
}
}
}

private fun formatEventBlock(event: SseEvent): String = buildString {
val time = formatTime(event.timestamp)
val bytes = formatBytes(event.byteCount)
val header = buildString {
append("[$time]")
event.eventType?.let { append(" event: $it") }
event.eventId?.let { append(" id: $it") }
append(" ($bytes)")
}
appendLine(header)
append(event.data)
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withStyle
import dev.skymansandy.wiretap.ui.theme.WiretapColors

internal fun highlightText(text: String, query: String): AnnotatedString {
internal fun highlightText(
text: String,
query: String,
activeRange: IntRange? = null,
): AnnotatedString {
if (query.isBlank()) return AnnotatedString(text)

return buildAnnotatedString {
Expand All @@ -21,7 +25,15 @@ internal fun highlightText(text: String, query: String): AnnotatedString {
var match = lowerText.indexOf(lowerQuery, cursor)
while (match >= 0) {
append(text.substring(cursor, match))
withStyle(SpanStyle(background = WiretapColors.SearchHighlightBackground, color = Color.Black)) {
val isActive = activeRange != null &&
match == activeRange.first &&
match + query.length - 1 == activeRange.last
val background = if (isActive) {
WiretapColors.SearchHighlightActiveBackground
} else {
WiretapColors.SearchHighlightBackground
}
withStyle(SpanStyle(background = background, color = Color.Black)) {
append(text.substring(match, match + query.length))
}
cursor = match + query.length
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,27 @@ import dev.skymansandy.wiretap.domain.model.SocketMessage
import dev.skymansandy.wiretap.domain.model.SocketMessageType
import dev.skymansandy.wiretap.helper.util.formatBytes
import dev.skymansandy.wiretap.helper.util.formatTime
import dev.skymansandy.wiretap.helper.util.highlightText

@Composable
internal fun MessageBubble(
modifier: Modifier = Modifier,
message: SocketMessage,
searchQuery: String = "",
activeMatchRange: IntRange? = null,
) {
when (message.contentType) {
SocketContentType.Ping,
SocketContentType.Pong,
SocketContentType.Close,
-> ControlFrameLabel(modifier = modifier, message = message)

else -> DataFrameBubble(modifier = modifier, message = message)
else -> DataFrameBubble(
modifier = modifier,
message = message,
searchQuery = searchQuery,
activeMatchRange = activeMatchRange,
)
}
}

Expand Down Expand Up @@ -70,6 +78,8 @@ private fun ControlFrameLabel(
private fun DataFrameBubble(
modifier: Modifier = Modifier,
message: SocketMessage,
searchQuery: String = "",
activeMatchRange: IntRange? = null,
) {
val isSent = message.direction == SocketMessageType.Sent
val alignment = if (isSent) Alignment.CenterEnd else Alignment.CenterStart
Expand All @@ -95,7 +105,7 @@ private fun DataFrameBubble(
.padding(10.dp),
) {
Text(
text = message.content,
text = highlightText(message.content, searchQuery, activeMatchRange),
style = MaterialTheme.typography.bodySmall,
fontFamily = FontFamily.Monospace,
color = textColor,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.tooling.preview.Preview
Expand All @@ -40,6 +42,7 @@ internal fun SearchField(
value = query,
onValueChange = onQueryChange,
textStyle = MaterialTheme.typography.bodyLarge.copy(color = LocalContentColor.current),
cursorBrush = SolidColor(Color.White),
singleLine = true,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() }),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,11 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import dev.skymansandy.wiretap.domain.model.HttpLog
import dev.skymansandy.wiretap.domain.model.ResponseSource
import dev.skymansandy.wiretap.helper.util.HTTP_LOG_FILE_NAME
import dev.skymansandy.wiretap.helper.util.buildCurlCommand
import dev.skymansandy.wiretap.helper.util.buildShareText
import dev.skymansandy.wiretap.helper.util.shareHttpLogAsFile
import dev.skymansandy.wiretap.helper.util.shareHttpLogs
import dev.skymansandy.wiretap.helper.util.shareLogAsFile
import dev.skymansandy.wiretap.helper.util.shareLogText
import dev.skymansandy.wiretap.navigation.api.WiretapScreen
import dev.skymansandy.wiretap.navigation.compose.LocalWiretapNavigator
import dev.skymansandy.wiretap.ui.common.LocalSnackbarHostState
Expand Down Expand Up @@ -220,7 +221,7 @@ private fun HttpLogDetailScreenContent(
text = { Text("Share as text") },
onClick = {
showShareMenu = false
val message = shareHttpLogs(
val message = shareLogText(
subject = "${entry.method} ${entry.responseCode} - ${entry.url}",
text = buildShareText(entry),
)
Expand All @@ -232,7 +233,7 @@ private fun HttpLogDetailScreenContent(
text = { Text("Share as cURL") },
onClick = {
showShareMenu = false
val message = shareHttpLogs(
val message = shareLogText(
subject = "cURL - ${entry.method} ${entry.url}",
text = buildCurlCommand(entry),
)
Expand All @@ -244,8 +245,9 @@ private fun HttpLogDetailScreenContent(
text = { Text("Share as file") },
onClick = {
showShareMenu = false
shareHttpLogAsFile(
shareLogAsFile(
content = buildShareText(entry),
fileName = HTTP_LOG_FILE_NAME,
)
},
)
Expand Down
Loading