repositories / dotfiles
dotfiles
bugabingas dorkfiles
owned by admin
quickshell/nuguland/NokoPanel.qml
Rawpragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import "Util.js" as Util
Column {
id: root
required property Theme theme
readonly property Flickable scrollView: historyFlick
required property Noko panelState
property int waveTick: 0
property int copiedIndex: -1
function roleColor(role) {
if (!theme)
return "transparent";
if (role === "warning")
return theme.warning;
if (role === "error")
return theme.error;
return theme.ui.accent;
}
function assetOk(name) {
const a = panelState && panelState.assets ? panelState.assets : ({});
if (name === "vad")
return !!(a.vad && a.vad.exists);
if (name === "model")
return !!(a.parakeet && a.parakeet.complete);
if (name === "runtime")
return !!(a.runtime && a.runtime.exists);
return false;
}
function toolOk(command) {
const deps = panelState && panelState.doctor && panelState.doctor.systemDependencies ? panelState.doctor.systemDependencies : [];
for (let i = 0; i < deps.length; i++)
if (deps[i].command === command)
return !!deps[i].exists;
return false;
}
function friendlyProblem() {
if (!panelState)
return "local speech status unknown";
if (panelState.listening)
return "capturing locally until toggled off";
if (panelState.transcribing)
return "transcribing locally";
if (panelState.installing)
return "downloading local runtime and speech models";
if (panelState.lastError.length === 0)
return "local speech ready";
return "setup required before recording";
}
function setupActionLabel() {
if (!panelState)
return "install setup";
if (panelState.installing)
return `${panelState.installLabel || "downloading"} ${Math.round(panelState.installProgress * 100)}%`;
if (panelState.ready && panelState.updateAvailable)
return "apply update";
return panelState.ready ? "" : "install setup";
}
function showSetupAction() {
return !!panelState && (panelState.installing || !panelState.ready || panelState.updateAvailable);
}
function updateAccent() {
if (!theme)
return "white";
if (!panelState || panelState.updateUnknown || panelState.updateAvailable)
return theme.warning;
if (panelState.checkingUpdates)
return theme.ui.accent;
return panelState.updateChecked ? theme.ui.accent : theme.ui.minor;
}
function waveLevel(index) {
if (!panelState || (!panelState.listening && !panelState.transcribing))
return 0.2 + (index % 3) * 0.08;
const phase = (waveTick + index * 5) / (panelState.transcribing ? 7 : 4);
return 0.24 + Math.abs(Math.sin(phase)) * (panelState.transcribing ? 0.42 : 0.7);
}
function updateLine() {
if (!panelState)
return "updates not checked";
if (panelState.checkingUpdates)
return "checking updates";
if (panelState.updateUnknown)
return "update check failed";
if (!panelState.updateChecked)
return "updates not checked";
return panelState.updateAvailable ? "update available" : "up to date";
}
width: parent ? parent.width : 0
height: parent ? parent.height : implicitHeight
spacing: 14
Component.onCompleted: if (panelState)
panelState.refresh()
Timer {
interval: 90
repeat: true
running: root.panelState && (root.panelState.listening || root.panelState.transcribing)
onTriggered: root.waveTick++
}
Timer {
id: copyFeedbackTimer
interval: 1200
onTriggered: root.copiedIndex = -1
}
PopupCard {
theme: root.theme
padding: 0
attention: root.panelState && root.panelState.role === "warning"
destructive: root.panelState && root.panelState.role === "error"
width: parent.width
height: 76
Column {
anchors.fill: parent
anchors.leftMargin: 14
anchors.rightMargin: root.showSetupAction() ? 242 : 14
anchors.topMargin: 13
anchors.bottomMargin: 12
spacing: 7
Row {
width: parent.width
spacing: 8
NuguText {
theme: root.theme
variant: "title"
text: "noko"
color: root.theme ? root.theme.content.normal : "white"
}
NuguText {
theme: root.theme
variant: "meta"
text: root.panelState ? root.panelState.status : "unknown"
color: root.theme ? root.roleColor(root.panelState ? root.panelState.role : "warning") : "white"
}
}
NuguText {
theme: root.theme
width: parent.width
variant: "meta"
text: root.friendlyProblem()
color: root.theme ? root.theme.ui.minor : "white"
}
}
NokoButton {
visible: root.showSetupAction()
anchors.right: parent.right
anchors.rightMargin: 14
anchors.verticalCenter: parent.verticalCenter
theme: root.theme
label: root.setupActionLabel()
kind: "primary"
buttonWidth: 210
onClicked: if (root.panelState) {
if (root.panelState.ready)
root.panelState.update();
else
root.panelState.install();
}
}
}
Column {
width: parent.width
spacing: 7
Row {
width: parent.width
height: 24
spacing: 8
NuguText {
id: setupTitle
anchors.verticalCenter: parent.verticalCenter
theme: root.theme
variant: "title"
font.pixelSize: 13
text: "setup"
color: root.theme ? root.theme.ui.accent : "white"
}
Item {
width: Math.max(1, parent.width - setupTitle.implicitWidth - updateBadge.width - parent.spacing * 2)
height: 1
}
PopupBadge {
id: updateBadge
anchors.verticalCenter: parent.verticalCenter
theme: root.theme
width: 136
text: root.updateLine()
icon: "●"
role: root.panelState && (root.panelState.updateUnknown || root.panelState.updateAvailable) ? "warning" : "active"
}
}
PopupCard {
theme: root.theme
padding: 0
width: parent.width
height: setupFlow.implicitHeight + 18 + (root.panelState && root.panelState.installing ? 25 : 0)
Column {
anchors.fill: parent
anchors.margins: 9
spacing: 6
Flow {
id: setupFlow
width: parent.width
spacing: 14
NokoAssetRow {
theme: root.theme
label: "voice detector"
ok: root.assetOk("vad")
}
NokoAssetRow {
theme: root.theme
label: "speech model"
ok: root.assetOk("model")
}
NokoAssetRow {
theme: root.theme
label: "local runtime"
ok: root.assetOk("runtime")
}
NokoAssetRow {
theme: root.theme
label: "mic capture"
ok: root.toolOk("pw-record")
}
NokoAssetRow {
theme: root.theme
label: "text insertion"
ok: root.toolOk("wtype")
}
}
Rectangle {
visible: root.panelState && root.panelState.installing
width: parent.width
height: 6
radius: 3
color: root.theme ? root.theme.withAlpha(root.theme.ui.minor, 0.22) : "transparent"
Rectangle {
width: parent.width * Math.max(0, Math.min(1, root.panelState ? root.panelState.installProgress : 0))
height: parent.height
radius: parent.radius
color: root.theme ? root.theme.ui.accent : "white"
}
}
NuguText {
visible: root.panelState && root.panelState.installing
theme: root.theme
variant: "meta"
font.pixelSize: 12
text: root.panelState ? root.panelState.installLabel : "downloading"
color: root.theme ? root.theme.ui.minor : "white"
}
}
}
}
Column {
width: parent.width
spacing: 7
NuguText {
theme: root.theme
variant: "title"
font.pixelSize: 13
text: "capture"
color: root.theme ? root.theme.ui.accent : "white"
}
PopupCard {
theme: root.theme
padding: 0
active: root.panelState && root.panelState.listening
width: parent.width
height: 58
Row {
anchors.centerIn: parent
spacing: 5
height: 36
Repeater {
model: 31
Rectangle {
required property int index
anchors.verticalCenter: parent.verticalCenter
width: 4
height: Math.max(5, parent.height * root.waveLevel(index))
radius: 2
color: root.theme ? (root.panelState && root.panelState.listening ? root.theme.ui.accent : root.theme.ui.minor) : "white"
opacity: root.panelState && root.panelState.ready ? 0.86 : 0.3
Behavior on height {
NumberAnimation {
duration: root.theme && root.theme.lowPowerMode ? 0 : 120
easing.type: Easing.OutCubic
}
}
}
}
}
}
NokoButton {
theme: root.theme
label: root.panelState && root.panelState.listening ? "stop capture" : (root.panelState && root.panelState.transcribing ? "transcribing…" : "start capture")
kind: root.panelState && root.panelState.listening ? "primary" : "ghost"
enabled: root.panelState && root.panelState.ready && !root.panelState.transcribing
buttonWidth: parent.width
onClicked: if (root.panelState)
root.panelState.trigger()
}
}
PopupCard {
theme: root.theme
padding: 0
width: parent.width
height: Math.max(150, root.height - y)
Column {
anchors.fill: parent
anchors.margins: 12
spacing: 7
NuguText {
theme: root.theme
variant: "title"
font.pixelSize: 13
text: "last transcripts"
color: root.theme ? root.theme.ui.accent : "white"
}
Item {
width: parent.width
height: Math.max(1, parent.height - y)
clip: true
Flickable {
id: historyFlick
anchors.fill: parent
anchors.rightMargin: contentHeight > height ? 8 : 0
clip: true
contentWidth: width
contentHeight: historyColumn.implicitHeight
boundsBehavior: Flickable.StopAtBounds
interactive: contentHeight > height
Column {
id: historyColumn
width: historyFlick.width
spacing: 7
Repeater {
model: root.panelState ? root.panelState.historyEntries : []
delegate: PopupCard {
id: transcriptEntry
required property int index
required property var modelData
readonly property string transcript: String(modelData.transcript || "")
readonly property bool copied: root.copiedIndex === index
theme: root.theme
padding: 0
width: historyColumn.width
height: entryColumn.implicitHeight + 10
Column {
id: entryColumn
anchors.left: parent.left
anchors.right: copyButton.left
anchors.top: parent.top
anchors.margins: 8
spacing: 3
NuguText {
theme: root.theme
width: parent.width
wrapMode: Text.WordWrap
text: transcriptEntry.transcript
color: root.theme ? root.theme.content.normal : "white"
}
NuguText {
theme: root.theme
variant: "meta"
width: parent.width
text: transcriptEntry.modelData.duration !== undefined ? `${transcriptEntry.modelData.duration}s` : "local history"
color: root.theme ? root.theme.ui.minor : "white"
}
}
PopupButton {
id: copyButton
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: 8
theme: root.theme
width: 58
height: 26
text: transcriptEntry.copied ? "copied" : "copy"
checked: transcriptEntry.copied
onControlFocusedChanged: if (controlFocused)
Util.revealIn(copyButton, historyFlick)
onClicked: if (transcriptEntry.transcript.length > 0) {
Quickshell.execDetached(["wl-copy", transcriptEntry.transcript]);
root.copiedIndex = transcriptEntry.index;
copyFeedbackTimer.restart();
}
}
}
}
NuguText {
visible: !root.panelState || root.panelState.historyEntries.length === 0
theme: root.theme
width: parent.width
text: "no captures yet"
color: root.theme ? root.theme.content.normal : "white"
}
}
}
Rectangle {
visible: historyFlick.contentHeight > historyFlick.height
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.right: parent.right
width: 3
radius: 2
color: root.theme ? root.theme.withAlpha(root.theme.ui.minor, 0.14) : "transparent"
Rectangle {
width: parent.width
height: Math.max(18, parent.height * historyFlick["visibleArea"]["heightRatio"])
y: Math.max(0, Math.min(parent.height - height, parent.height * historyFlick["visibleArea"]["yPosition"]))
radius: parent.radius
color: root.theme ? root.theme.withAlpha(root.theme.ui.accent, 0.55) : "white"
}
}
}
}
}
}