style(tui): match epic-13 reference layout — titled panel blocks, status indicator, keybinds bar
This commit is contained in:
@@ -1,9 +1,11 @@
|
|||||||
package com.correx.apps.tui
|
package com.correx.apps.tui
|
||||||
|
|
||||||
import com.correx.apps.tui.components.inputBarLine
|
import com.correx.apps.tui.components.activeSessionWidget
|
||||||
import com.correx.apps.tui.components.renderApprovalPanel
|
import com.correx.apps.tui.components.approvalPanelWidget
|
||||||
|
import com.correx.apps.tui.components.filteredSessions
|
||||||
|
import com.correx.apps.tui.components.inputBarWidget
|
||||||
import com.correx.apps.tui.components.sessionListWidget
|
import com.correx.apps.tui.components.sessionListWidget
|
||||||
import com.correx.apps.tui.components.statusBarLine
|
import com.correx.apps.tui.components.statusBarWidget
|
||||||
import com.correx.apps.tui.input.Action
|
import com.correx.apps.tui.input.Action
|
||||||
import com.correx.apps.tui.input.KeyResolver
|
import com.correx.apps.tui.input.KeyResolver
|
||||||
import com.correx.apps.tui.input.mapKey
|
import com.correx.apps.tui.input.mapKey
|
||||||
@@ -13,14 +15,17 @@ import com.correx.apps.tui.state.InputMode
|
|||||||
import com.correx.apps.tui.state.TuiState
|
import com.correx.apps.tui.state.TuiState
|
||||||
import com.correx.apps.tui.ws.ConnectionEvent
|
import com.correx.apps.tui.ws.ConnectionEvent
|
||||||
import com.correx.apps.tui.ws.TuiWsClient
|
import com.correx.apps.tui.ws.TuiWsClient
|
||||||
import dev.tamboui.layout.Rect
|
import dev.tamboui.layout.Constraint
|
||||||
|
import dev.tamboui.layout.Layout
|
||||||
import dev.tamboui.style.Style
|
import dev.tamboui.style.Style
|
||||||
import dev.tamboui.terminal.Frame
|
import dev.tamboui.terminal.Frame
|
||||||
import dev.tamboui.text.Text
|
|
||||||
import dev.tamboui.tui.EventHandler
|
import dev.tamboui.tui.EventHandler
|
||||||
import dev.tamboui.tui.Renderer
|
import dev.tamboui.tui.Renderer
|
||||||
import dev.tamboui.tui.TuiRunner
|
import dev.tamboui.tui.TuiRunner
|
||||||
import dev.tamboui.tui.event.KeyEvent as TambouiKeyEvent
|
import dev.tamboui.tui.event.KeyEvent as TambouiKeyEvent
|
||||||
|
import dev.tamboui.widgets.block.Block
|
||||||
|
import dev.tamboui.widgets.block.BorderType
|
||||||
|
import dev.tamboui.widgets.block.Borders
|
||||||
import dev.tamboui.widgets.list.ListState
|
import dev.tamboui.widgets.list.ListState
|
||||||
import dev.tamboui.widgets.paragraph.Paragraph
|
import dev.tamboui.widgets.paragraph.Paragraph
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@@ -30,9 +35,13 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
|
||||||
private const val DEFAULT_PORT = 8080
|
private const val DEFAULT_PORT = 8080
|
||||||
|
private const val STATUS_HEIGHT = 3
|
||||||
|
private const val ACTIVE_SESSION_HEIGHT = 5
|
||||||
private const val APPROVAL_HEIGHT = 6
|
private const val APPROVAL_HEIGHT = 6
|
||||||
private const val STATUS_ROWS = 1
|
private const val INPUT_HEIGHT = 3
|
||||||
private const val HELP_ROWS = 1
|
private const val KEYBINDS_HEIGHT = 3
|
||||||
|
|
||||||
|
private const val KEYBINDS_TEXT = "n new c cancel a approve r reject s steer / filter q quit"
|
||||||
|
|
||||||
fun main(args: Array<String>) {
|
fun main(args: Array<String>) {
|
||||||
val host = args.getOrElse(0) { "localhost" }
|
val host = args.getOrElse(0) { "localhost" }
|
||||||
@@ -88,56 +97,55 @@ fun main(args: Array<String>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun render(frame: Frame, state: TuiState, listState: ListState) {
|
private fun render(frame: Frame, state: TuiState, listState: ListState) {
|
||||||
val total = frame.area()
|
val area = frame.area()
|
||||||
val hasInput = state.input.mode != InputMode.None
|
val hasInput = state.input.mode != InputMode.None
|
||||||
val listHeight = if (hasInput) {
|
val hasApproval = state.approval.active != null
|
||||||
total.height() - STATUS_ROWS - HELP_ROWS - 1
|
|
||||||
} else {
|
|
||||||
total.height() - STATUS_ROWS - HELP_ROWS
|
|
||||||
}
|
|
||||||
val statusArea = Rect(total.x(), total.y(), total.width(), STATUS_ROWS)
|
|
||||||
val listArea = Rect(total.x(), total.y() + STATUS_ROWS, total.width(), listHeight)
|
|
||||||
val inputArea = if (hasInput) {
|
|
||||||
Rect(total.x(), total.y() + STATUS_ROWS + listHeight, total.width(), 1)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
val helpArea = Rect(total.x(), total.y() + total.height() - HELP_ROWS, total.width(), HELP_ROWS)
|
|
||||||
|
|
||||||
|
val constraints = buildList {
|
||||||
|
add(Constraint.length(STATUS_HEIGHT))
|
||||||
|
add(Constraint.fill())
|
||||||
|
add(Constraint.length(ACTIVE_SESSION_HEIGHT))
|
||||||
|
if (hasApproval) add(Constraint.length(APPROVAL_HEIGHT))
|
||||||
|
if (hasInput) add(Constraint.length(INPUT_HEIGHT))
|
||||||
|
add(Constraint.length(KEYBINDS_HEIGHT))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("SpreadOperator")
|
||||||
|
val rects = Layout.vertical()
|
||||||
|
.constraints(*constraints.toTypedArray())
|
||||||
|
.split(area)
|
||||||
|
|
||||||
|
var idx = 0
|
||||||
|
|
||||||
|
frame.renderWidget(statusBarWidget(state), rects[idx++])
|
||||||
|
|
||||||
|
val filtered = filteredSessions(state.sessions)
|
||||||
|
val selectedSession = filtered.firstOrNull { it.id == state.sessions.selectedId }
|
||||||
|
?: filtered.firstOrNull()
|
||||||
|
frame.renderStatefulWidget(sessionListWidget(state.sessions, state.approval, listState), rects[idx++], listState)
|
||||||
|
|
||||||
|
frame.renderWidget(activeSessionWidget(selectedSession), rects[idx++])
|
||||||
|
|
||||||
|
if (hasApproval) {
|
||||||
|
frame.renderWidget(approvalPanelWidget(state.approval.active!!), rects[idx++])
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasInput) {
|
||||||
|
inputBarWidget(state.input)?.let { frame.renderWidget(it, rects[idx]) }
|
||||||
|
idx++
|
||||||
|
}
|
||||||
|
|
||||||
|
val keybindsBlock = Block.builder()
|
||||||
|
.title("keybinds")
|
||||||
|
.borders(Borders.ALL)
|
||||||
|
.borderType(BorderType.ROUNDED)
|
||||||
|
.build()
|
||||||
frame.renderWidget(
|
frame.renderWidget(
|
||||||
Paragraph.builder().text(Text.from(statusBarLine(state))).build(),
|
Paragraph.builder()
|
||||||
statusArea
|
.text(KEYBINDS_TEXT)
|
||||||
)
|
.style(Style.create().dim().gray())
|
||||||
|
.block(keybindsBlock)
|
||||||
frame.renderStatefulWidget(sessionListWidget(state.sessions, listState), listArea, listState)
|
.build(),
|
||||||
|
rects[idx]
|
||||||
if (inputArea != null) {
|
|
||||||
val line = inputBarLine(state.input)
|
|
||||||
if (line != null) {
|
|
||||||
frame.renderWidget(
|
|
||||||
Paragraph.builder().text(Text.from(line)).build(),
|
|
||||||
inputArea
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val approvalActive = state.approval.active
|
|
||||||
if (approvalActive != null && listHeight > APPROVAL_HEIGHT) {
|
|
||||||
val approvalY = total.y() + STATUS_ROWS + (listHeight - APPROVAL_HEIGHT).coerceAtLeast(0)
|
|
||||||
val approvalArea = Rect(total.x(), approvalY, total.width(), APPROVAL_HEIGHT.coerceAtMost(listHeight))
|
|
||||||
renderApprovalPanel(frame, approvalActive, approvalArea)
|
|
||||||
}
|
|
||||||
|
|
||||||
val dimStyle = Style.create().dim().gray()
|
|
||||||
frame.renderWidget(
|
|
||||||
Paragraph.builder().text(helpText(state.input.mode)).style(dimStyle).left().build(),
|
|
||||||
helpArea
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun helpText(mode: InputMode): String = when (mode) {
|
|
||||||
InputMode.None -> "q quit ↑/↓ select n new / filter a approve r reject s steer c cancel"
|
|
||||||
InputMode.Filter -> "enter apply esc cancel"
|
|
||||||
InputMode.WorkflowId -> "enter start session esc cancel"
|
|
||||||
InputMode.SteeringNote -> "enter send note esc cancel"
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,21 +1,28 @@
|
|||||||
package com.correx.apps.tui.components
|
package com.correx.apps.tui.components
|
||||||
|
|
||||||
import com.correx.apps.tui.state.SessionSummary
|
import com.correx.apps.tui.state.SessionSummary
|
||||||
import dev.tamboui.layout.Rect
|
|
||||||
import dev.tamboui.terminal.Frame
|
|
||||||
import dev.tamboui.text.Line
|
import dev.tamboui.text.Line
|
||||||
import dev.tamboui.text.Span
|
import dev.tamboui.text.Span
|
||||||
import dev.tamboui.text.Text
|
import dev.tamboui.text.Text
|
||||||
|
import dev.tamboui.widgets.block.Block
|
||||||
|
import dev.tamboui.widgets.block.BorderType
|
||||||
|
import dev.tamboui.widgets.block.Borders
|
||||||
import dev.tamboui.widgets.paragraph.Paragraph
|
import dev.tamboui.widgets.paragraph.Paragraph
|
||||||
|
|
||||||
fun renderActiveSession(frame: Frame, session: SessionSummary?, area: Rect) {
|
fun activeSessionWidget(session: SessionSummary?): Paragraph {
|
||||||
val text = if (session == null) {
|
val text = if (session == null) {
|
||||||
Text.from(Line.from(Span.raw("(no active session selected)")))
|
Text.from(Line.from(Span.raw("(no session selected)")))
|
||||||
} else {
|
} else {
|
||||||
Text.from(
|
val lines = buildList<Line> {
|
||||||
Line.from(Span.raw("stage: ${session.currentStage ?: "—"}")),
|
add(Line.from(Span.raw("stage: ${session.currentStage ?: "—"}")))
|
||||||
Line.from(Span.raw("last output: ${session.lastOutput ?: "—"}"))
|
session.lastOutput?.let { add(Line.from(Span.raw("last output: $it"))) }
|
||||||
)
|
|
||||||
}
|
}
|
||||||
frame.renderWidget(Paragraph.builder().text(text).build(), area)
|
Text.from(lines)
|
||||||
|
}
|
||||||
|
val block = Block.builder()
|
||||||
|
.title("active session")
|
||||||
|
.borders(Borders.ALL)
|
||||||
|
.borderType(BorderType.ROUNDED)
|
||||||
|
.build()
|
||||||
|
return Paragraph.builder().text(text).block(block).build()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
package com.correx.apps.tui.components
|
package com.correx.apps.tui.components
|
||||||
|
|
||||||
import com.correx.apps.tui.state.ApprovalInfo
|
import com.correx.apps.tui.state.ApprovalInfo
|
||||||
import dev.tamboui.layout.Rect
|
|
||||||
import dev.tamboui.style.Color
|
import dev.tamboui.style.Color
|
||||||
import dev.tamboui.style.Style
|
import dev.tamboui.style.Style
|
||||||
import dev.tamboui.terminal.Frame
|
|
||||||
import dev.tamboui.text.Line
|
import dev.tamboui.text.Line
|
||||||
import dev.tamboui.text.Span
|
import dev.tamboui.text.Span
|
||||||
import dev.tamboui.text.Text
|
import dev.tamboui.text.Text
|
||||||
@@ -13,23 +11,22 @@ import dev.tamboui.widgets.block.BorderType
|
|||||||
import dev.tamboui.widgets.block.Borders
|
import dev.tamboui.widgets.block.Borders
|
||||||
import dev.tamboui.widgets.paragraph.Paragraph
|
import dev.tamboui.widgets.paragraph.Paragraph
|
||||||
|
|
||||||
fun renderApprovalPanel(frame: Frame, approval: ApprovalInfo, area: Rect) {
|
fun approvalPanelWidget(approval: ApprovalInfo): Paragraph {
|
||||||
val dimStyle = Style.create().dim().gray()
|
val dimStyle = Style.create().dim().gray()
|
||||||
val lines = buildList<Line> {
|
val lines = buildList<Line> {
|
||||||
add(Line.from(Span.styled("⚠ APPROVAL REQUIRED — Tier ${approval.tier}", Style.create().yellow().bold())))
|
add(Line.from(Span.styled("⚠ APPROVAL REQUIRED — Tier ${approval.tier}", Style.create().yellow().bold())))
|
||||||
approval.toolName?.let { add(Line.from(Span.raw("tool: $it"))) }
|
approval.toolName?.let { add(Line.from(Span.raw("tool: $it"))) }
|
||||||
|
if (approval.riskSummary.isNotEmpty()) {
|
||||||
add(Line.from(Span.raw("risk: ${approval.riskSummary}")))
|
add(Line.from(Span.raw("risk: ${approval.riskSummary}")))
|
||||||
|
}
|
||||||
approval.preview?.let { add(Line.from(Span.raw("preview: $it"))) }
|
approval.preview?.let { add(Line.from(Span.raw("preview: $it"))) }
|
||||||
add(Line.from(Span.styled("[A] approve [R] reject", dimStyle)))
|
add(Line.from(Span.styled("[A] approve [R] reject [S] steer [D] details", dimStyle)))
|
||||||
}
|
}
|
||||||
val block = Block.builder()
|
val block = Block.builder()
|
||||||
.title("approval")
|
.title("approval prompt")
|
||||||
.borders(Borders.ALL)
|
.borders(Borders.ALL)
|
||||||
.borderType(BorderType.ROUNDED)
|
.borderType(BorderType.ROUNDED)
|
||||||
.borderColor(Color.YELLOW)
|
.borderColor(Color.YELLOW)
|
||||||
.build()
|
.build()
|
||||||
frame.renderWidget(
|
return Paragraph.builder().text(Text.from(lines)).block(block).build()
|
||||||
Paragraph.builder().text(Text.from(lines)).block(block).build(),
|
|
||||||
area
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,17 +5,28 @@ import com.correx.apps.tui.state.InputState
|
|||||||
import dev.tamboui.style.Style
|
import dev.tamboui.style.Style
|
||||||
import dev.tamboui.text.Line
|
import dev.tamboui.text.Line
|
||||||
import dev.tamboui.text.Span
|
import dev.tamboui.text.Span
|
||||||
|
import dev.tamboui.text.Text
|
||||||
|
import dev.tamboui.widgets.block.Block
|
||||||
|
import dev.tamboui.widgets.block.BorderType
|
||||||
|
import dev.tamboui.widgets.block.Borders
|
||||||
|
import dev.tamboui.widgets.paragraph.Paragraph
|
||||||
|
|
||||||
fun inputBarLine(input: InputState): Line? {
|
fun inputBarWidget(input: InputState): Paragraph? {
|
||||||
if (input.mode == InputMode.None) return null
|
if (input.mode == InputMode.None) return null
|
||||||
val (promptText, promptStyle) = when (input.mode) {
|
val (promptText, promptStyle) = when (input.mode) {
|
||||||
InputMode.WorkflowId -> "new session › " to Style.create().cyan()
|
InputMode.WorkflowId -> "new session > " to Style.create().cyan()
|
||||||
InputMode.Filter -> "filter › " to Style.create().yellow()
|
InputMode.Filter -> "filter > " to Style.create().yellow()
|
||||||
InputMode.SteeringNote -> "steer › " to Style.create().magenta()
|
InputMode.SteeringNote -> "steer > " to Style.create().magenta()
|
||||||
InputMode.None -> "" to Style.create()
|
InputMode.None -> "" to Style.create()
|
||||||
}
|
}
|
||||||
return Line.from(
|
val line = Line.from(
|
||||||
Span.styled(promptText, promptStyle),
|
Span.styled(promptText, promptStyle),
|
||||||
Span.raw(input.text + "█")
|
Span.raw("${input.text}_")
|
||||||
)
|
)
|
||||||
|
val block = Block.builder()
|
||||||
|
.title("input")
|
||||||
|
.borders(Borders.ALL)
|
||||||
|
.borderType(BorderType.ROUNDED)
|
||||||
|
.build()
|
||||||
|
return Paragraph.builder().text(Text.from(line)).block(block).build()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.correx.apps.tui.components
|
package com.correx.apps.tui.components
|
||||||
|
|
||||||
|
import com.correx.apps.tui.state.ApprovalState
|
||||||
import com.correx.apps.tui.state.SessionSummary
|
import com.correx.apps.tui.state.SessionSummary
|
||||||
import com.correx.apps.tui.state.SessionsState
|
import com.correx.apps.tui.state.SessionsState
|
||||||
import dev.tamboui.style.Style
|
import dev.tamboui.style.Style
|
||||||
@@ -17,9 +18,9 @@ private const val SEC_PER_MIN = 60L
|
|||||||
private const val SEC_PER_HOUR = 3600L
|
private const val SEC_PER_HOUR = 3600L
|
||||||
private const val ID_LEN = 6
|
private const val ID_LEN = 6
|
||||||
|
|
||||||
fun sessionListWidget(sessions: SessionsState, listState: ListState): ListWidget {
|
fun sessionListWidget(sessions: SessionsState, approval: ApprovalState, listState: ListState): ListWidget {
|
||||||
val filtered = filteredSessions(sessions)
|
val filtered = filteredSessions(sessions)
|
||||||
val items = filtered.map { ListItem.from(formatSessionItem(it)) }
|
val items = filtered.map { ListItem.from(formatSessionItem(it, approval)) }
|
||||||
val selIdx = filtered.indexOfFirst { it.id == sessions.selectedId }.let { if (it < 0) 0 else it }
|
val selIdx = filtered.indexOfFirst { it.id == sessions.selectedId }.let { if (it < 0) 0 else it }
|
||||||
listState.select(selIdx)
|
listState.select(selIdx)
|
||||||
@Suppress("SpreadOperator")
|
@Suppress("SpreadOperator")
|
||||||
@@ -29,7 +30,7 @@ fun sessionListWidget(sessions: SessionsState, listState: ListState): ListWidget
|
|||||||
.highlightSymbol("▶ ")
|
.highlightSymbol("▶ ")
|
||||||
.block(
|
.block(
|
||||||
Block.builder()
|
Block.builder()
|
||||||
.title("sessions")
|
.title("session list")
|
||||||
.borders(Borders.ALL)
|
.borders(Borders.ALL)
|
||||||
.borderType(BorderType.ROUNDED)
|
.borderType(BorderType.ROUNDED)
|
||||||
.build()
|
.build()
|
||||||
@@ -43,23 +44,30 @@ fun filteredSessions(sessions: SessionsState): List<SessionSummary> {
|
|||||||
else sessions.sessions.filter { it.workflowId.contains(f, ignoreCase = true) }
|
else sessions.sessions.filter { it.workflowId.contains(f, ignoreCase = true) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun formatSessionItem(s: SessionSummary): Line {
|
fun formatSessionItem(s: SessionSummary, approval: ApprovalState): Line {
|
||||||
val dimStyle = Style.create().dim().gray()
|
val dimStyle = Style.create().dim().gray()
|
||||||
val statusSpan = when (s.status) {
|
val statusSpan = when (s.status.lowercase()) {
|
||||||
"running" -> Span.styled(s.status, Style.create().green())
|
"running", "active" -> Span.styled(s.status.uppercase(), Style.create().green())
|
||||||
"error" -> Span.styled(s.status, Style.create().red())
|
"error", "failed" -> Span.styled(s.status.uppercase(), Style.create().red())
|
||||||
"paused" -> Span.styled(s.status, Style.create().yellow())
|
"paused" -> Span.styled(s.status.uppercase(), Style.create().yellow())
|
||||||
"completed" -> Span.styled(s.status, dimStyle)
|
"completed", "done" -> Span.styled(s.status.uppercase(), dimStyle)
|
||||||
"idle" -> Span.styled(s.status, Style.create().cyan())
|
"idle" -> Span.styled(s.status.uppercase(), Style.create().cyan())
|
||||||
else -> Span.raw(s.status)
|
else -> Span.raw(s.status.uppercase())
|
||||||
}
|
}
|
||||||
val stage = s.currentStage?.let { " stage $it" } ?: ""
|
val stage = s.currentStage?.let { " stage $it" } ?: ""
|
||||||
|
val awaitingApproval = approval.active?.sessionId == s.id
|
||||||
|
val trailing = if (awaitingApproval) {
|
||||||
|
Span.styled(" awaiting approval", Style.create().yellow())
|
||||||
|
} else {
|
||||||
|
Span.styled(" ${formatAgo(s.lastEventAt)}", dimStyle)
|
||||||
|
}
|
||||||
return Line.from(
|
return Line.from(
|
||||||
Span.styled("[${s.id.take(ID_LEN)}]", dimStyle),
|
Span.styled("[${s.id.take(ID_LEN)}]", dimStyle),
|
||||||
Span.raw(" \"${s.workflowId}\" "),
|
Span.raw(" \"${s.workflowId}\""),
|
||||||
|
Span.raw(" "),
|
||||||
statusSpan,
|
statusSpan,
|
||||||
Span.raw(stage),
|
Span.raw(stage),
|
||||||
Span.styled(" ${formatAgo(s.lastEventAt)}", dimStyle)
|
trailing
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,27 +4,32 @@ import com.correx.apps.tui.state.TuiState
|
|||||||
import dev.tamboui.style.Style
|
import dev.tamboui.style.Style
|
||||||
import dev.tamboui.text.Line
|
import dev.tamboui.text.Line
|
||||||
import dev.tamboui.text.Span
|
import dev.tamboui.text.Span
|
||||||
|
import dev.tamboui.text.Text
|
||||||
|
import dev.tamboui.widgets.block.Block
|
||||||
|
import dev.tamboui.widgets.block.BorderType
|
||||||
|
import dev.tamboui.widgets.block.Borders
|
||||||
|
import dev.tamboui.widgets.paragraph.Paragraph
|
||||||
|
|
||||||
private const val MS_PER_SEC = 1000L
|
private const val MS_PER_SEC = 1000L
|
||||||
|
|
||||||
fun statusBarLine(state: TuiState): Line {
|
fun statusBarWidget(state: TuiState): Paragraph {
|
||||||
val connSpans = when {
|
val connSpans = when {
|
||||||
state.connection.reconnecting -> {
|
state.connection.reconnecting -> {
|
||||||
val rem = state.connection.nextRetryAtMs?.let {
|
val rem = state.connection.nextRetryAtMs?.let {
|
||||||
maxOf(0, (it - System.currentTimeMillis()) / MS_PER_SEC)
|
maxOf(0, (it - System.currentTimeMillis()) / MS_PER_SEC)
|
||||||
} ?: 0
|
} ?: 0
|
||||||
listOf(
|
listOf(
|
||||||
Span.styled("○ ", Style.create().yellow()),
|
Span.styled("○", Style.create().yellow()),
|
||||||
Span.raw("reconnecting (attempt ${state.connection.attempt}, retry in ${rem}s)")
|
Span.raw(" reconnecting (attempt ${state.connection.attempt}, retry in ${rem}s)")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
state.connection.connected -> listOf(
|
state.connection.connected -> listOf(
|
||||||
Span.styled("● ", Style.create().green()),
|
Span.styled("●", Style.create().green()),
|
||||||
Span.raw("connected")
|
Span.raw(" connected")
|
||||||
)
|
)
|
||||||
else -> listOf(
|
else -> listOf(
|
||||||
Span.styled("○ ", Style.create().dim().gray()),
|
Span.styled("○", Style.create().dim().gray()),
|
||||||
Span.raw("disconnected")
|
Span.raw(" disconnected")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val prov = if (state.provider.id.isNotEmpty()) {
|
val prov = if (state.provider.id.isNotEmpty()) {
|
||||||
@@ -32,10 +37,20 @@ fun statusBarLine(state: TuiState): Line {
|
|||||||
} else {
|
} else {
|
||||||
state.provider.status
|
state.provider.status
|
||||||
}
|
}
|
||||||
return Line.from(
|
val count = state.sessions.sessions.size
|
||||||
|
val sep = Span.raw(" │ ")
|
||||||
|
val line = Line.from(
|
||||||
connSpans + listOf(
|
connSpans + listOf(
|
||||||
Span.raw(" │ provider: "),
|
sep,
|
||||||
Span.raw(prov)
|
Span.raw("provider: $prov"),
|
||||||
|
sep,
|
||||||
|
Span.raw("sessions: $count")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
val block = Block.builder()
|
||||||
|
.title("status bar")
|
||||||
|
.borders(Borders.ALL)
|
||||||
|
.borderType(BorderType.ROUNDED)
|
||||||
|
.build()
|
||||||
|
return Paragraph.builder().text(Text.from(line)).block(block).build()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user