Warning, /plasma-mobile/qmlkonsole/src/contents/ui/TerminalPage.qml is written in an unsupported language. File is not indexed.

0001 // SPDX-FileCopyrightText: 2019-2020 Jonah BrĂ¼chert <jbb@kaidan.im>
0002 // SPDX-FileCopyrightText: 2021-2022 Devin Lin <espidev@gmail.com>
0003 //
0004 // SPDX-License-Identifier: GPL-2.0-or-later
0005 
0006 import QtQuick
0007 import QtQuick.Layouts
0008 import QtQuick.Controls
0009 
0010 import org.kde.konsoleqml
0011 import org.kde.kirigami as Kirigami
0012 
0013 import org.kde.qmlkonsole
0014 
0015 Kirigami.Page {
0016     id: root
0017     property TerminalEmulator currentTerminal: tabSwipeView.contentChildren[tabSwipeView.currentIndex].termWidget
0018 
0019     background: Item {}
0020 
0021     topPadding: 0
0022     bottomPadding: 0
0023     leftPadding: 0
0024     rightPadding: 0
0025 
0026     property bool initialSessionCreated: false
0027 
0028     function forceTerminalFocus() {
0029         const wasVisible = Qt.inputMethod.visible;
0030         currentTerminal.forceActiveFocus();
0031         if (!wasVisible) {
0032             Qt.inputMethod.hide();
0033         }
0034     }
0035 
0036     Component.onCompleted: {
0037         // focus terminal text input immediately after load
0038         forceTerminalFocus();
0039     }
0040 
0041     // switch tab button
0042     titleDelegate: ToolButton {
0043         Layout.fillHeight: true
0044         onClicked: selectTabDialog.open()
0045         contentItem: RowLayout {
0046             spacing: 0
0047             Kirigami.Heading {
0048                 text: currentTerminal.tabName
0049                 Layout.leftMargin: Kirigami.Units.largeSpacing
0050                 Layout.rightMargin: Kirigami.Units.largeSpacing
0051             }
0052             Kirigami.Icon {
0053                 Layout.alignment: Qt.AlignVCenter
0054                 Layout.rightMargin: Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing
0055                 implicitHeight: Kirigami.Units.iconSizes.small
0056                 implicitWidth: Kirigami.Units.iconSizes.small
0057                 source: "go-down"
0058             }
0059         }
0060     }
0061 
0062     property bool isWideScreen: root.width > 540
0063 
0064     function openSettings() {
0065         if (isWideScreen) {
0066             root.settingsDialogLoader.active = true;
0067             root.settingsDialogLoader.item.open();
0068         } else {
0069             pageStack.push("qrc:/SettingsPage.qml",
0070                 {
0071                     terminal: currentTerminal
0072                 }
0073             );
0074         }
0075     }
0076 
0077     property var settingsDialogLoader: Loader {
0078         active: false
0079         sourceComponent: SettingsDialog {
0080             id: settingsDialog
0081             terminal: currentTerminal
0082         }
0083     }
0084 
0085     function switchToTab(index) {
0086         tabSwipeView.setCurrentIndex(index);
0087     }
0088 
0089     function closeTab(index) {
0090         if (tabSwipeView.contentChildren[index].termWidget.session.hasActiveProcess) {
0091             selectTabDialog.close();
0092             confirmDialog.indexToClose = index;
0093             confirmDialog.open();
0094         } else {
0095             TerminalTabModel.removeTab(index);
0096         }
0097     }
0098 
0099     Shortcut {
0100         sequence: "Shift+Left"
0101         onActivated: {
0102             if (tabSwipeView.currentIndex > 0)
0103                 tabSwipeView.currentIndex--
0104         }
0105     }
0106 
0107     Shortcut {
0108         sequence: "Shift+Right"
0109         onActivated: {
0110             if (tabSwipeView.currentIndex < tabSwipeView.count - 1)
0111                 tabSwipeView.currentIndex++
0112         }
0113     }
0114 
0115     property list<Kirigami.Action> menuActions: [
0116         Kirigami.Action {
0117             id: newTabAction
0118             icon.name: "list-add"
0119             text: i18nc("@action:intoolbar", "New Tab")
0120             onTriggered: {
0121                 TerminalTabModel.newTab();
0122                 tabSwipeView.currentIndex = tabSwipeView.contentChildren.length - 1;
0123             }
0124             shortcut: "Ctrl+Shift+T"
0125             displayHint: Kirigami.DisplayHint.KeepVisible
0126         },
0127         Kirigami.Action {
0128             icon.name: "dialog-scripts"
0129             text: i18nc("@action:intoolbar", "Saved Commands")
0130             onTriggered: savedCommandsDialog.open()
0131             displayHint: Kirigami.DisplayHint.KeepVisible
0132         },
0133         Kirigami.Action {
0134             displayHint: Kirigami.DisplayHint.AlwaysHide
0135             icon.name: "search"
0136             text: i18n("reverse-i-search")
0137             onTriggered: {
0138                 currentTerminal.pressKey(Qt.Key_R, Qt.ControlModifier, true)
0139             }
0140         },
0141         Kirigami.Action {
0142             displayHint: Kirigami.DisplayHint.AlwaysHide
0143             icon.name: "dialog-cancel"
0144             text: i18n("Cancel current command")
0145             onTriggered: {
0146                 currentTerminal.pressKey(Qt.Key_C, Qt.ControlModifier, true)
0147             }
0148         },
0149         Kirigami.Action {
0150             displayHint: Kirigami.DisplayHint.AlwaysHide
0151             icon.name: "document-close"
0152             text: i18n("Send EOF")
0153             onTriggered: {
0154                 currentTerminal.pressKey(Qt.Key_D, Qt.ControlModifier, true)
0155             }
0156         },
0157         Kirigami.Action {
0158             displayHint: Kirigami.DisplayHint.AlwaysHide
0159             icon.name: "bboxprev"
0160             text: i18n("Cursor to line start")
0161             onTriggered: {
0162                 currentTerminal.pressKey(Qt.Key_A, Qt.ControlModifier, true)
0163             }
0164         },
0165         Kirigami.Action {
0166             displayHint: Kirigami.DisplayHint.AlwaysHide
0167             icon.name: "bboxnext"
0168             text: i18n("Cursor to line end")
0169             onTriggered: {
0170                 currentTerminal.pressKey(Qt.Key_E, Qt.ControlModifier, true)
0171             }
0172         },
0173         Kirigami.Action {
0174             displayHint: Kirigami.DisplayHint.AlwaysHide
0175             icon.name: "edit-cut"
0176             text: i18n("Kill to line end")
0177             onTriggered: {
0178                 currentTerminal.pressKey(Qt.Key_K, Qt.ControlModifier, true)
0179             }
0180         },
0181         Kirigami.Action {
0182             displayHint: Kirigami.DisplayHint.AlwaysHide
0183             icon.name: "edit-paste"
0184             text: i18n("Paste from kill buffer")
0185             onTriggered: {
0186                 currentTerminal.pressKey(Qt.Key_Y, Qt.ControlModifier, true)
0187             }
0188         },
0189         Kirigami.Action {
0190             displayHint: Kirigami.DisplayHint.AlwaysHide
0191             icon.name: "edit-copy"
0192             text: i18n("Copy")
0193             onTriggered: {
0194                 currentTerminal.copyClipboard();
0195                 root.currentTerminal.touchSelectionMode = false;
0196             }
0197             shortcut: "Ctrl+Shift+C"
0198         },
0199         Kirigami.Action {
0200             displayHint: Kirigami.DisplayHint.AlwaysHide
0201             icon.name: "edit-paste"
0202             text: i18n("Paste")
0203             onTriggered: currentTerminal.pasteClipboard();
0204             shortcut: "Ctrl+Shift+V"
0205         },
0206         Kirigami.Action {
0207             displayHint: Kirigami.DisplayHint.IconOnly
0208             text: i18n("Settings")
0209             icon.name: "settings-configure"
0210             onTriggered: root.openSettings()
0211         }
0212     ]
0213 
0214     header: Loader {
0215         visible: active
0216         active: root.isWideScreen && root.height > 360
0217         sourceComponent: Control {
0218             id: tabControl
0219             height: visible ? implicitHeight : 0
0220             visible: tabControlListView.count > 1
0221 
0222             leftPadding: 0
0223             rightPadding: 0
0224             topPadding: 0
0225             bottomPadding: 0
0226 
0227             width: root.width
0228 
0229             background: Rectangle {
0230                 color: Kirigami.Theme.backgroundColor
0231             }
0232 
0233             contentItem: Tabs {
0234                 id: tabControlListView
0235                 currentIndex: tabSwipeView.currentIndex
0236 
0237                 onSwitchToTabRequested: index => root.switchToTab(index)
0238                 onCloseTabRequested: index => root.closeTab(index)
0239                 onAddTabRequested: newTabAction.trigger()
0240             }
0241         }
0242     }
0243 
0244     ColumnLayout {
0245         spacing: 0
0246         anchors.fill: parent
0247 
0248         SavedCommandsDialog {
0249             id: savedCommandsDialog
0250             terminal: root.currentTerminal
0251         }
0252 
0253         Kirigami.Dialog {
0254             id: confirmDialog
0255 
0256             property int indexToClose: 0
0257             property var selectedTerminal: tabSwipeView.contentChildren[indexToClose]
0258 
0259             title: i18nc("@title:window", "Confirm closing %1", selectedTerminal ? selectedTerminal.termWidget.tabName : "")
0260             standardButtons: Dialog.Yes | Dialog.Cancel
0261             padding: Kirigami.Units.gridUnit
0262 
0263             onAccepted: {
0264                 TerminalTabModel.removeTab(indexToClose);
0265                 selectTabDialog.open();
0266             }
0267             onRejected: {
0268                 selectTabDialog.open();
0269             }
0270 
0271             RowLayout {
0272                 Label {
0273                     Layout.maximumWidth: Kirigami.Units.gridUnit * 15
0274                     wrapMode: Text.Wrap
0275                     text: i18n("A process is currently running in this tab. Are you sure you want to close it?")
0276                 }
0277             }
0278         }
0279 
0280         Kirigami.Dialog {
0281             id: selectTabDialog
0282             title: i18nc("@title:window", "Select Tab")
0283             standardButtons: Dialog.Close
0284 
0285             ListView {
0286                 id: tabListView
0287                 implicitWidth: Kirigami.Units.gridUnit * 16
0288                 implicitHeight: Kirigami.Units.gridUnit * 18
0289                 Kirigami.Theme.inherit: false
0290                 Kirigami.Theme.colorSet: Kirigami.Theme.View
0291                 model: TerminalTabModel
0292 
0293                 delegate: ItemDelegate {
0294                     width: tabListView.width
0295                     topPadding: Kirigami.Units.smallSpacing
0296                     bottomPadding: Kirigami.Units.smallSpacing
0297 
0298                     onClicked: {
0299                         tabSwipeView.currentIndex = index;
0300                         selectTabDialog.close();
0301                     }
0302 
0303                     contentItem: RowLayout {
0304                         Label {
0305                             text: model.name
0306                             Layout.fillWidth: true
0307                         }
0308 
0309                         RadioButton {
0310                             Layout.alignment: Qt.AlignVCenter
0311                             checked: tabSwipeView.currentIndex == index
0312                             onClicked: {
0313                                 root.switchToTab(index);
0314                                 selectTabDialog.close();
0315                             }
0316                         }
0317 
0318                         ToolButton {
0319                             Layout.alignment: Qt.AlignVCenter
0320                             icon.name: "delete"
0321                             text: i18n("Close")
0322                             display: ToolButton.IconOnly
0323                             onClicked: root.closeTab(index)
0324                         }
0325                     }
0326                 }
0327             }
0328         }
0329 
0330         Kirigami.InlineMessage {
0331             id: selectionModePopup
0332             visible: root.currentTerminal.touchSelectionMode
0333             implicitWidth: parent.width
0334             text: i18n("selection mode")
0335 
0336             onVisibleChanged: root.forceTerminalFocus();
0337 
0338             type: Kirigami.MessageType.Information
0339 
0340             actions: [
0341                 Kirigami.Action {
0342                     text: i18n("Disable")
0343                     onTriggered: root.currentTerminal.touchSelectionMode = false
0344                 }
0345             ]
0346         }
0347 
0348         // tabs
0349         SwipeView {
0350             id: tabSwipeView
0351             interactive: !selectionModePopup.visible && Kirigami.Settings.hasTransientTouchInput // don't conflict with selection mode
0352 
0353             Layout.fillWidth: true
0354             Layout.fillHeight: true
0355 
0356             onCurrentItemChanged: currentTerminal.forceActiveFocus()
0357 
0358             Repeater {
0359                 id: terminalRepeater
0360                 model: TerminalTabModel
0361 
0362                 delegate: Item {
0363                     property alias termWidget: terminal
0364 
0365                     TerminalEmulator {
0366                         id: terminal
0367                         anchors.fill: parent
0368 
0369                         readonly property string tabName: model.name
0370                         readonly property int modelIndex: model.index
0371                         readonly property bool isCurrentItem: SwipeView.isCurrentItem
0372 
0373                         // with touch, to select text we first require users to press-and-hold to enter the selection mode
0374                         property bool touchSelectionMode: false
0375 
0376                         onIsCurrentItemChanged: {
0377                             if (isCurrentItem) {
0378                                 root.forceTerminalFocus();
0379                             }
0380                         }
0381 
0382                         font.family: TerminalSettings.fontFamily
0383                         font.pixelSize: TerminalSettings.fontSize
0384 
0385                         colorScheme: TerminalSettings.colorScheme
0386                         opacity: TerminalSettings.windowOpacity
0387 
0388                         Component.onCompleted: {
0389                             if (!root.initialSessionCreated) {
0390                                 // setup for CLI arguments
0391                                 if (Util.initialWorkDir) {
0392                                     mainsession.initialWorkingDirectory = Util.initialWorkDir;
0393                                 }
0394                                 if (Util.initialCommand) {
0395                                     mainsession.sendText(Util.initialCommand);
0396                                     terminal.pressKey(Qt.Key_Enter, Qt.NoModifier, true);
0397                                 }
0398 
0399                                 root.initialSessionCreated = true;
0400                             }
0401 
0402                             mainsession.startShellProgram();
0403 
0404                             if (root.hasOwnProperty("contextualActions")) {
0405                                 root.contextualActions = Qt.binding(() => menuActions)
0406                             } else {
0407                                 root.actions = Qt.binding(() => menuActions)
0408                             }
0409                         }
0410 
0411                         function pressKey(key, modifiers, pressed, nativeScanCode, text) {
0412                             terminal.simulateKeyPress(key, modifiers, pressed, nativeScanCode, text);
0413                             root.forceTerminalFocus();
0414                         }
0415 
0416                         session: TerminalSession {
0417                             id: mainsession
0418                             initialWorkingDirectory: "$HOME"
0419                             shellProgram: ShellCommand.executable
0420                             shellProgramArgs: ShellCommand.args
0421                             onFinished: {
0422                                 if (terminalRepeater.count == 1) {
0423                                     Qt.quit()
0424                                 }
0425 
0426                                 root.closeTab(terminal.modelIndex)
0427                             }
0428                             onMatchFound: {
0429                                 console.log("found at: %1 %2 %3 %4".arg(startColumn).arg(startLine).arg(endColumn).arg(endLine));
0430                             }
0431                             onNoMatchFound: {
0432                                 console.log("not found");
0433                             }
0434                         }
0435 
0436                         ScrollBar {
0437                             Kirigami.Theme.colorSet: Kirigami.Theme.Complementary // text color of terminal is also complementary
0438                             Kirigami.Theme.inherit: false
0439                             anchors {
0440                                 right: parent.right
0441                                 top: parent.top
0442                                 bottom: parent.bottom
0443                             }
0444                             visible: true
0445                             orientation: Qt.Vertical
0446                             size: (terminal.lines / (terminal.lines + terminal.scrollbarMaximum - terminal.scrollbarMinimum))
0447                             position: terminal.scrollbarCurrentValue / (terminal.lines + terminal.scrollbarMaximum)
0448                             interactive: false
0449                         }
0450 
0451                         // terminal focus on mouse click
0452                         TapHandler {
0453                             acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus
0454                             cursorShape: Qt.IBeamCursor
0455                             onTapped: root.forceTerminalFocus();
0456                         }
0457 
0458                         // enter touch selection mode
0459                         TapHandler {
0460                             acceptedDevices: PointerDevice.TouchScreen
0461                             enabled: !terminal.touchSelectionMode
0462                             onLongPressed: terminal.touchSelectionMode = true
0463                         }
0464 
0465                         // simulate scrolling for touch (TODO velocity)
0466                         DragHandler {
0467                             acceptedDevices: PointerDevice.TouchScreen
0468                             enabled: !terminal.touchSelectionMode
0469 
0470                             property real previousY
0471                             onActiveChanged: {
0472                                 previousY = 0;
0473                             }
0474                             onTranslationChanged: {
0475                                 terminal.simulateWheel(0, 0, 0, 0, Qt.point(0, (translation.y - previousY) * 2));
0476                                 previousY = translation.y;
0477                             }
0478                         }
0479                     }
0480                 }
0481             }
0482         }
0483     }
0484 
0485     footer: TerminalKeyToolBar {
0486         visible: Kirigami.Settings.isMobile || Kirigami.Settings.tabletMode
0487         terminal: root.currentTerminal
0488     }
0489 }