A tool that takes minutes -- a build, a wide search, a command nobody can
predict -- read as a window that had hung: the card said "running" and
nothing else, forever. The chip now counts from the point the card starts
running (from five seconds on, before that it is noise), which is where a
person looks when a call seems stuck; the status line and the Stop button
were already there. A card that appears already running (a replayed
transcript) starts its clock too.
--uitest: two checks for it (a card four minutes eighteen in says
"running · 4:18"; a finished one stops counting).
And the run now writes uitest.log in its workspace, every line and the
verdict. That is the only way to read a run *on a phone*: stdout reaches
nobody there, an app started from adb cannot be handed CST_APP_SHOT or any
other environment (so the shots a desktop run can take are out), and logcat
does not carry this build's Qt messages at all -- checked: a plain
`qInfo` from the app is not in the buffer.
First device run (NX789J, 105 checks): 88 ok, 17 FAIL, every failure
pre-existing and unrelated to this work -- the settings dialog does not
open from the gear (so nothing in it is reachable: the five dialog checks,
the two provider-form ones and the four clicks that follow), Open/Share
leave no status line on the device, and the clipboard-image checks have no
clipboard. All the md_to_pdf and card checks pass on the phone, including
"black ink on paper (darkest pixel 0)" from 4c69ef3.
226 lines
8.1 KiB
QML
226 lines
8.1 KiB
QML
import QtQuick
|
|
import QtQuick.Controls
|
|
import QtQuick.Layouts
|
|
|
|
// A tool call: what ran, whether it is still running, and what came back.
|
|
// Long output stays folded until asked for, diff lines are colored.
|
|
Rectangle {
|
|
id: card
|
|
property string name: ""
|
|
property string state: "pending" // pending | ready | running | done | denied
|
|
// | unfinished (replayed, no result)
|
|
property string detail: ""
|
|
property string output: ""
|
|
property int exitCode: 0
|
|
property bool hasResult: false
|
|
property bool expanded: false
|
|
property string images: ""
|
|
|
|
readonly property bool running: state === "pending" || state === "ready" || state === "running"
|
|
readonly property color stateColor: state === "denied" ? Theme.err
|
|
: state === "unfinished" ? Theme.textFaint
|
|
: running ? Theme.warn
|
|
: hasResult && exitCode !== 0 ? Theme.err
|
|
: Theme.ok
|
|
readonly property string stateText: state === "pending" ? "reading"
|
|
: state === "ready" ? "queued"
|
|
: state === "running" ? "running"
|
|
: state === "denied" ? "denied"
|
|
: state === "unfinished" ? "no result"
|
|
: hasResult && exitCode !== 0 ? "failed" : "done"
|
|
|
|
/* How long this call has been going. A tool that takes minutes -- a
|
|
* build, a wide search, a command nobody can predict -- must not read as
|
|
* a window that has hung: while the card runs, its chip counts. */
|
|
property double startedAt: 0
|
|
property int runningFor: 0
|
|
|
|
function elapsedText() {
|
|
const s = runningFor
|
|
if (s < 60)
|
|
return s + "s"
|
|
const m = Math.floor(s / 60)
|
|
if (m < 60)
|
|
return m + ":" + (s % 60 < 10 ? "0" : "") + (s % 60)
|
|
return Math.floor(m / 60) + "h" + (m % 60 < 10 ? "0" : "") + (m % 60)
|
|
}
|
|
|
|
/* The clock starts when the card starts running -- and a card that is
|
|
* already running when it appears (a replayed transcript) starts one too,
|
|
* hence Component.onCompleted. */
|
|
function noteRunning() {
|
|
if (running && startedAt === 0)
|
|
startedAt = Date.now()
|
|
else if (!running)
|
|
startedAt = 0
|
|
}
|
|
|
|
onRunningChanged: noteRunning()
|
|
Component.onCompleted: noteRunning()
|
|
|
|
Timer {
|
|
interval: 1000
|
|
repeat: true
|
|
running: card.running && card.startedAt > 0
|
|
onTriggered: card.runningFor = Math.floor((Date.now() - card.startedAt) / 1000)
|
|
}
|
|
|
|
/* What the chip says: the state, how long it has been running (from five
|
|
* seconds on -- before that it is noise), or the exit code once there is
|
|
* a result. "no result" is not an outcome, so no exit code belongs on
|
|
* it. */
|
|
function chipText() {
|
|
if (running)
|
|
return runningFor >= 5
|
|
? stateText + " · " + elapsedText()
|
|
: stateText
|
|
return stateText + (hasResult && state !== "unfinished"
|
|
? " · exit " + exitCode : "")
|
|
}
|
|
|
|
function lines() {
|
|
const all = output.split("\n")
|
|
return all.length > 500 ? all.slice(0, 500).concat(["… " + (all.length - 500) + " more lines"]) : all
|
|
}
|
|
|
|
radius: Theme.radius
|
|
color: Theme.surfaceAlt
|
|
border.width: 1
|
|
border.color: expanded ? Theme.stateColor : Theme.border
|
|
implicitHeight: layout.implicitHeight + Theme.pad
|
|
|
|
ColumnLayout {
|
|
id: layout
|
|
anchors {
|
|
left: parent.left
|
|
right: parent.right
|
|
top: parent.top
|
|
margins: Theme.pad
|
|
}
|
|
spacing: 6
|
|
|
|
RowLayout {
|
|
spacing: Theme.gap
|
|
BusyIndicator {
|
|
visible: card.running
|
|
running: card.running
|
|
implicitWidth: 16
|
|
implicitHeight: 16
|
|
Layout.alignment: Qt.AlignVCenter
|
|
}
|
|
Rectangle {
|
|
visible: !card.running
|
|
Layout.alignment: Qt.AlignVCenter
|
|
width: 6
|
|
height: 6
|
|
radius: 3
|
|
color: card.stateColor
|
|
}
|
|
/* What kind of tool this was, from the icon theme (Theme.toolIcon
|
|
* maps the engine's names onto Breeze ones). */
|
|
ThemeIcon {
|
|
Layout.alignment: Qt.AlignVCenter
|
|
name: Theme.toolIcon(card.name)
|
|
size: 16
|
|
color: Theme.textDim
|
|
}
|
|
Text {
|
|
text: card.name
|
|
color: Theme.text
|
|
font.family: Theme.mono
|
|
font.pixelSize: Theme.monoSize
|
|
font.bold: true
|
|
}
|
|
Rectangle {
|
|
Layout.alignment: Qt.AlignVCenter
|
|
radius: Theme.radiusSm
|
|
color: "transparent"
|
|
border.width: 1
|
|
border.color: card.stateColor
|
|
implicitWidth: chip.implicitWidth + 10
|
|
implicitHeight: chip.implicitHeight + 2
|
|
Text {
|
|
id: chip
|
|
objectName: "toolChip"
|
|
anchors.centerIn: parent
|
|
text: card.chipText()
|
|
color: card.stateColor
|
|
font.pixelSize: Theme.smallSize
|
|
}
|
|
}
|
|
Text {
|
|
Layout.fillWidth: true
|
|
elide: Text.ElideRight
|
|
text: card.detail.replace(/\s+/g, " ").trim()
|
|
color: Theme.textFaint
|
|
font.family: Theme.mono
|
|
font.pixelSize: Theme.smallSize
|
|
}
|
|
ToolButton {
|
|
implicitWidth: 26
|
|
implicitHeight: 22
|
|
visible: card.output.length > 0
|
|
contentItem: ThemeIcon {
|
|
name: card.expanded ? "arrow-down" : "arrow-right"
|
|
glyph: card.expanded ? "▾" : "▸"
|
|
size: 14
|
|
color: parent.hovered ? Theme.text : Theme.textDim
|
|
}
|
|
background: Rectangle {
|
|
radius: Theme.radiusSm
|
|
color: parent.hovered ? Theme.surfaceHi : "transparent"
|
|
}
|
|
onClicked: card.expanded = !card.expanded
|
|
}
|
|
}
|
|
|
|
// What the tool made (view_image, an MCP image): shown as it is, with
|
|
// the output left folded -- the picture is the result.
|
|
ImageStrip {
|
|
Layout.fillWidth: true
|
|
visible: card.images.length > 0
|
|
text: card.images
|
|
}
|
|
|
|
// Folded view: the first line of the result, so a done card still says
|
|
// something without being opened.
|
|
Text {
|
|
Layout.fillWidth: true
|
|
visible: !card.expanded && card.output.length > 0
|
|
text: {
|
|
const l = card.output.split("\n")
|
|
return l[0] + (l.length > 1 ? " …" : "")
|
|
}
|
|
elide: Text.ElideRight
|
|
color: Theme.textFaint
|
|
font.family: Theme.mono
|
|
font.pixelSize: Theme.smallSize
|
|
}
|
|
|
|
ColumnLayout {
|
|
Layout.fillWidth: true
|
|
visible: card.expanded && card.output.length > 0
|
|
spacing: 0
|
|
Repeater {
|
|
model: card.expanded ? card.lines() : []
|
|
Text {
|
|
Layout.fillWidth: true
|
|
text: modelData.length ? modelData : " "
|
|
color: Theme.lineColor(modelData)
|
|
font.family: Theme.mono
|
|
font.pixelSize: Theme.monoSize
|
|
wrapMode: Text.NoWrap
|
|
textFormat: Text.PlainText
|
|
}
|
|
}
|
|
Text {
|
|
Layout.topMargin: 4
|
|
visible: card.output.split("\n").length > 500
|
|
text: "output truncated for the view"
|
|
color: Theme.textFaint
|
|
font.pixelSize: Theme.smallSize
|
|
}
|
|
}
|
|
}
|
|
}
|