857 lines
29 KiB
QML
857 lines
29 KiB
QML
import QtQuick
|
|
import QtQuick.Controls
|
|
import QtQuick.Layouts
|
|
import "emojis.js" as Emojis
|
|
|
|
ApplicationWindow {
|
|
id: root
|
|
title: dbus.activeCwd ? "Ollie — " + dbus.activeCwd : "Ollie"
|
|
width: 900
|
|
height: 600
|
|
visible: true
|
|
|
|
Component.onCompleted: activator.windowOpened()
|
|
|
|
onClosing: function(close) {
|
|
if (theme.closeToTray && systemTrayAvailable) {
|
|
close.accepted = false
|
|
root.hide()
|
|
}
|
|
}
|
|
|
|
Component.onDestruction: {
|
|
if (typeof activator !== 'undefined' && activator) {
|
|
activator.windowClosed()
|
|
}
|
|
}
|
|
|
|
color: palette.window
|
|
font.family: theme.fontFamily || Qt.application.font.family
|
|
font.pointSize: theme.fontSize > 0 ? theme.fontSize : Qt.application.font.pointSize
|
|
|
|
palette {
|
|
window: theme.windowColor
|
|
windowText: theme.windowTextColor
|
|
base: theme.baseColor
|
|
alternateBase: theme.alternateBaseColor
|
|
text: theme.textColor
|
|
button: theme.buttonColor
|
|
buttonText: theme.buttonTextColor
|
|
highlight: theme.highlightColor
|
|
highlightedText: theme.highlightedTextColor
|
|
toolTipBase: theme.tooltipBaseColor
|
|
toolTipText: theme.tooltipTextColor
|
|
link: theme.linkColor
|
|
mid: theme.midColor
|
|
dark: theme.darkColor
|
|
light: theme.lightColor
|
|
shadow: theme.shadowColor
|
|
placeholderText: theme.placeholderTextColor
|
|
}
|
|
|
|
// --- Settings Dialog ---
|
|
Dialog {
|
|
id: settingsDialog
|
|
title: "Settings"
|
|
anchors.centerIn: parent
|
|
width: 380
|
|
modal: true
|
|
standardButtons: Dialog.Close
|
|
|
|
contentItem: ColumnLayout {
|
|
spacing: 16
|
|
Layout.margins: 16
|
|
|
|
Label {
|
|
text: "Color Scheme"
|
|
font.bold: true
|
|
}
|
|
|
|
ComboBox {
|
|
id: schemeCombo
|
|
Layout.fillWidth: true
|
|
model: ["System"].concat(theme.availableSchemes)
|
|
currentIndex: {
|
|
if (theme.mode === "system") return 0
|
|
let idx = theme.availableSchemes.indexOf(theme.mode)
|
|
return idx >= 0 ? idx + 1 : 0
|
|
}
|
|
onActivated: {
|
|
if (currentIndex === 0) {
|
|
theme.mode = "system"
|
|
} else {
|
|
theme.mode = theme.availableSchemes[currentIndex - 1]
|
|
}
|
|
}
|
|
}
|
|
|
|
Label {
|
|
text: theme.isDark ? "Current: dark" : "Current: light"
|
|
opacity: 0.5
|
|
font.pixelSize: Qt.application.font.pixelSize * 0.85
|
|
}
|
|
|
|
// --- Font section ---
|
|
Rectangle {
|
|
Layout.fillWidth: true
|
|
height: 1
|
|
color: palette.mid
|
|
Layout.topMargin: 8
|
|
}
|
|
|
|
Label {
|
|
text: "Font"
|
|
font.bold: true
|
|
}
|
|
|
|
GridLayout {
|
|
columns: 2
|
|
columnSpacing: 12
|
|
rowSpacing: 8
|
|
Layout.fillWidth: true
|
|
|
|
Label { text: "Family:" }
|
|
ComboBox {
|
|
id: fontFamilyCombo
|
|
Layout.fillWidth: true
|
|
model: theme.availableFamilies
|
|
editable: true
|
|
currentIndex: {
|
|
let fam = theme.fontFamily || Qt.application.font.family
|
|
let idx = model.indexOf(fam)
|
|
return idx >= 0 ? idx : 0
|
|
}
|
|
onActivated: theme.fontFamily = currentText
|
|
onAccepted: theme.fontFamily = editText
|
|
}
|
|
|
|
Label { text: "Style:" }
|
|
ComboBox {
|
|
id: fontStyleCombo
|
|
Layout.fillWidth: true
|
|
model: theme.availableStyles
|
|
currentIndex: {
|
|
let st = theme.fontStyle
|
|
let idx = model.indexOf(st)
|
|
return idx >= 0 ? idx : 0
|
|
}
|
|
onActivated: theme.fontStyle = currentText
|
|
}
|
|
|
|
Label { text: "Size:" }
|
|
SpinBox {
|
|
id: fontSizeSpin
|
|
from: 6
|
|
to: 72
|
|
value: theme.fontSize > 0 ? theme.fontSize : Qt.application.font.pointSize
|
|
onValueModified: theme.fontSize = value
|
|
}
|
|
}
|
|
|
|
Button {
|
|
text: "Reset to system font"
|
|
onClicked: theme.resetFont()
|
|
}
|
|
|
|
// --- Close to tray section ---
|
|
Rectangle {
|
|
Layout.fillWidth: true
|
|
height: 1
|
|
color: palette.mid
|
|
Layout.topMargin: 8
|
|
}
|
|
|
|
Label {
|
|
text: "System Tray"
|
|
font.bold: true
|
|
}
|
|
|
|
CheckBox {
|
|
text: "Close to tray instead of quitting"
|
|
checked: theme.closeToTray
|
|
onToggled: theme.closeToTray = checked
|
|
enabled: systemTrayAvailable
|
|
}
|
|
|
|
Label {
|
|
text: systemTrayAvailable
|
|
? "Window will hide to the system tray when closed."
|
|
: "System tray is not available."
|
|
opacity: 0.5
|
|
font.pixelSize: Qt.application.font.pixelSize * 0.85
|
|
wrapMode: Text.Wrap
|
|
Layout.fillWidth: true
|
|
visible: theme.closeToTray || !systemTrayAvailable
|
|
}
|
|
}
|
|
}
|
|
|
|
function openNewSessionDialog() {
|
|
nameInput.text = ""
|
|
newSessionDialog.backendModel = dbus.listBackends()
|
|
newSessionDialog.agentModel = dbus.listAgents()
|
|
newSessionDialog.cwdField.text = dbus.activeCwd || ""
|
|
newSessionDialog.backendCombo.currentIndex = 0
|
|
newSessionDialog.agentCombo.currentIndex = 0
|
|
newSessionDialog.modelCombo.editText = ""
|
|
newSessionDialog.open()
|
|
}
|
|
|
|
Dialog {
|
|
id: newSessionDialog
|
|
title: "New Session"
|
|
modal: true
|
|
anchors.centerIn: parent
|
|
width: 450
|
|
standardButtons: Dialog.Ok | Dialog.Cancel
|
|
|
|
property alias cwdField: cwdInput
|
|
property alias backendCombo: backendSelect
|
|
property alias agentCombo: agentSelect
|
|
property alias modelCombo: modelSelect
|
|
property var backendModel: []
|
|
property var agentModel: []
|
|
|
|
onAccepted: {
|
|
let cwd = cwdInput.text.trim()
|
|
if (cwd === "") cwd = dbus.activeCwd || ""
|
|
let backend = backendSelect.currentText
|
|
let model = modelSelect.editText.trim() || modelSelect.currentText
|
|
let agent = agentSelect.currentText
|
|
let name = nameInput.text.trim()
|
|
dbus.createSession(cwd, backend, model, name, agent)
|
|
}
|
|
|
|
GridLayout {
|
|
columns: 2
|
|
width: parent.width
|
|
columnSpacing: 10
|
|
rowSpacing: 8
|
|
|
|
Label { text: "Name:" }
|
|
TextField {
|
|
id: nameInput
|
|
Layout.fillWidth: true
|
|
placeholderText: "(auto-generated)"
|
|
}
|
|
|
|
Label { text: "Working directory:" }
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
TextField {
|
|
id: cwdInput
|
|
Layout.fillWidth: true
|
|
placeholderText: "/home/user/project"
|
|
}
|
|
Button {
|
|
text: "Browse"
|
|
onClicked: {
|
|
let path = theme.chooseFolder("Choose working directory", cwdInput.text)
|
|
if (path !== "")
|
|
cwdInput.text = path
|
|
}
|
|
}
|
|
}
|
|
|
|
Label { text: "Backend:" }
|
|
ComboBox {
|
|
id: backendSelect
|
|
Layout.fillWidth: true
|
|
model: newSessionDialog.backendModel
|
|
onCurrentTextChanged: {
|
|
if (currentText)
|
|
modelSelect.model = dbus.listModelsByBackend(currentText)
|
|
}
|
|
}
|
|
|
|
Label { text: "Agent:" }
|
|
ComboBox {
|
|
id: agentSelect
|
|
Layout.fillWidth: true
|
|
model: newSessionDialog.agentModel
|
|
currentIndex: 0
|
|
}
|
|
|
|
Label { text: "Model:" }
|
|
ComboBox {
|
|
id: modelSelect
|
|
Layout.fillWidth: true
|
|
editable: true
|
|
editText: ""
|
|
}
|
|
}
|
|
}
|
|
|
|
function openCwdDialog() {
|
|
let path = theme.chooseFolder("Change working directory",
|
|
dbus.activeCwd || "")
|
|
if (path !== "")
|
|
dbus.setConfig(dbus.activeSessionId, "cwd", path)
|
|
}
|
|
|
|
// --- Session Graph Window ---
|
|
Window {
|
|
id: graphWindow
|
|
title: "Ollie — Session Topology"
|
|
width: 700
|
|
height: 500
|
|
visible: false
|
|
color: palette.window
|
|
|
|
Component.onCompleted: activator.windowOpened()
|
|
Component.onDestruction: {
|
|
if (typeof activator !== 'undefined' && activator) {
|
|
activator.windowClosed()
|
|
}
|
|
}
|
|
|
|
SessionGraph {
|
|
id: graphView
|
|
anchors.fill: parent
|
|
sessions: dbus.sessions
|
|
peerListFn: function(sid) { return dbus.peerList(sid) }
|
|
}
|
|
}
|
|
|
|
SplitView {
|
|
anchors.fill: parent
|
|
orientation: Qt.Horizontal
|
|
|
|
// --- Session sidebar ---
|
|
Pane {
|
|
SplitView.preferredWidth: 220
|
|
SplitView.minimumWidth: 160
|
|
padding: 0
|
|
|
|
ColumnLayout {
|
|
anchors.fill: parent
|
|
spacing: 0
|
|
|
|
// Toolbar
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
Layout.margins: 6
|
|
|
|
Label {
|
|
text: "Sessions"
|
|
font.bold: true
|
|
Layout.fillWidth: true
|
|
}
|
|
|
|
ToolButton {
|
|
icon.name: "list-add"
|
|
onClicked: root.openNewSessionDialog()
|
|
ToolTip.text: "New session"
|
|
ToolTip.visible: hovered
|
|
}
|
|
|
|
ToolButton {
|
|
icon.name: "configure"
|
|
onClicked: settingsDialog.open()
|
|
ToolTip.text: "Settings"
|
|
ToolTip.visible: hovered
|
|
}
|
|
|
|
ToolButton {
|
|
icon.name: "network-workgroup"
|
|
onClicked: graphWindow.visible = !graphWindow.visible
|
|
ToolTip.text: "Session topology"
|
|
ToolTip.visible: hovered
|
|
}
|
|
}
|
|
|
|
// Session list
|
|
ListView {
|
|
id: sessionList
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
clip: true
|
|
model: dbus.sessions
|
|
|
|
delegate: ItemDelegate {
|
|
id: sessionDelegate
|
|
width: sessionList.width
|
|
highlighted: modelData.id === dbus.activeSessionId
|
|
|
|
property bool renaming: false
|
|
property string sessionState: modelData.state
|
|
|
|
Connections {
|
|
target: dbus
|
|
function onSessionStateUpdated(sessionId, newState) {
|
|
if (sessionId === modelData.id)
|
|
sessionDelegate.sessionState = newState
|
|
}
|
|
}
|
|
|
|
background: Rectangle {
|
|
color: sessionDelegate.highlighted
|
|
? palette.highlight : "transparent"
|
|
radius: 4
|
|
}
|
|
|
|
contentItem: RowLayout {
|
|
spacing: 6
|
|
|
|
Rectangle {
|
|
width: 8
|
|
height: 8
|
|
radius: 4
|
|
color: {
|
|
let state = sessionDelegate.sessionState
|
|
if (state === "idle") return "#4caf50"
|
|
if (state.startsWith("calling")) return "#ff9800"
|
|
return "#2196f3"
|
|
}
|
|
}
|
|
|
|
Label {
|
|
id: sessionLabel
|
|
text: modelData.id
|
|
Layout.fillWidth: true
|
|
elide: Text.ElideRight
|
|
font.family: "monospace"
|
|
color: sessionDelegate.highlighted
|
|
? palette.highlightedText : palette.text
|
|
visible: !sessionDelegate.renaming
|
|
}
|
|
|
|
TextField {
|
|
id: renameField
|
|
Layout.fillWidth: true
|
|
font.family: "monospace"
|
|
visible: sessionDelegate.renaming
|
|
onAccepted: {
|
|
let newName = text.trim()
|
|
if (newName !== "" && newName !== modelData.id) {
|
|
dbus.renameSession(modelData.id, newName)
|
|
}
|
|
sessionDelegate.renaming = false
|
|
}
|
|
Keys.onEscapePressed: sessionDelegate.renaming = false
|
|
onVisibleChanged: {
|
|
if (visible) {
|
|
text = modelData.id
|
|
selectAll()
|
|
forceActiveFocus()
|
|
}
|
|
}
|
|
onActiveFocusChanged: {
|
|
if (!activeFocus && sessionDelegate.renaming) {
|
|
sessionDelegate.renaming = false
|
|
}
|
|
}
|
|
}
|
|
|
|
Label {
|
|
text: modelData.agent
|
|
opacity: 0.6
|
|
font.pixelSize: Qt.application.font.pixelSize * 0.85
|
|
color: sessionDelegate.highlighted
|
|
? palette.highlightedText : palette.text
|
|
visible: !sessionDelegate.renaming
|
|
}
|
|
|
|
ToolButton {
|
|
icon.name: "process-stop"
|
|
implicitWidth: 24
|
|
implicitHeight: 24
|
|
visible: !sessionDelegate.renaming
|
|
onClicked: dbus.killSession(modelData.id)
|
|
ToolTip.text: "Kill session"
|
|
ToolTip.visible: hovered
|
|
}
|
|
}
|
|
|
|
onClicked: dbus.activeSessionId = modelData.id
|
|
onDoubleClicked: sessionDelegate.renaming = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Main content: tiling pane area ---
|
|
Pane {
|
|
id: mainContent
|
|
SplitView.fillWidth: true
|
|
padding: 0
|
|
|
|
property int focusedPaneIndex: 0
|
|
property var panes: []
|
|
property int paneCount: 0
|
|
|
|
// BSP tree: each node is {pane: item} or {split: 'h'|'v', left: node, right: node}
|
|
property var bspTree: null
|
|
property real gap: 2
|
|
|
|
function layoutTree(node, x, y, w, h) {
|
|
if (!node) return
|
|
if (node.pane) {
|
|
node.pane.x = x
|
|
node.pane.y = y
|
|
node.pane.width = w
|
|
node.pane.height = h
|
|
} else if (node.split) {
|
|
if (node.split === 'h') {
|
|
let lw = (w - gap) / 2
|
|
layoutTree(node.left, x, y, lw, h)
|
|
layoutTree(node.right, x + lw + gap, y, w - lw - gap, h)
|
|
} else {
|
|
let th = (h - gap) / 2
|
|
layoutTree(node.left, x, y, w, th)
|
|
layoutTree(node.right, x, y + th + gap, w, h - th - gap)
|
|
}
|
|
}
|
|
}
|
|
|
|
function relayout() {
|
|
if (bspTree)
|
|
layoutTree(bspTree, 0, 0, width, height)
|
|
}
|
|
|
|
// Find and replace a leaf node in the tree
|
|
function splitNode(tree, targetPane, newPane, direction) {
|
|
if (!tree) return null
|
|
if (tree.pane === targetPane) {
|
|
return {split: direction, left: {pane: targetPane}, right: {pane: newPane}}
|
|
}
|
|
if (tree.split) {
|
|
let newLeft = splitNode(tree.left, targetPane, newPane, direction)
|
|
if (newLeft) { tree.left = newLeft; return tree }
|
|
let newRight = splitNode(tree.right, targetPane, newPane, direction)
|
|
if (newRight) { tree.right = newRight; return tree }
|
|
}
|
|
return null
|
|
}
|
|
|
|
// Remove a pane from the tree, collapsing its parent split
|
|
function removeNode(tree, targetPane) {
|
|
if (!tree) return null
|
|
if (tree.pane === targetPane) return null // found it
|
|
if (tree.split) {
|
|
if (tree.left && tree.left.pane === targetPane) return tree.right
|
|
if (tree.right && tree.right.pane === targetPane) return tree.left
|
|
let newLeft = removeNode(tree.left, targetPane)
|
|
if (newLeft !== tree.left) { tree.left = newLeft; return tree }
|
|
let newRight = removeNode(tree.right, targetPane)
|
|
if (newRight !== tree.right) { tree.right = newRight; return tree }
|
|
}
|
|
return tree
|
|
}
|
|
|
|
onWidthChanged: relayout()
|
|
onHeightChanged: relayout()
|
|
onPaneCountChanged: relayout()
|
|
|
|
// Container for panes (absolute positioning)
|
|
Item {
|
|
id: paneContainer
|
|
anchors.fill: parent
|
|
|
|
ChatPane {
|
|
id: initialPane
|
|
sessionId: ""
|
|
Component.onCompleted: {
|
|
sessionId = dbus.activeSessionId
|
|
mainContent.panes = [initialPane]
|
|
mainContent.paneCount = 1
|
|
mainContent.bspTree = {pane: initialPane}
|
|
mainContent.relayout()
|
|
}
|
|
isFocused: mainContent.focusedPaneIndex === 0
|
|
onActiveFocusChanged: if (activeFocus) mainContent.focusedPaneIndex = 0
|
|
onEndedDrag: function(gx, gy) { root.handlePaneSwap(initialPane, gx, gy) }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Window shortcuts ---
|
|
Shortcut {
|
|
sequence: "Ctrl+Q"
|
|
onActivated: Qt.quit()
|
|
}
|
|
|
|
Shortcut {
|
|
sequence: "Ctrl+N"
|
|
onActivated: activator.NewWindow()
|
|
}
|
|
|
|
// --- Split pane shortcuts ---
|
|
Shortcut {
|
|
sequence: "Ctrl+\\"
|
|
onActivated: {
|
|
let comp = Qt.createComponent("ChatPane.qml")
|
|
if (comp.status !== Component.Ready) {
|
|
console.warn("ChatPane.qml:", comp.errorString())
|
|
return
|
|
}
|
|
let focusedPane = mainContent.panes[mainContent.focusedPaneIndex]
|
|
if (!focusedPane) return
|
|
|
|
let pane = comp.createObject(paneContainer, {
|
|
sessionId: dbus.activeSessionId,
|
|
isFocused: false
|
|
})
|
|
if (pane) {
|
|
// Split direction: alternate based on focused pane's aspect ratio
|
|
let dir = focusedPane.width >= focusedPane.height ? 'h' : 'v'
|
|
mainContent.bspTree = mainContent.splitNode(mainContent.bspTree, focusedPane, pane, dir)
|
|
|
|
let panes = mainContent.panes
|
|
let insertAt = mainContent.focusedPaneIndex + 1
|
|
panes.splice(insertAt, 0, pane)
|
|
mainContent.panes = panes
|
|
mainContent.paneCount = panes.length
|
|
mainContent.focusedPaneIndex = insertAt
|
|
mainContent.relayout()
|
|
|
|
pane.activeFocusChanged.connect(function() {
|
|
if (pane.activeFocus) {
|
|
mainContent.focusedPaneIndex = mainContent.panes.indexOf(pane)
|
|
}
|
|
})
|
|
pane.endedDrag.connect(function(gx, gy) {
|
|
root.handlePaneSwap(pane, gx, gy)
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
Shortcut {
|
|
sequence: "Ctrl+W"
|
|
onActivated: {
|
|
if (mainContent.panes.length <= 1) return
|
|
let idx = mainContent.focusedPaneIndex
|
|
if (idx < 0 || idx >= mainContent.panes.length)
|
|
idx = mainContent.panes.length - 1
|
|
let pane = mainContent.panes[idx]
|
|
|
|
mainContent.bspTree = mainContent.removeNode(mainContent.bspTree, pane)
|
|
|
|
let panes = mainContent.panes
|
|
panes.splice(idx, 1)
|
|
mainContent.panes = panes
|
|
mainContent.paneCount = panes.length
|
|
pane.destroy()
|
|
mainContent.focusedPaneIndex = Math.min(idx, panes.length - 1)
|
|
mainContent.relayout()
|
|
}
|
|
}
|
|
|
|
// Handle pane swap: find target pane at drop coordinates (root-window coords)
|
|
function handlePaneSwap(sourcePane, rootX, rootY) {
|
|
for (let i = 0; i < mainContent.panes.length; i++) {
|
|
let p = mainContent.panes[i]
|
|
if (p === sourcePane) continue
|
|
let localPt = p.mapFromItem(null, rootX, rootY)
|
|
if (localPt.x >= 0 && localPt.x <= p.width &&
|
|
localPt.y >= 0 && localPt.y <= p.height) {
|
|
// Swap sessions
|
|
let tmp = sourcePane.sessionId
|
|
sourcePane.sessionId = p.sessionId
|
|
p.sessionId = tmp
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// Sidebar click updates focused pane's session
|
|
Connections {
|
|
target: dbus
|
|
function onActiveSessionIdChanged() {
|
|
let idx = mainContent.focusedPaneIndex
|
|
if (idx >= 0 && idx < mainContent.panes.length) {
|
|
mainContent.panes[idx].sessionId = dbus.activeSessionId
|
|
}
|
|
}
|
|
function onErrorOccurred(message) {
|
|
errorDialog.text = message
|
|
errorDialog.open()
|
|
}
|
|
}
|
|
|
|
Dialog {
|
|
id: errorDialog
|
|
title: "Error"
|
|
modal: true
|
|
anchors.centerIn: parent
|
|
standardButtons: Dialog.Ok
|
|
property alias text: errorLabel.text
|
|
Label {
|
|
id: errorLabel
|
|
wrapMode: Text.Wrap
|
|
width: parent.width
|
|
}
|
|
}
|
|
|
|
// --- Process Manager Window ---
|
|
Window {
|
|
id: processManagerWindow
|
|
title: "Ollie — Detached Processes"
|
|
width: 600
|
|
height: 400
|
|
color: palette.window
|
|
|
|
property var processes: []
|
|
property int selectedPid: -1
|
|
property string selectedOutput: ""
|
|
|
|
Timer {
|
|
id: procRefreshTimer
|
|
interval: 1500
|
|
running: processManagerWindow.visible
|
|
repeat: true
|
|
triggeredOnStart: true
|
|
onTriggered: {
|
|
if (dbus.activeSessionId === "") return
|
|
processManagerWindow.processes = dbus.listDetached(dbus.activeSessionId)
|
|
if (processManagerWindow.selectedPid > 0) {
|
|
processManagerWindow.selectedOutput =
|
|
dbus.getDetachedOutput(dbus.activeSessionId, processManagerWindow.selectedPid)
|
|
}
|
|
}
|
|
}
|
|
|
|
ColumnLayout {
|
|
anchors.fill: parent
|
|
anchors.margins: 8
|
|
spacing: 8
|
|
|
|
Label {
|
|
text: "Session: " + (dbus.activeSessionId || "none")
|
|
font.bold: true
|
|
}
|
|
|
|
// Process list
|
|
ListView {
|
|
id: procList
|
|
Layout.fillWidth: true
|
|
Layout.preferredHeight: 140
|
|
clip: true
|
|
model: processManagerWindow.processes
|
|
|
|
delegate: ItemDelegate {
|
|
width: procList.width
|
|
highlighted: modelData.pid === processManagerWindow.selectedPid
|
|
|
|
contentItem: RowLayout {
|
|
spacing: 8
|
|
|
|
Rectangle {
|
|
width: 8; height: 8; radius: 4
|
|
color: modelData.status === "running" ? "#4caf50" : "#9e9e9e"
|
|
}
|
|
|
|
Label {
|
|
text: modelData.command
|
|
Layout.fillWidth: true
|
|
elide: Text.ElideRight
|
|
font.family: "monospace"
|
|
}
|
|
|
|
Label {
|
|
text: "pid " + modelData.pid
|
|
opacity: 0.6
|
|
font.pixelSize: Qt.application.font.pixelSize * 0.85
|
|
}
|
|
|
|
Label {
|
|
text: modelData.status
|
|
opacity: 0.6
|
|
font.pixelSize: Qt.application.font.pixelSize * 0.85
|
|
}
|
|
|
|
Button {
|
|
text: "Term"
|
|
visible: modelData.status === "running"
|
|
onClicked: dbus.signalDetached(dbus.activeSessionId, modelData.pid, "TERM")
|
|
}
|
|
|
|
Button {
|
|
text: "Kill"
|
|
visible: modelData.status === "running"
|
|
onClicked: dbus.signalDetached(dbus.activeSessionId, modelData.pid, "KILL")
|
|
}
|
|
|
|
Button {
|
|
text: "Dismiss"
|
|
visible: modelData.status !== "running"
|
|
onClicked: dbus.dismissDetached(dbus.activeSessionId, modelData.pid)
|
|
}
|
|
}
|
|
|
|
onClicked: {
|
|
processManagerWindow.selectedPid = modelData.pid
|
|
processManagerWindow.selectedOutput =
|
|
dbus.getDetachedOutput(dbus.activeSessionId, modelData.pid)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Separator
|
|
Rectangle {
|
|
Layout.fillWidth: true
|
|
height: 1
|
|
color: palette.mid
|
|
}
|
|
|
|
// Output tail
|
|
RowLayout {
|
|
spacing: 8
|
|
Label {
|
|
text: processManagerWindow.selectedPid > 0
|
|
? "——— Output: pid " + processManagerWindow.selectedPid + " ———"
|
|
: "Select a process to view output"
|
|
opacity: 0.6
|
|
font.pixelSize: Qt.application.font.pixelSize * 0.85
|
|
Layout.fillWidth: true
|
|
}
|
|
Button {
|
|
text: "Copy"
|
|
visible: processManagerWindow.selectedPid > 0
|
|
onClicked: {
|
|
outputArea.selectAll()
|
|
outputArea.copy()
|
|
outputArea.deselect()
|
|
}
|
|
}
|
|
}
|
|
|
|
Flickable {
|
|
id: outputFlick
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
clip: true
|
|
contentWidth: width
|
|
contentHeight: outputArea.implicitHeight
|
|
boundsBehavior: Flickable.StopAtBounds
|
|
|
|
property bool atBottom: true
|
|
|
|
onContentYChanged: {
|
|
let bottom = contentHeight - height
|
|
atBottom = (bottom <= 0 || contentY >= bottom - 4)
|
|
}
|
|
|
|
onContentHeightChanged: {
|
|
if (atBottom) {
|
|
contentY = Math.max(0, contentHeight - height)
|
|
}
|
|
}
|
|
|
|
TextArea {
|
|
id: outputArea
|
|
width: outputFlick.width
|
|
readOnly: true
|
|
font.family: "monospace"
|
|
font.pixelSize: Qt.application.font.pixelSize * 0.9
|
|
text: processManagerWindow.selectedOutput
|
|
wrapMode: TextEdit.Wrap
|
|
background: null
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|