mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-04-04 04:42:05 -04:00
* Add tmux
* Add mux modal
* Restore the settings config version
* Revert typo
* Use DankModal for InputModal
* Simplify terminal flags
* use showWithOptions for inputModals instead
* Fix translation
* use Quickshell.env("TERMINAL") to choose terminal
* Fix typo
* Hide muxModal after creating new session
* Add mux check, moved exclusion to service, And use ScriptModel
* Revert unrelated change
* Add blank line
622 lines
23 KiB
QML
622 lines
23 KiB
QML
pragma ComponentBehavior: Bound
|
|
|
|
import QtQuick
|
|
import QtQuick.Controls
|
|
import QtQuick.Layouts
|
|
import Quickshell.Hyprland
|
|
import Quickshell.Io
|
|
import Quickshell
|
|
import qs.Common
|
|
import qs.Modals.Common
|
|
import qs.Services
|
|
import qs.Widgets
|
|
|
|
DankModal {
|
|
id: muxModal
|
|
|
|
layerNamespace: "dms:mux"
|
|
|
|
property int selectedIndex: -1
|
|
property string searchText: ""
|
|
property var filteredSessions: []
|
|
|
|
function updateFilteredSessions() {
|
|
var filtered = []
|
|
var lowerSearch = searchText.trim().toLowerCase()
|
|
for (var i = 0; i < MuxService.sessions.length; i++) {
|
|
var session = MuxService.sessions[i]
|
|
if (lowerSearch.length > 0 && !session.name.toLowerCase().includes(lowerSearch))
|
|
continue
|
|
filtered.push(session)
|
|
}
|
|
filteredSessions = filtered
|
|
|
|
if (selectedIndex >= filteredSessions.length) {
|
|
selectedIndex = Math.max(0, filteredSessions.length - 1)
|
|
}
|
|
}
|
|
|
|
onSearchTextChanged: updateFilteredSessions()
|
|
|
|
Connections {
|
|
target: MuxService
|
|
function onSessionsChanged() {
|
|
updateFilteredSessions()
|
|
}
|
|
}
|
|
|
|
HyprlandFocusGrab {
|
|
id: grab
|
|
windows: [muxModal.contentWindow]
|
|
active: CompositorService.isHyprland && muxModal.shouldHaveFocus
|
|
}
|
|
|
|
function toggle() {
|
|
if (shouldBeVisible) {
|
|
hide()
|
|
} else {
|
|
show()
|
|
}
|
|
}
|
|
|
|
function show() {
|
|
open()
|
|
selectedIndex = -1
|
|
searchText = ""
|
|
MuxService.refreshSessions()
|
|
shouldHaveFocus = true
|
|
|
|
Qt.callLater(() => {
|
|
if (muxPanel && muxPanel.searchField) {
|
|
muxPanel.searchField.forceActiveFocus();
|
|
}
|
|
})
|
|
}
|
|
|
|
function hide() {
|
|
close()
|
|
selectedIndex = -1
|
|
searchText = ""
|
|
}
|
|
|
|
function attachToSession(name) {
|
|
MuxService.attachToSession(name)
|
|
hide()
|
|
}
|
|
|
|
function renameSession(name) {
|
|
inputModal.showWithOptions({
|
|
title: I18n.tr("Rename Session"),
|
|
message: I18n.tr("Enter a new name for session \"%1\"").arg(name),
|
|
initialText: name,
|
|
onConfirm: function (newName) {
|
|
MuxService.renameSession(name, newName)
|
|
}
|
|
})
|
|
}
|
|
|
|
function killSession(name) {
|
|
confirmModal.showWithOptions({
|
|
title: I18n.tr("Kill Session"),
|
|
message: I18n.tr("Are you sure you want to kill session \"%1\"?").arg(name),
|
|
confirmText: I18n.tr("Kill"),
|
|
confirmColor: Theme.primary,
|
|
onConfirm: function () {
|
|
MuxService.killSession(name)
|
|
}
|
|
})
|
|
}
|
|
|
|
function createNewSession() {
|
|
inputModal.showWithOptions({
|
|
title: I18n.tr("New Session"),
|
|
message: I18n.tr("Please write a name for your new %1 session").arg(MuxService.displayName),
|
|
onConfirm: function (name) {
|
|
MuxService.createSession(name)
|
|
hide()
|
|
}
|
|
})
|
|
}
|
|
|
|
function selectNext() {
|
|
selectedIndex = Math.min(selectedIndex + 1, filteredSessions.length - 1)
|
|
}
|
|
|
|
function selectPrevious() {
|
|
selectedIndex = Math.max(selectedIndex - 1, -1)
|
|
}
|
|
|
|
function activateSelected() {
|
|
if (selectedIndex === -1) {
|
|
createNewSession()
|
|
} else if (selectedIndex >= 0 && selectedIndex < filteredSessions.length) {
|
|
attachToSession(filteredSessions[selectedIndex].name)
|
|
}
|
|
}
|
|
|
|
visible: false
|
|
modalWidth: 600
|
|
modalHeight: 600
|
|
backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
|
cornerRadius: Theme.cornerRadius
|
|
borderColor: Theme.outlineMedium
|
|
borderWidth: 1
|
|
enableShadow: true
|
|
keepContentLoaded: true
|
|
|
|
onBackgroundClicked: hide()
|
|
|
|
Timer {
|
|
interval: 3000
|
|
running: muxModal.shouldBeVisible
|
|
repeat: true
|
|
onTriggered: MuxService.refreshSessions()
|
|
}
|
|
|
|
IpcHandler {
|
|
function open(): string {
|
|
muxModal.show()
|
|
return "MUX_OPEN_SUCCESS"
|
|
}
|
|
|
|
function close(): string {
|
|
muxModal.hide()
|
|
return "MUX_CLOSE_SUCCESS"
|
|
}
|
|
|
|
function toggle(): string {
|
|
muxModal.toggle()
|
|
return "MUX_TOGGLE_SUCCESS"
|
|
}
|
|
|
|
target: "mux"
|
|
}
|
|
|
|
// Backwards compatibility
|
|
IpcHandler {
|
|
function open(): string {
|
|
muxModal.show()
|
|
return "TMUX_OPEN_SUCCESS"
|
|
}
|
|
|
|
function close(): string {
|
|
muxModal.hide()
|
|
return "TMUX_CLOSE_SUCCESS"
|
|
}
|
|
|
|
function toggle(): string {
|
|
muxModal.toggle()
|
|
return "TMUX_TOGGLE_SUCCESS"
|
|
}
|
|
|
|
target: "tmux"
|
|
}
|
|
|
|
InputModal {
|
|
id: inputModal
|
|
onShouldBeVisibleChanged: {
|
|
if (shouldBeVisible) {
|
|
muxModal.shouldHaveFocus = false;
|
|
muxModal.contentWindow.visible = false;
|
|
return;
|
|
}
|
|
if (muxModal.shouldBeVisible) {
|
|
muxModal.contentWindow.visible = true;
|
|
}
|
|
Qt.callLater(function () {
|
|
if (!muxModal.shouldBeVisible) {
|
|
return;
|
|
}
|
|
muxModal.shouldHaveFocus = true;
|
|
muxModal.modalFocusScope.forceActiveFocus();
|
|
if (muxPanel.searchField) {
|
|
muxPanel.searchField.forceActiveFocus();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
ConfirmModal {
|
|
id: confirmModal
|
|
onShouldBeVisibleChanged: {
|
|
if (shouldBeVisible) {
|
|
muxModal.shouldHaveFocus = false;
|
|
muxModal.contentWindow.visible = false;
|
|
return;
|
|
}
|
|
if (muxModal.shouldBeVisible) {
|
|
muxModal.contentWindow.visible = true;
|
|
}
|
|
Qt.callLater(function () {
|
|
if (!muxModal.shouldBeVisible) {
|
|
return;
|
|
}
|
|
muxModal.shouldHaveFocus = true;
|
|
muxModal.modalFocusScope.forceActiveFocus();
|
|
if (muxPanel.searchField) {
|
|
muxPanel.searchField.forceActiveFocus();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
directContent: Item {
|
|
id: muxPanel
|
|
|
|
clip: false
|
|
|
|
property alias searchField: searchField
|
|
|
|
Keys.onPressed: event => {
|
|
if ((event.key === Qt.Key_J && (event.modifiers & Qt.ControlModifier)) ||
|
|
(event.key === Qt.Key_Down)) {
|
|
selectNext()
|
|
event.accepted = true
|
|
} else if ((event.key === Qt.Key_K && (event.modifiers & Qt.ControlModifier)) ||
|
|
(event.key === Qt.Key_Up)) {
|
|
selectPrevious()
|
|
event.accepted = true
|
|
} else if (event.key === Qt.Key_N && (event.modifiers & Qt.ControlModifier)) {
|
|
createNewSession()
|
|
event.accepted = true
|
|
} else if (event.key === Qt.Key_R && (event.modifiers & Qt.ControlModifier)) {
|
|
if (MuxService.supportsRename && selectedIndex >= 0 && selectedIndex < filteredSessions.length) {
|
|
renameSession(filteredSessions[selectedIndex].name)
|
|
}
|
|
event.accepted = true
|
|
} else if (event.key === Qt.Key_D && (event.modifiers & Qt.ControlModifier)) {
|
|
if (selectedIndex >= 0 && selectedIndex < filteredSessions.length) {
|
|
killSession(filteredSessions[selectedIndex].name)
|
|
}
|
|
event.accepted = true
|
|
} else if (event.key === Qt.Key_Escape) {
|
|
hide()
|
|
event.accepted = true
|
|
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
|
activateSelected()
|
|
event.accepted = true
|
|
}
|
|
}
|
|
|
|
Column {
|
|
width: parent.width - Theme.spacingM * 2
|
|
height: parent.height - Theme.spacingM * 2
|
|
x: Theme.spacingM
|
|
y: Theme.spacingM
|
|
spacing: Theme.spacingS
|
|
|
|
// Header
|
|
Item {
|
|
width: parent.width
|
|
height: 40
|
|
|
|
StyledText {
|
|
anchors.left: parent.left
|
|
anchors.leftMargin: Theme.spacingS
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: I18n.tr("%1 Sessions").arg(MuxService.displayName)
|
|
font.pixelSize: Theme.fontSizeLarge + 4
|
|
font.weight: Font.Bold
|
|
color: Theme.surfaceText
|
|
}
|
|
|
|
StyledText {
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: Theme.spacingS
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: I18n.tr("%1 active, %2 filtered").arg(MuxService.sessions.length).arg(muxModal.filteredSessions.length)
|
|
font.pixelSize: Theme.fontSizeMedium
|
|
color: Theme.surfaceVariantText
|
|
}
|
|
}
|
|
|
|
// Search field
|
|
DankTextField {
|
|
id: searchField
|
|
|
|
width: parent.width
|
|
height: 48
|
|
cornerRadius: Theme.cornerRadius
|
|
backgroundColor: Theme.surfaceContainerHigh
|
|
normalBorderColor: Theme.outlineMedium
|
|
focusedBorderColor: Theme.primary
|
|
leftIconName: "search"
|
|
leftIconSize: Theme.iconSize
|
|
leftIconColor: Theme.surfaceVariantText
|
|
leftIconFocusedColor: Theme.primary
|
|
showClearButton: true
|
|
font.pixelSize: Theme.fontSizeMedium
|
|
placeholderText: I18n.tr("Search sessions...")
|
|
keyForwardTargets: [muxPanel]
|
|
|
|
onTextEdited: {
|
|
muxModal.searchText = text
|
|
muxModal.selectedIndex = 0
|
|
}
|
|
}
|
|
|
|
// New Session Button
|
|
Rectangle {
|
|
width: parent.width
|
|
height: 56
|
|
radius: Theme.cornerRadius
|
|
color: muxModal.selectedIndex === -1 ? Theme.primaryContainer :
|
|
(newMouse.containsMouse ? Theme.surfaceContainerHigh : Theme.surfaceContainer)
|
|
|
|
RowLayout {
|
|
anchors.fill: parent
|
|
anchors.leftMargin: Theme.spacingM
|
|
anchors.rightMargin: Theme.spacingM
|
|
spacing: Theme.spacingM
|
|
|
|
Rectangle {
|
|
Layout.preferredWidth: 40
|
|
Layout.preferredHeight: 40
|
|
radius: 20
|
|
color: Theme.primaryContainer
|
|
|
|
DankIcon {
|
|
anchors.centerIn: parent
|
|
name: "add"
|
|
size: Theme.iconSize
|
|
color: Theme.primary
|
|
}
|
|
}
|
|
|
|
Column {
|
|
Layout.fillWidth: true
|
|
spacing: 2
|
|
|
|
StyledText {
|
|
text: I18n.tr("New Session")
|
|
font.pixelSize: Theme.fontSizeMedium
|
|
font.weight: Font.Medium
|
|
color: Theme.surfaceText
|
|
}
|
|
|
|
StyledText {
|
|
text: I18n.tr("Create a new %1 session (n)").arg(MuxService.displayName)
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
color: Theme.surfaceVariantText
|
|
}
|
|
}
|
|
}
|
|
|
|
MouseArea {
|
|
id: newMouse
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: muxModal.createNewSession()
|
|
}
|
|
}
|
|
|
|
// Sessions List
|
|
Rectangle {
|
|
width: parent.width
|
|
height: parent.height - 88 - 48 - shortcutsBar.height - Theme.spacingS * 3
|
|
radius: Theme.cornerRadius
|
|
color: "transparent"
|
|
|
|
ScrollView {
|
|
anchors.fill: parent
|
|
clip: true
|
|
|
|
Column {
|
|
width: parent.width
|
|
spacing: Theme.spacingXS
|
|
|
|
Repeater {
|
|
model: ScriptModel {
|
|
values: muxModal.filteredSessions
|
|
}
|
|
|
|
delegate: Rectangle {
|
|
required property var modelData
|
|
required property int index
|
|
|
|
width: parent.width
|
|
height: 64
|
|
radius: Theme.cornerRadius
|
|
color: muxModal.selectedIndex === index ? Theme.primaryContainer :
|
|
(sessionMouse.containsMouse ? Theme.surfaceContainerHigh : "transparent")
|
|
|
|
MouseArea {
|
|
id: sessionMouse
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: muxModal.attachToSession(modelData.name)
|
|
}
|
|
|
|
RowLayout {
|
|
anchors.fill: parent
|
|
anchors.leftMargin: Theme.spacingM
|
|
anchors.rightMargin: Theme.spacingM
|
|
spacing: Theme.spacingM
|
|
|
|
// Avatar
|
|
Rectangle {
|
|
Layout.preferredWidth: 40
|
|
Layout.preferredHeight: 40
|
|
radius: 20
|
|
color: modelData.attached ? Theme.primaryContainer : Theme.surfaceContainerHigh
|
|
|
|
StyledText {
|
|
anchors.centerIn: parent
|
|
text: modelData.name.charAt(0).toUpperCase()
|
|
font.pixelSize: Theme.fontSizeLarge
|
|
font.weight: Font.Bold
|
|
color: modelData.attached ? Theme.primary : Theme.surfaceText
|
|
}
|
|
}
|
|
|
|
// Info
|
|
Column {
|
|
Layout.fillWidth: true
|
|
spacing: 2
|
|
|
|
StyledText {
|
|
text: modelData.name
|
|
font.pixelSize: Theme.fontSizeMedium
|
|
font.weight: Font.Medium
|
|
color: Theme.surfaceText
|
|
elide: Text.ElideRight
|
|
}
|
|
|
|
StyledText {
|
|
text: {
|
|
var parts = []
|
|
if (modelData.windows !== "N/A")
|
|
parts.push(I18n.tr("%1 windows").arg(modelData.windows))
|
|
parts.push(modelData.attached ? I18n.tr("attached") : I18n.tr("detached"))
|
|
return parts.join(" \u2022 ")
|
|
}
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
color: Theme.surfaceVariantText
|
|
}
|
|
}
|
|
|
|
// Rename button (tmux only)
|
|
Rectangle {
|
|
Layout.preferredWidth: 36
|
|
Layout.preferredHeight: 36
|
|
radius: 18
|
|
visible: MuxService.supportsRename
|
|
color: renameMouse.containsMouse ? Theme.surfaceContainerHighest : "transparent"
|
|
|
|
DankIcon {
|
|
anchors.centerIn: parent
|
|
name: "edit"
|
|
size: Theme.iconSizeSmall
|
|
color: renameMouse.containsMouse ? Theme.primary : Theme.surfaceVariantText
|
|
}
|
|
|
|
MouseArea {
|
|
id: renameMouse
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: muxModal.renameSession(modelData.name)
|
|
}
|
|
}
|
|
|
|
// Delete button
|
|
Rectangle {
|
|
Layout.preferredWidth: 36
|
|
Layout.preferredHeight: 36
|
|
radius: 18
|
|
color: deleteMouse.containsMouse ? Theme.errorContainer : "transparent"
|
|
|
|
DankIcon {
|
|
anchors.centerIn: parent
|
|
name: "delete"
|
|
size: Theme.iconSizeSmall
|
|
color: deleteMouse.containsMouse ? Theme.error : Theme.surfaceVariantText
|
|
}
|
|
|
|
MouseArea {
|
|
id: deleteMouse
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: {
|
|
muxModal.killSession(modelData.name)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Empty state
|
|
Item {
|
|
width: parent.width
|
|
height: muxModal.filteredSessions.length === 0 ? 200 : 0
|
|
visible: muxModal.filteredSessions.length === 0
|
|
|
|
Column {
|
|
anchors.centerIn: parent
|
|
spacing: Theme.spacingM
|
|
|
|
DankIcon {
|
|
name: muxModal.searchText.length > 0 ? "search_off" : "terminal"
|
|
size: 48
|
|
color: Theme.surfaceVariantText
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
}
|
|
|
|
StyledText {
|
|
text: muxModal.searchText.length > 0 ? I18n.tr("No sessions found") : I18n.tr("No active %1 sessions").arg(MuxService.displayName)
|
|
font.pixelSize: Theme.fontSizeMedium
|
|
color: Theme.surfaceVariantText
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
}
|
|
|
|
StyledText {
|
|
text: muxModal.searchText.length > 0 ? I18n.tr("Try a different search") : I18n.tr("Press 'n' or click 'New Session' to create one")
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
color: Theme.surfaceVariantText
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Shortcuts bar
|
|
Row {
|
|
id: shortcutsBar
|
|
width: parent.width
|
|
spacing: Theme.spacingM
|
|
bottomPadding: Theme.spacingS
|
|
|
|
Repeater {
|
|
model: {
|
|
var shortcuts = [
|
|
{ key: "↑↓", label: I18n.tr("Navigate") },
|
|
{ key: "↵", label: I18n.tr("Attach") },
|
|
{ key: "^N", label: I18n.tr("New") },
|
|
{ key: "^D", label: I18n.tr("Kill") },
|
|
{ key: "Esc", label: I18n.tr("Close") }
|
|
]
|
|
if (MuxService.supportsRename)
|
|
shortcuts.splice(3, 0, { key: "^R", label: I18n.tr("Rename") })
|
|
return shortcuts
|
|
}
|
|
|
|
delegate: Row {
|
|
required property var modelData
|
|
spacing: 4
|
|
|
|
Rectangle {
|
|
width: keyText.width + Theme.spacingS
|
|
height: keyText.height + 4
|
|
radius: 4
|
|
color: Theme.surfaceContainerHighest
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
|
|
StyledText {
|
|
id: keyText
|
|
anchors.centerIn: parent
|
|
text: modelData.key
|
|
font.pixelSize: Theme.fontSizeSmall - 1
|
|
font.weight: Font.Medium
|
|
color: Theme.surfaceVariantText
|
|
}
|
|
}
|
|
|
|
StyledText {
|
|
text: modelData.label
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
color: Theme.surfaceVariantText
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|