-
Notifications
You must be signed in to change notification settings - Fork 0
Refactor to Declarative Component System & Color Spruce-up #14
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
01d32e7
c5eb978
a7d71ef
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| package com.github.dylanwatsonsoftware.bobatea | ||
|
|
||
| import kotlinx.coroutines.delay | ||
|
|
||
| class Boba { | ||
| companion object { | ||
| suspend fun run(terminal: Terminal, root: BobaComponent) { | ||
| terminal.clear() | ||
| terminal.enableMouseTracking(true) | ||
| var lastTick = currentTimeMillis() | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
fd 'Boba\.(jvm|wasm)\.kt$' bobatea/src | while read -r f; do
echo "== $f =="
sed -n '1,120p' "$f"
done
rg -n 'currentTimeMillis\(' bobatea/srcRepository: dylanwatsonsoftware/bobatea Length of output: 1544 Use monotonic time for frame-delta calculations instead of wall-clock time. Both Affected: lines 10, 28-30, 42 in 🤖 Prompt for AI Agents |
||
|
|
||
| // Initialize root coordinates to match terminal 1-based system | ||
| root.x = 1 | ||
| root.y = 1 | ||
|
|
||
| try { | ||
| while (true) { | ||
| val (width, height) = terminal.size() | ||
| val output = root.render(width, height) | ||
| terminal.clear() | ||
| terminal.write(output) | ||
|
|
||
| val event = terminal.readEvent(50) | ||
| if (event != null) { | ||
| root.onEvent(event) | ||
| } | ||
|
|
||
| val now = currentTimeMillis() | ||
| root.tick(now - lastTick) | ||
| lastTick = now | ||
|
|
||
| // Yield to other coroutines | ||
| delay(10) | ||
|
Comment on lines
+23
to
+33
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The idle loop is capped at roughly 16 FPS.
🤖 Prompt for AI Agents |
||
| } | ||
| } finally { | ||
| terminal.disableMouseTracking() | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| internal expect fun currentTimeMillis(): Long | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -41,49 +41,38 @@ class ExpandableComponent( | |
| result.append("Press ${color("SPACE/ENTER", GREEN)} or ${color("CLICK", GREEN)} to toggle\n") | ||
| result.append("Press ${color("Q", GREEN)} to exit") | ||
|
|
||
| return wrapInBox(result.toString().trimEnd('\n'), availableWidth, availableHeight) | ||
| val output = wrapInBox(result.toString().trimEnd('\n'), availableWidth, availableHeight) | ||
| val lines = output.lines() | ||
| widthPx = lines.maxOfOrNull { visibleLength(it) } ?: 0 | ||
| heightPx = lines.size | ||
| return output | ||
| } | ||
|
|
||
| suspend fun interact(terminal: Terminal) { | ||
| val (availableWidth, availableHeight) = terminal.size() | ||
| val titleLine = margin + (if (borderStyle != BorderStyle.NONE) 1 else 0) + padding | ||
|
|
||
| fun printExpandable() { | ||
| terminal.clear() | ||
| terminal.write(render(availableWidth, availableHeight) + "\n") | ||
| } | ||
|
|
||
| printExpandable() | ||
|
|
||
| terminal.enableMouseTracking(allMotion = true) | ||
| try { | ||
| while (true) { | ||
| when (val event = terminal.readEvent()) { | ||
| is BobaEvent.Key -> { | ||
| when (event.code) { | ||
| SPACE.key, ENTER.key -> { | ||
| expanded = !expanded | ||
| printExpandable() | ||
| } | ||
| 'q'.code, 'Q'.code -> return | ||
| } | ||
| override fun onEvent(event: BobaEvent): Boolean { | ||
| val titleLine = getContentStartY() | ||
| val titleStart = getContentStartX() | ||
| when (event) { | ||
| is BobaEvent.Key -> { | ||
| when (event.code) { | ||
| SPACE.key, ENTER.key -> { | ||
| expanded = !expanded | ||
| return true | ||
|
Comment on lines
+55
to
+59
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Gate SPACE/ENTER behind focus or parent routing. Line 56 toggles on every SPACE/ENTER event. In 🤖 Prompt for AI Agents |
||
| } | ||
| is BobaEvent.Mouse -> { | ||
| val currentlyHovered = event.y == titleLine + 1 && event.x <= title.length + 4 | ||
| if (currentlyHovered != isHovered) { | ||
| isHovered = currentlyHovered | ||
| printExpandable() | ||
| } | ||
| } | ||
| } | ||
| is BobaEvent.Mouse -> { | ||
| val currentlyHovered = event.y == titleLine && event.x >= titleStart && event.x < titleStart + title.length + 4 | ||
| if (currentlyHovered != isHovered) { | ||
| isHovered = currentlyHovered | ||
| return true | ||
| } | ||
|
|
||
| if (event.action == MouseAction.PRESS && event.button < 64 && currentlyHovered) { | ||
| expanded = !expanded | ||
| printExpandable() | ||
| } | ||
| } | ||
| if (event.action == MouseAction.PRESS && event.button < 64 && currentlyHovered) { | ||
| expanded = !expanded | ||
| return true | ||
| } | ||
| } | ||
| } finally { | ||
| terminal.disableMouseTracking() | ||
| } | ||
| return false | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,8 +23,8 @@ class Inline( | |
| val resolvedWidth = BobaComponent.resolveDimension(width, availableWidth) | ||
| val resolvedHeight = BobaComponent.resolveDimension(height, availableHeight) | ||
|
|
||
| val borderSize = if (borderStyle != BorderStyle.NONE) 2 else 0 | ||
| val horizontalTotal = padding * 2 + borderSize | ||
| val borderSizeVal = if (this.borderStyle != BorderStyle.NONE) 2 else 0 | ||
| val horizontalTotal = this.padding * 2 + borderSizeVal | ||
|
|
||
| val innerAvailableWidth = (resolvedWidth ?: availableWidth)?.let { max(0, it - horizontalTotal) } | ||
| val childAvailableWidth = if (children.isNotEmpty()) { | ||
|
|
@@ -33,19 +33,44 @@ class Inline( | |
| null | ||
| } | ||
|
|
||
| var currentX = this.x + this.padding + (if (this.borderStyle != BorderStyle.NONE) 1 else 0) | ||
| val t = table { | ||
| borderType = BorderType.BLANK | ||
| padding(0) | ||
| body { | ||
| row { | ||
| children.forEach { child -> | ||
| cell(Text(child.render(childAvailableWidth, resolvedHeight))) | ||
| val renderedChild = child.render(childAvailableWidth, resolvedHeight) | ||
| val lines = renderedChild.lines() | ||
|
|
||
| child.x = currentX | ||
| child.y = this@Inline.y + this@Inline.padding + (if (this@Inline.borderStyle != BorderStyle.NONE) 1 else 0) | ||
| child.widthPx = lines.maxOfOrNull { visibleLength(it) } ?: 0 | ||
| child.heightPx = lines.size | ||
|
|
||
| currentX += child.widthPx | ||
| cell(Text(renderedChild)) | ||
|
Comment on lines
42
to
+52
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Set the child's position before calling Nested containers use their own Suggested fix row {
children.forEach { child ->
- val renderedChild = child.render(childAvailableWidth, resolvedHeight)
- val lines = renderedChild.lines()
-
child.x = currentX
child.y = this@Inline.y + this@Inline.padding + (if (this@Inline.borderStyle != BorderStyle.NONE) 1 else 0)
+ val renderedChild = child.render(childAvailableWidth, resolvedHeight)
+ val lines = renderedChild.lines()
child.widthPx = lines.maxOfOrNull { visibleLength(it) } ?: 0
child.heightPx = lines.size
currentX += child.widthPx
cell(Text(renderedChild))🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| val content = getMordant().render(t) | ||
| return wrapInBox(content, availableWidth, availableHeight) | ||
| val wrapped = wrapInBox(content, availableWidth, availableHeight) | ||
| val wrappedLines = wrapped.lines() | ||
| widthPx = wrappedLines.maxOfOrNull { visibleLength(it) } ?: 0 | ||
| heightPx = wrappedLines.size | ||
| return wrapped | ||
| } | ||
|
|
||
| override fun onEvent(event: BobaEvent): Boolean { | ||
| for (child in children) { | ||
| if (child.onEvent(event)) return true | ||
| } | ||
| return super.onEvent(event) | ||
| } | ||
|
|
||
| override fun tick(deltaMs: Long) { | ||
| children.forEach { it.tick(deltaMs) } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The back-button hitbox is still a fixed one-row, 16-cell box.
render()goes throughwrapInBox(...), butonEventassumes the label always occupies one row and exactly 16 cells. That can miss clicks on narrow layouts and on terminals where←renders wider than one cell. Reuse the label's measured bounds instead of hard-coding them.Also applies to: 33-37
🤖 Prompt for AI Agents