diff options
Diffstat (limited to 'src-qt5/desktop-utils/lumina-terminal')
-rw-r--r-- | src-qt5/desktop-utils/lumina-terminal/TermWindow.cpp | 301 | ||||
-rw-r--r-- | src-qt5/desktop-utils/lumina-terminal/TermWindow.h | 70 | ||||
-rw-r--r-- | src-qt5/desktop-utils/lumina-terminal/TerminalWidget.cpp | 504 | ||||
-rw-r--r-- | src-qt5/desktop-utils/lumina-terminal/TerminalWidget.h | 68 | ||||
-rw-r--r-- | src-qt5/desktop-utils/lumina-terminal/TrayIcon.cpp | 172 | ||||
-rw-r--r-- | src-qt5/desktop-utils/lumina-terminal/TrayIcon.h | 59 | ||||
-rw-r--r-- | src-qt5/desktop-utils/lumina-terminal/TtyProcess.cpp | 229 | ||||
-rw-r--r-- | src-qt5/desktop-utils/lumina-terminal/TtyProcess.h | 83 | ||||
-rw-r--r-- | src-qt5/desktop-utils/lumina-terminal/lumina-terminal.pro | 96 | ||||
-rw-r--r-- | src-qt5/desktop-utils/lumina-terminal/main.cpp | 47 |
10 files changed, 1629 insertions, 0 deletions
diff --git a/src-qt5/desktop-utils/lumina-terminal/TermWindow.cpp b/src-qt5/desktop-utils/lumina-terminal/TermWindow.cpp new file mode 100644 index 00000000..82f71e6b --- /dev/null +++ b/src-qt5/desktop-utils/lumina-terminal/TermWindow.cpp @@ -0,0 +1,301 @@ +//=========================================== +// Lumina-DE source code +// Copyright (c) 2015, Ken Moore +// Available under the 3-clause BSD license +// See the LICENSE file for full details +//=========================================== +#include "TermWindow.h" +//#include "ui_TermWindow.h" + +#include <QDesktopWidget> +#include <QDebug> +#include <QTimer> +#include <QApplication> +#include <QVBoxLayout> +#include "TerminalWidget.h" + +// =============== +// PUBLIC +// =============== +TermWindow::TermWindow(QSettings *set) : QWidget(0, Qt::Window | Qt::BypassWindowManagerHint){//, ui(new Ui::TermWindow){ + CLOSING = false; //internal flag + settings = set; + //Create the Window + this->setLayout(new QVBoxLayout()); + this->setCursor(Qt::SplitVCursor); + tabWidget = new QTabWidget(this); + tabWidget->clear(); //just in case + tabWidget->setCursor(Qt::ArrowCursor); + tabWidget->setTabBarAutoHide(true); + tabWidget->setTabsClosable(true); + tabWidget->setMovable(true); + tabWidget->setUsesScrollButtons(true); + this->layout()->addWidget(tabWidget); + //Setup the animation + ANIM = new QPropertyAnimation(this, "geometry", this); + ANIM->setDuration(300); //1/3 second animation + connect(ANIM, SIGNAL(finished()), this, SLOT(AnimFinished()) ); + //Create the keyboard shortcuts + //hideS = new QShortcut(QKeySequence(Qt::Key_Escape),this); + closeS = new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_Q),this); + newTabS = new QShortcut(QKeySequence::AddTab,this); + closeTabS = new QShortcut(QKeySequence::Close,this); + prevTabS = new QShortcut(QKeySequence::PreviousChild,this); + nextTabS = new QShortcut(QKeySequence::NextChild,this); + //Print out all the keyboard shortcuts onto the screen + qDebug() << "New Tab Shortcut:" << QKeySequence::keyBindings(QKeySequence::AddTab); + qDebug() << "Close Tab Shortcut:" << QKeySequence::keyBindings(QKeySequence::Close); + qDebug() << "Next Tab Shortcut:" << QKeySequence::keyBindings(QKeySequence::NextChild); + qDebug() << "Previous Tab Shortcut:" << QKeySequence::keyBindings(QKeySequence::PreviousChild); + //Connect the signals/slots + connect(tabWidget, SIGNAL(tabCloseRequested(int)), this, SLOT(Close_Tab(int)) ); + connect(tabWidget, SIGNAL(currentChanged(int)), this, SLOT(focusOnWidget()) ); + connect(closeTabS, SIGNAL(activated()), this, SLOT(Close_Tab()) ); + connect(newTabS, SIGNAL(activated()), this, SLOT(New_Tab()) ); + //connect(hideS, SIGNAL(activated()), this, SLOT(HideWindow()) ); + connect(closeS, SIGNAL(activated()), this, SLOT(CloseWindow()) ); + connect(prevTabS, SIGNAL(activated()), this, SLOT(Prev_Tab()) ); + connect(nextTabS, SIGNAL(activated()), this, SLOT(Next_Tab()) ); + + //Now set the defaults + screennum = 0; //default value + setTopOfScreen(true); //default value + if(settings->contains("lastSize")){ + //qDebug() << "Re-use last size:" << settings->value("lastSize").toSize(); + this->resize( settings->value("lastSize").toSize() ); + CalculateGeom(); + //qDebug() << "After size:" << this->size(); + } + + //this->resize(this->width(),300); + //this->setMinimumSize(20, 300); + +} + + +TermWindow::~TermWindow(){ + +} + +void TermWindow::cleanup(){ + //called right before the window is closed + //Make sure to close any open tabs/processes + CLOSING = true; + for(int i=0; i<tabWidget->count(); i++){ + static_cast<TerminalWidget*>(tabWidget->widget(i))->aboutToClose(); + } +} + +void TermWindow::OpenDirs(QStringList dirs){ + for(int i=0; i<dirs.length(); i++){ + //Open a new tab for each directory + TerminalWidget *page = new TerminalWidget(tabWidget, dirs[i]); + QString ID = GenerateTabID(); + page->setWhatsThis(ID); + tabWidget->addTab(page, ID); + tabWidget->setCurrentWidget(page); + page->setFocus(); + qDebug() << "New Tab:" << ID << dirs[i]; + connect(page, SIGNAL(ProcessClosed(QString)), this, SLOT(Close_Tab(QString)) ); + } +} + +void TermWindow::setCurrentScreen(int num){ + screennum = num; + QTimer::singleShot(0,this, SLOT(ReShowWindow())); +} + +void TermWindow::setTopOfScreen(bool ontop){ + onTop = ontop; + this->layout()->setContentsMargins(0, (onTop ? 0 : 3), 0, (onTop ? 3 : 0)); + tabWidget->setTabPosition(onTop ? QTabWidget::South : QTabWidget::North); + QTimer::singleShot(0,this, SLOT(ReShowWindow())); +} + +// ======================= +// PUBLIC SLOTS +// ======================= +void TermWindow::ShowWindow(){ + if(animRunning>=0){ return; } //something running + animRunning = 1; + this->hide(); + QApplication::processEvents(); + CalculateGeom(); + //Now setup the animation + ANIM->setEndValue(this->geometry()); + if(onTop){ //use top edge + ANIM->setStartValue( QRect(this->x(), this->y(), this->width(), 0) ); //same location - no height + }else{ + ANIM->setStartValue( QRect(this->x(), this->geometry().bottom(), this->width(), 0) ); //same location - no height + } + this->show(); + //qDebug() << "Start Animation" << ANIM->startValue() << ANIM->endValue(); + ANIM->start(); +} + +void TermWindow::HideWindow(){ + if(animRunning>=0){ return; } //something running + //Now setup the animation + //Note: Do *not* use the private settings/variables because it may be changing right now - use the current geometry *ONLY* + animRunning = 0; + ANIM->setStartValue(this->geometry()); + QDesktopWidget *desk = QApplication::desktop(); + int screen = desk->screenNumber(this); //which screen it is currently on + if(desk->availableGeometry(screen).top() == this->geometry().top()){ //use top edge + ANIM->setEndValue( QRect(this->x(), this->y(), this->width(), 0) ); //same location - no height + }else{ + ANIM->setEndValue( QRect(this->x(), this->y()+this->height(), this->width(), 0) ); //same location - no height + } + this->show(); + ANIM->start(); +} + +void TermWindow::CloseWindow(){ + if(animRunning>=0){ return; } //something running + //Now setup the animation + animRunning = 2; + ANIM->setStartValue(this->geometry()); + if(onTop){ //use top edge + ANIM->setEndValue( QRect(this->x(), this->y(), this->width(), 0) ); //same location - no height + }else{ + ANIM->setEndValue( QRect(this->x(), this->geometry().bottom(), this->width(), 0) ); //same location - no height + } + this->show(); + ANIM->start(); +} + +void TermWindow::ReShowWindow(){ + if(this->isVisible()){ + HideWindow(); //start with same animation as hide + animRunning = 3; //flag as a re-show (hide, then show); + }else{ + //Already hidden, just show it + ShowWindow(); + } +} +// ======================= +// PRIVATE +// ======================= +void TermWindow::CalculateGeom(){ + //qDebug() << "Calculating Geom:" << this->size(); + QDesktopWidget *desk = QApplication::desktop(); + if(desk->screenCount() <= screennum){ screennum = desk->primaryScreen(); } //invalid screen detected + //Now align the window with the proper screen edge + QRect workarea = desk->availableGeometry(screennum); //this respects the WORKAREA property + if(onTop){ + this->setGeometry( workarea.x(), workarea.y(), workarea.width(), this->height()); //maintain current hight of window + + }else{ + this->setGeometry( workarea.x(), workarea.y() + workarea.height() - this->height(), workarea.width(), this->height()); //maintain current hight of window + } + this->setFixedWidth(this->width()); //Make sure the window is not re-sizeable in the width dimension + this->setMinimumHeight(0); +} + +QString TermWindow::GenerateTabID(){ + //generate a unique ID for this new tab + int num = 1; + for(int i=0; i<tabWidget->count(); i++){ + if(tabWidget->widget(i)->whatsThis().toInt() >= num){ num = tabWidget->widget(i)->whatsThis().toInt()+1; } + } + return QString::number(num); +} + +// ======================= +// PRIVATE SLOTS +// ======================= + +//Tab Interactions +void TermWindow::New_Tab(){ + OpenDirs(QStringList() << QDir::homePath()); +} + +void TermWindow::Close_Tab(int tab){ + //qDebug() << "Close Tab:" << tab; + if(tab<0){ tab = tabWidget->currentIndex(); } + static_cast<TerminalWidget*>(tabWidget->widget(tab))->aboutToClose(); + tabWidget->widget(tab)->deleteLater(); //delete the page within the tag + tabWidget->removeTab(tab); // remove the tab itself + //Let the tray know when the last terminal is closed + if(tabWidget->count() < 1){ + emit TerminalFinished(); + } +} + +void TermWindow::Close_Tab(QString ID){ + //Close a tab based on it's ID instead of it's tab number + for(int i=0; i<tabWidget->count(); i++){ + if(tabWidget->widget(i)->whatsThis()==ID){ + Close_Tab(i); + return; //all done + } + } +} + +void TermWindow::Next_Tab(){ + qDebug() << "Next Tab"; + int next = tabWidget->currentIndex()+1; + if(next>=tabWidget->count()){ next = 0; } + tabWidget->setCurrentIndex(next); +} + +void TermWindow::Prev_Tab(){ + qDebug() << "Previous Tab"; + int next = tabWidget->currentIndex()-1; + if(next<0){ next = tabWidget->count()-1; } + tabWidget->setCurrentIndex(next); +} + +void TermWindow::focusOnWidget(){ + if(tabWidget->currentWidget()!=0){ + tabWidget->currentWidget()->setFocus(); + } +} + +//Animation finishing +void TermWindow::AnimFinished(){ + if(animRunning <0){ return; } //nothing running + if(animRunning==0){ + //Hide Event + this->hide(); //need to hide the whole thing now + this->setGeometry( ANIM->startValue().toRect() ); //reset back to initial size after hidden + emit TerminalHidden(); + }else if(animRunning==1){ + //Show Event + this->activateWindow(); + tabWidget->currentWidget()->setFocus(); + emit TerminalVisible(); + }else if(animRunning==2){ + //Close Event + this->hide(); //need to hide the whole thing now + emit TerminalClosed(); + }else if(animRunning>2){ + //Re-Show event + this->hide(); + this->setGeometry( ANIM->startValue().toRect() ); //reset back to initial size after hidden + //Now re-show it + QTimer::singleShot(0,this, SLOT(ShowWindow())); + } + animRunning = -1; //done +} + +// =================== +// PROTECTED +// =================== +void TermWindow::mouseMoveEvent(QMouseEvent *ev){ + //Note: With mouse tracking turned off, this event only happens when the user is holding down the mouse button + if(onTop){ + //Move the bottom edge to the current point + if( (ev->globalPos().y() - this->y()) < 50){ return; } //quick check that the window is not smaller than 20 pixels + QRect geom = this->geometry(); + geom.setBottom(ev->globalPos().y()); + this->setGeometry(geom); + }else{ + //Move the top edge to the current point + if( (this->y() + this->height() -ev->globalPos().y()) < 50){ return; } //quick check that the window is not smaller than 20 pixels + QRect geom = this->geometry(); + geom.setTop(ev->globalPos().y()); + this->setGeometry(geom); + } + settings->setValue("lastSize",this->geometry().size()); +}
\ No newline at end of file diff --git a/src-qt5/desktop-utils/lumina-terminal/TermWindow.h b/src-qt5/desktop-utils/lumina-terminal/TermWindow.h new file mode 100644 index 00000000..d68c5457 --- /dev/null +++ b/src-qt5/desktop-utils/lumina-terminal/TermWindow.h @@ -0,0 +1,70 @@ +//=========================================== +// Lumina-DE source code +// Copyright (c) 2015, Ken Moore +// Available under the 3-clause BSD license +// See the LICENSE file for full details +//=========================================== +#ifndef _LUMINA_DESKTOP_UTILITIES_TERMINAL_MAIN_WINDOW_H +#define _LUMINA_DESKTOP_UTILITIES_TERMINAL_MAIN_WINDOW_H + +#include <QWidget> +#include <QPropertyAnimation> +#include <QTabWidget> +#include <QDir> +#include <QShortcut> +#include <QMouseEvent> +#include <QSettings> + +class TermWindow : public QWidget{ + Q_OBJECT +public: + TermWindow(QSettings *set); + ~TermWindow(); + + void cleanup(); //called right before the window is closed + void OpenDirs(QStringList); + + void setCurrentScreen(int num = 0); + void setTopOfScreen(bool ontop); + +public slots: + void ShowWindow(); + void HideWindow(); + void CloseWindow(); + void ReShowWindow(); + +private: + QTabWidget *tabWidget; + QSettings *settings; + QShortcut *hideS, *closeS, *newTabS, *closeTabS, *prevTabS, *nextTabS; + int screennum; + bool onTop, CLOSING; + QPropertyAnimation *ANIM; + int animRunning; //internal flag for what animation is currently running + + //Calculate the window geometry necessary based on screen/location + void CalculateGeom(); + QString GenerateTabID(); + +private slots: + //Tab Interactions + void New_Tab(); + void Close_Tab(int tab = -1); + void Close_Tab(QString ID); //alternate form of the close routine - based on tab ID + void Next_Tab(); + void Prev_Tab(); + void focusOnWidget(); + //Animation finishing + void AnimFinished(); + +protected: + void mouseMoveEvent(QMouseEvent*); + +signals: + void TerminalHidden(); + void TerminalVisible(); + void TerminalClosed(); + void TerminalFinished(); +}; + +#endif diff --git a/src-qt5/desktop-utils/lumina-terminal/TerminalWidget.cpp b/src-qt5/desktop-utils/lumina-terminal/TerminalWidget.cpp new file mode 100644 index 00000000..a90d9846 --- /dev/null +++ b/src-qt5/desktop-utils/lumina-terminal/TerminalWidget.cpp @@ -0,0 +1,504 @@ +//=========================================== +// Lumina-DE source code +// Copyright (c) 2015, Ken Moore +// Available under the 3-clause BSD license +// See the LICENSE file for full details +//=========================================== +#include "TerminalWidget.h" + +#include <QProcessEnvironment> +#include <QDebug> +#include <QApplication> +#include <QScrollBar> +#include <QTextBlock> + +#include <LuminaXDG.h> + +//Special control code ending symbols (aside from letters) + +TerminalWidget::TerminalWidget(QWidget *parent, QString dir) : QTextEdit(parent){ + //Setup the text widget + this->setLineWrapMode(QTextEdit::WidgetWidth); + this->setAcceptRichText(false); + this->setOverwriteMode(true); + this->setFocusPolicy(Qt::StrongFocus); + this->setWordWrapMode(QTextOption::NoWrap); + this->setContextMenuPolicy(Qt::CustomContextMenu); + DEFFMT = this->textCursor().charFormat(); //save the default structure for later + CFMT = this->textCursor().charFormat(); //current format + selCursor = this->textCursor(); //used for keeping track of selections + lastCursor = this->textCursor(); + startrow = endrow = -1; + altkeypad = false; + QFontDatabase FDB; + QStringList fonts = FDB.families(QFontDatabase::Latin); + for(int i=0; i<fonts.length(); i++){ + if(FDB.isFixedPitch(fonts[i])){ this->setFont(QFont(fonts[i])); qDebug() << "Using Font:" << fonts[i]; break; } + } + //Create/open the TTY port + PROC = new TTYProcess(this); + qDebug() << "Open new TTY"; + //int fd; + bool ok = PROC->startTTY( QProcessEnvironment::systemEnvironment().value("SHELL","/bin/sh"), QStringList(), dir); + qDebug() << " - opened:" << ok; + this->setEnabled(PROC->isOpen()); + contextMenu = new QMenu(this); + copyA = contextMenu->addAction(LXDG::findIcon("edit-copy"), tr("Copy Selection"), this, SLOT(copySelection()) ); + pasteA = contextMenu->addAction(LXDG::findIcon("edit-paste"), tr("Paste"), this, SLOT(pasteSelection()) ); + //Connect the signals/slots + connect(PROC, SIGNAL(readyRead()), this, SLOT(UpdateText()) ); + connect(PROC, SIGNAL(processClosed()), this, SLOT(ShellClosed()) ); + +} + +TerminalWidget::~TerminalWidget(){ + aboutToClose(); +} + +void TerminalWidget::aboutToClose(){ + if(PROC->isOpen()){ PROC->closeTTY(); } //TTY PORT +} + +// ================== +// PRIVATE +// ================== +void TerminalWidget::InsertText(QString txt){ + if(txt.isEmpty()){ return; } + //qDebug() << "Insert Text:" << txt << "Cursor Pos:" << this->textCursor().position() << "Column:" << this->textCursor().columnNumber(); + QTextCursor cur = this->textCursor(); + cur.setCharFormat(CFMT); + cur.insertText( txt, CFMT); + this->setTextCursor(cur); +} + +void TerminalWidget::applyData(QByteArray data){ + //Make sure the current cursor is the right cursor + if(this->textCursor()==selCursor){ this->setTextCursor(lastCursor); } + //Iterate through the data and apply it when possible + QByteArray chars; + //qDebug() << "Data:" << data; + for(int i=0; i<data.size(); i++){ + if( data.at(i)=='\b' ){ + //Flush current text buffer to widget + //Simple cursor backward 1 (NOT backspace in this context!! - this widget should be in "insert" mode instead) + InsertText(chars); chars.clear(); + this->moveCursor(QTextCursor::Left, QTextCursor::MoveAnchor); + //}else if( data.at(i)=='\t' ){ + //chars.append(" "); + }else if( data.at(i)=='\x1B' ){ + //Flush current text buffer to widget + if(!chars.isEmpty()){ InsertText(chars); chars.clear(); } + //ANSI Control Code start + //Look for the end of the code + int end = -1; + for(int j=1; j<(data.size()-i) && end<0; j++){ + if(QChar(data.at(i+j)).isLetter() || (QChar(data.at(i+j)).isSymbol() && data.at(i+j)!=';') ){ end = j; } + else if(data.at(i+j)=='\x1B'){ end = j-1; } //start of the next control code + } + if(end<0){ return; } //skip everything else - no end to code found + applyANSI(data.mid(i+1, end)); + //qDebug() << "Code:" << data.mid(i+1, end) << "Next Char:" << data[i+end+2]; + i+=end; //move the final loop along - already handled these bytes + + }else if( data.at(i) != '\r' ){ + //Special Check: if inserting text within a line, clear the rest of this line first + if(i==0 && this->textCursor().position() < this->document()->characterCount()-1){ + applyANSI("[K"); + } + chars.append(data.at(i)); + //Plaintext character - just add it here + //qDebug() << "Insert Text:" << data.at(i) << CFMT.foreground().color() << CFMT.background().color(); + //qDebug() << " " << this->currentCharFormat().foreground().color() << this->currentCharFormat().background().color(); + //this->textCursor().insertText( QChar(data.at(i)), CFMT ); + } + } //end loop over data + if(!chars.isEmpty()){ InsertText(chars); } +} + +void TerminalWidget::applyANSI(QByteArray code){ + //Note: the first byte is often the "[" character + qDebug() << "Handle ANSI:" << code; + if(code.length()==1){ + //KEYPAD MODES + if(code.at(0)=='='){ altkeypad = true; } + else if(code.at(0)=='>'){ altkeypad = false; } + else{ + qDebug() << "Unhandled ANSI Code:" << code; + } + }else if(code.startsWith("[")){ + // VT100 ESCAPE CODES + //CURSOR MOVEMENT + if( code.endsWith("A") ){ //Move Up + int num = 1; + if(code.size()>2){ num = code.mid(1, code.size()-2).toInt(); } //everything in the middle + QTextCursor cur = this->textCursor(); + cur.movePosition(QTextCursor::Up, QTextCursor::MoveAnchor, num); + this->setTextCursor(cur); + }else if(code.endsWith("B")){ //Move Down + int num = 1; + if(code.size()>2){ num = code.mid(1, code.size()-2).toInt(); } //everything in the middle + QTextCursor cur = this->textCursor(); + cur.movePosition(QTextCursor::Down, QTextCursor::MoveAnchor, num); + this->setTextCursor(cur); + }else if(code.endsWith("C")){ //Move Forward + int num = 1; + if(code.size()>2){ num = code.mid(1, code.size()-2).toInt(); } //everything in the middle + QTextCursor cur = this->textCursor(); + cur.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, num); + this->setTextCursor(cur); + }else if(code.endsWith("D")){ //Move Back + int num = 1; + if(code.size()>2){ num = code.mid(1, code.size()-2).toInt(); } //everything in the middle + QTextCursor cur = this->textCursor(); + cur.movePosition(QTextCursor::Left, QTextCursor::MoveAnchor, num); + this->setTextCursor(cur); + }else if(code.endsWith("E")){ //Move Next/down Lines (go toward end) + int num = 1; + if(code.size()>2){ num = code.mid(1, code.size()-2).toInt(); } //everything in the middle + QTextCursor cur = this->textCursor(); + cur.movePosition(QTextCursor::NextRow, QTextCursor::MoveAnchor, num); + this->setTextCursor(cur); + }else if(code.endsWith("F")){ //Move Previous/up Lines (go to beginning) + int num = 1; + if(code.size()>2){ num = code.mid(1, code.size()-2).toInt(); } //everything in the middle + QTextCursor cur = this->textCursor(); + cur.movePosition(QTextCursor::PreviousRow, QTextCursor::MoveAnchor, num); + this->setTextCursor(cur); + }else if(code.endsWith("G")){ //Move to specific column + int num = 1; + if(code.size()>2){ num = code.mid(1, code.size()-2).toInt(); } //everything in the middle + QTextCursor cur = this->textCursor(); + cur.setPosition(num); + this->setTextCursor(cur); + }else if(code.endsWith("H") || code.endsWith("f") ){ //Move to specific position (row/column) + int mid = code.indexOf(";"); + if(mid>1){ + int numR, numC; numR = numC = 1; + if(mid >=2){ numR = code.mid(1,mid-1).toInt(); } + if(mid < code.size()-1){ numC = code.mid(mid+1,code.size()-mid-2).toInt(); } + + if(startrow>=0 && endrow>=0){ + if(numR == startrow){ numR = 0;} + else if(numR==endrow){ numR = this->document()->lineCount()-1; } + } + qDebug() << "Set Text Position (absolute):" << "Code:" << code << "Row:" << numR << "Col:" << numC; + //qDebug() << " - Current Pos:" << this->textCursor().position() << "Line Count:" << this->document()->lineCount(); + //if(!this->textCursor().movePosition(QTextCursor::Start, QTextCursor::MoveAnchor,1) ){ qDebug() << "Could not go to start"; } + QTextCursor cur(this->textCursor()); + cur.setPosition(QTextCursor::Start, QTextCursor::MoveAnchor); //go to start of document + //qDebug() << " - Pos After Start Move:" << cur.position(); + if( !cur.movePosition(QTextCursor::Down, QTextCursor::MoveAnchor, numR) ){ qDebug() << "Could not go to row:" << numR; } + //qDebug() << " - Pos After Down Move:" << cur.position(); + if( !cur.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, numC) ){ qDebug() << "Could not go to col:" << numC; } + /*this->textCursor().setPosition( this->document()->findBlockByLineNumber(numR).position() ); + qDebug() << " - Pos After Row Move:" << this->textCursor().position(); + if( !this->textCursor().movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, numC) ){ qDebug() << "Could not go to col:" << numC; }*/ + //qDebug() << " - Ending Pos:" << cur.position(); + this->setTextCursor(cur); + }else{ + //Go to home position + this->moveCursor(QTextCursor::Start); + } + + // CURSOR MANAGEMENT + }else if(code.endsWith("r")){ //Tag top/bottom lines as perticular numbers + int mid = code.indexOf(";"); + qDebug() << "New Row Codes:" << code << "midpoint:" << mid; + if(mid>1){ + if(mid >=2){ startrow = code.mid(1,mid-1).toInt(); } + if(mid < code.size()-1){ endrow = code.mid(mid+1,code.size()-mid-2).toInt(); } + } + qDebug() << "New Row Codes:" << startrow << endrow; + // DISPLAY CLEAR CODES + }else if(code.endsWith("J")){ //ED - Erase Display + int num = 0; + if(code.size()>2){ num = code.mid(1, code.size()-2).toInt(); } //everything in the middle + //qDebug() << "Erase Display:" << num; + if(num==1){ + //Clear from cursor to beginning of screen + QTextCursor cur = this->textCursor(); + cur.movePosition(QTextCursor::Start, QTextCursor::KeepAnchor, 1); + cur.removeSelectedText(); + this->setTextCursor(cur); + }else if(num==2){ + //Clear the whole screen + qDebug() << "Clear Screen:" << this->document()->lineCount(); + this->clear(); + }else{ + //Clear from cursor to end of screen + QTextCursor cur = this->textCursor(); + cur.movePosition(QTextCursor::End, QTextCursor::KeepAnchor, 1); + cur.removeSelectedText(); + this->setTextCursor(cur); + } + }else if(code.endsWith("K")){ //EL - Erase in Line + int num = 0; + if(code.size()>2){ num = code.mid(1, code.size()-2).toInt(); } //everything in the middle + //qDebug() << "Erase Number" << num; + //Now determine what should be cleared based on code + if(num==1){ + //Clear from current cursor to beginning of line + QTextCursor cur = this->textCursor(); + cur.movePosition(QTextCursor::StartOfLine, QTextCursor::KeepAnchor, 1); + cur.removeSelectedText(); + this->setTextCursor(cur); + }else if(num==2){ + //Clear the entire line + QTextCursor cur = this->textCursor(); + cur.movePosition(QTextCursor::StartOfLine, QTextCursor::MoveAnchor, 1); + cur.movePosition(QTextCursor::EndOfLine, QTextCursor::KeepAnchor, 1); + cur.removeSelectedText(); + this->setTextCursor(cur); + }else{ + //Clear from current cursor to end of line + QTextCursor cur = this->textCursor(); + cur.movePosition(QTextCursor::EndOfLine, QTextCursor::KeepAnchor, 1); + cur.removeSelectedText(); + this->setTextCursor(cur); + } + + //SCROLL MOVEMENT CODES + //}else if(code.endsWith("S")){ // SU - Scroll Up + //qDebug() << "Scroll Up:" << code; + //}else if(code.endsWith("T")){ // SD - Scroll Down + //qDebug() << "Scroll Down:" << code; + + // GRAPHICS RENDERING + }else if(code.endsWith("m")){ + //Format: "[<number>;<number>m" (no limit to sections separated by ";") + int start = 1; + int end = code.indexOf(";"); + while(end>start){ + applyANSIColor(code.mid(start, end-start).toInt()); + //Now update the iterators and try again + start = end; + end = code.indexOf(";",start+1); //go to the next one + } + //Need the last section as well + end = code.size()-1; + if(end>start){ applyANSIColor(code.mid(start, end-start).toInt());} + else{ applyANSIColor(0); } + + + // GRAPHICS MODES + //}else if(code.endsWith("h")){ + + //}else if(code.endsWith("l")){ + + }else{ + qDebug() << "Unhandled Control Code:" << code; + } + + } //End VT100 control codes + else{ + qDebug() << "Unhandled Control Code:" << code; + } +} + +void TerminalWidget::applyANSIColor(int code){ + //qDebug() << "Apply Color code:" << code; + if(code <=0){ CFMT = DEFFMT; } //Reset back to default + else if(code==1){ CFMT.setFontWeight(75); } //BOLD font + else if(code==2){ CFMT.setFontWeight(25); } //Faint font (smaller than normal by a bit) + else if(code==3){ CFMT.setFontWeight(75); } //Italic font + else if(code==4){ CFMT.setFontUnderline(true); } //Underline + //5-6: Blink text (unsupported) + //7: Reverse foreground/background (unsupported) + //8: Conceal (unsupported) + else if(code==9){ CFMT.setFontStrikeOut(true); } //Crossed out + //10-19: Change font family (unsupported) + //20: Fraktur Font (unsupported) + //21: Bold:off or Underline:Double (unsupported) + else if(code==22){ CFMT.setFontWeight(50); } //Normal weight + //23: Reset font (unsupported) + else if(code==24){ CFMT.setFontUnderline(false); } //disable underline + //25: Disable blinking (unsupported) + //26: Reserved + //27: Reset reversal (7) (unsupported) + //28: Reveal (cancel 8) (unsupported) + else if(code==29){ CFMT.setFontStrikeOut(false); } //Not Crossed out + else if(code>=30 && code<=39){ + //Set the font color + QColor color; + if(code==30){color=QColor(Qt::black); } + else if(code==31){ color=QColor(Qt::red); } + else if(code==32){ color=QColor(Qt::green); } + else if(code==33){ color=QColor(Qt::yellow); } + else if(code==34){ color=QColor(Qt::blue); } + else if(code==35){ color=QColor(Qt::magenta); } + else if(code==36){ color=QColor(Qt::cyan); } + else if(code==37){ color=QColor(Qt::white); } + //48: Special extended color setting (unsupported) + else if(code==39){ color= DEFFMT.foreground().color(); } //reset to default color +QBrush brush = CFMT.background(); + color.setAlpha(255); //fully opaque + brush.setColor(color); + CFMT.setForeground( brush ); + this->setTextColor(color); //just in case the format is not used + } + else if(code>=40 && code<=49){ + //Set the font color + QColor color; + if(code==40){color=QColor(Qt::black); } + else if(code==41){ color=QColor(Qt::red); } + else if(code==42){ color=QColor(Qt::green); } + else if(code==43){ color=QColor(Qt::yellow); } + else if(code==44){ color=QColor(Qt::blue); } + else if(code==45){ color=QColor(Qt::magenta); } + else if(code==46){ color=QColor(Qt::cyan); } + else if(code==47){ color=QColor(Qt::white); } + //48: Special extended color setting (unsupported) + else if(code==49){ color= DEFFMT.background().color(); } //reset to default color + QBrush brush = CFMT.background(); + color.setAlpha(255); //fully opaque + brush.setColor(color); + CFMT.setBackground( brush ); + } + //50: Reserved + //51: Framed + //52: Encircled + else if(code==53){ CFMT.setFontOverline(true); } //enable overline + //54: Not framed/circled (51/52) + else if(code==55){ CFMT.setFontOverline(false); } //disable overline + //56-59: Reserved + //60+: Not generally supported (special code for particular terminals such as aixterm) +} + +//Outgoing Data parsing +void TerminalWidget::sendKeyPress(int key){ + QByteArray ba; + //if(this->textCursor()==selCursor){ this->setTextCursor(lastCursor); } + //int fromEnd = this->document()->characterCount() - this->textCursor().position(); + //Check for special keys + switch(key){ + case Qt::Key_Delete: + ba.append("\x7F"); + break; + case Qt::Key_Backspace: + ba.append("\x08"); + break; + case Qt::Key_Left: + if(altkeypad){ ba.append("^[D"); } + else{ ba.append("\x1b[D"); } + break; + case Qt::Key_Right: + if(altkeypad){ ba.append("^[C"); } + else{ ba.append("\x1b[C"); } + break; + case Qt::Key_Up: + if(altkeypad){ ba.append("^[A"); } + else{ ba.append("\x1b[A"); } + break; + case Qt::Key_Down: + if(altkeypad){ ba.append("^[B"); } + else{ ba.append("\x1b[B"); } + break; + case Qt::Key_Home: + ba.append("\x1b[H"); + break; + case Qt::Key_End: + ba.append("\x1b[F"); + break; + } + qDebug() << "Forward Input:" << ba; + if(!ba.isEmpty()){ PROC->writeTTY(ba); } +} + +// ================== +// PRIVATE SLOTS +// ================== +void TerminalWidget::UpdateText(){ + //read the data from the process + //qDebug() << "UpdateText"; + if(!PROC->isOpen()){ return; } + applyData(PROC->readTTY()); + //adjust the scrollbar as needed + this->ensureCursorVisible(); + //this->verticalScrollBar()->setValue(this->verticalScrollBar()->maximum()); +} + +void TerminalWidget::ShellClosed(){ + emit ProcessClosed(this->whatsThis()); +} + +void TerminalWidget::copySelection(){ + QApplication::clipboard()->setText( selCursor.selectedText() ); +} + +void TerminalWidget::pasteSelection(){ + QString text = QApplication::clipboard()->text(); + if(!text.isEmpty()){ + QByteArray ba; ba.append(text); //avoid any byte conversions + PROC->writeTTY(ba); + } +} + +// ================== +// PROTECTED +// ================== +void TerminalWidget::keyPressEvent(QKeyEvent *ev){ + + if(ev->text().isEmpty() || ev->text()=="\b" ){ + sendKeyPress(ev->key()); + //PROC->writeTTY( QByteArray::fromHex(ev->nativeVirtualKey()) ); + }else{ + if( (ev->key()==Qt::Key_Enter || ev->key()==Qt::Key_Return) && !this->textCursor().atEnd() ){ + sendKeyPress(Qt::Key_End); //just in case the cursor is not at the end (TTY will split lines and such - ugly) + } + QByteArray ba; ba.append(ev->text()); //avoid any byte conversions + //qDebug() << "Forward Input:" << ba; + PROC->writeTTY(ba); + } + + ev->ignore(); +} + +void TerminalWidget::mousePressEvent(QMouseEvent *ev){ + this->setFocus(); + if(ev->button()==Qt::RightButton){ + QTextEdit::mousePressEvent(ev); + }else if(ev->button()==Qt::MiddleButton){ + pasteSelection(); + }else if(ev->button()==Qt::LeftButton){ + if(this->textCursor()!=selCursor){ lastCursor = this->textCursor(); } + selCursor = this->cursorForPosition(ev->pos()); + } + Q_UNUSED(ev); +} + +void TerminalWidget::mouseMoveEvent(QMouseEvent *ev){ + if(ev->button()==Qt::LeftButton){ + selCursor.setPosition(this->cursorForPosition(ev->pos()).position(), QTextCursor::KeepAnchor); + if(selCursor.hasSelection()){ this->setTextCursor(selCursor); } + }else{ + QTextEdit::mouseMoveEvent(ev); + } +} + +void TerminalWidget::mouseReleaseEvent(QMouseEvent *ev){ + if(ev->button()==Qt::LeftButton){ + selCursor.setPosition(this->cursorForPosition(ev->pos()).position(), QTextCursor::KeepAnchor); + if(selCursor.hasSelection()){ this->setTextCursor(selCursor); } + else{ this->setTextCursor(lastCursor); } + }else if(ev->button()==Qt::RightButton){ + copyA->setEnabled( selCursor.hasSelection() ); + pasteA->setEnabled( !QApplication::clipboard()->text().isEmpty() ); + contextMenu->popup( this->mapToGlobal(ev->pos()) ); + } + Q_UNUSED(ev); +} + +void TerminalWidget::mouseDoubleClickEvent(QMouseEvent *ev){ + Q_UNUSED(ev); +} + +void TerminalWidget::resizeEvent(QResizeEvent *ev){ + if(!PROC->isOpen()){ return; } + QSize pix = ev->size(); //pixels + QSize chars; + chars.setWidth( pix.width()/this->fontMetrics().width("W") ); + chars.setHeight( pix.height()/this->fontMetrics().lineSpacing() ); + + PROC->setTerminalSize(chars,pix); + QTextEdit::resizeEvent(ev); +} diff --git a/src-qt5/desktop-utils/lumina-terminal/TerminalWidget.h b/src-qt5/desktop-utils/lumina-terminal/TerminalWidget.h new file mode 100644 index 00000000..32fd55ad --- /dev/null +++ b/src-qt5/desktop-utils/lumina-terminal/TerminalWidget.h @@ -0,0 +1,68 @@ +//=========================================== +// Lumina-DE source code +// Copyright (c) 2015, Ken Moore +// Available under the 3-clause BSD license +// See the LICENSE file for full details +//=========================================== +#ifndef _LUMINA_DESKTOP_UTILITIES_TERMINAL_PROCESS_WIDGET_H +#define _LUMINA_DESKTOP_UTILITIES_TERMINAL_PROCESS_WIDGET_H + +#include <QTextEdit> +#include <QKeyEvent> +#include <QResizeEvent> +#include <QSocketNotifier> +#include <QTimer> +#include <QMenu> +#include <QClipboard> + +#include "TtyProcess.h" + +class TerminalWidget : public QTextEdit{ + Q_OBJECT +public: + TerminalWidget(QWidget *parent =0, QString dir=""); + ~TerminalWidget(); + + void aboutToClose(); + +private: + TTYProcess *PROC; + QTextCharFormat DEFFMT, CFMT; //default/current text format + QTextCursor selCursor, lastCursor; + QMenu *contextMenu; + QAction *copyA, *pasteA; + int selectionStart; + + //Incoming Data parsing + void InsertText(QString); + void applyData(QByteArray data); //overall data parsing + void applyANSI(QByteArray code); //individual code application + void applyANSIColor(int code); //Add the designated color code to the CFMT structure + + //Outgoing Data parsing + void sendKeyPress(int key); + + //Special incoming data flags + int startrow, endrow; //indexes for the first/last row ("\x1b[A;Br" CC) + bool altkeypad; +private slots: + void UpdateText(); + void ShellClosed(); + + void copySelection(); + void pasteSelection(); + +signals: + void ProcessClosed(QString); + +protected: + void keyPressEvent(QKeyEvent *ev); + void mousePressEvent(QMouseEvent *ev); + void mouseMoveEvent(QMouseEvent *ev); + void mouseReleaseEvent(QMouseEvent *ev); + void mouseDoubleClickEvent(QMouseEvent *ev); + //void contextMenuEvent(QContextMenuEvent *ev); + void resizeEvent(QResizeEvent *ev); +}; + +#endif diff --git a/src-qt5/desktop-utils/lumina-terminal/TrayIcon.cpp b/src-qt5/desktop-utils/lumina-terminal/TrayIcon.cpp new file mode 100644 index 00000000..ea970df9 --- /dev/null +++ b/src-qt5/desktop-utils/lumina-terminal/TrayIcon.cpp @@ -0,0 +1,172 @@ +//=========================================== +// Lumina-DE source code +// Copyright (c) 2015, Ken Moore +// Available under the 3-clause BSD license +// See the LICENSE file for full details +//=========================================== +#include "TrayIcon.h" + +#include <QDir> +#include <QDesktopWidget> + +#include <LuminaUtils.h> + +TrayIcon::TrayIcon() : QSystemTrayIcon(){ + //Create the child widgets here + settings = new QSettings("lumina-desktop","lumina-terminal"); + this->setContextMenu(new QMenu()); + ScreenMenu = new QMenu(); + connect(ScreenMenu, SIGNAL(triggered(QAction*)), this, SLOT(ChangeScreen(QAction*)) ); + TERM = new TermWindow(settings); + //Load the current settings + TERM->setTopOfScreen(settings->value("TopOfScreen",true).toBool()); + TERM->setCurrentScreen(settings->value("OnScreen",0).toInt()); + connect(TERM, SIGNAL(TerminalHidden()), this, SLOT(TermHidden())); + connect(TERM, SIGNAL(TerminalVisible()), this, SLOT(TermVisible())); + connect(TERM, SIGNAL(TerminalClosed()), this, SLOT(startCleanup())); + connect(TERM, SIGNAL(TerminalFinished()), this, SLOT(stopApplication())); + connect(this, SIGNAL(activated(QSystemTrayIcon::ActivationReason)), this, SLOT(TrayActivated(QSystemTrayIcon::ActivationReason)) ); +} + +TrayIcon::~TrayIcon(){ + delete TERM; + delete ScreenMenu; +} + +// ============= +// PUBLIC +// ============= +void TrayIcon::parseInputs(QStringList inputs){ + //Note that this is only run on the primary process - otherwise inputs are sent to the slotSingleInstance() below + termVisible = !inputs.contains("-toggle"); //will automatically show the terminal on first run, even if "-toggle" is set + + setupContextMenu(); + updateIcons(); + inputs = adjustInputs(inputs); //will adjust termVisible as necessary + if(inputs.isEmpty()){ inputs << QDir::homePath(); } //always start up with one terminal minimum + TERM->OpenDirs(inputs); + if(termVisible){ QTimer::singleShot(0, TERM, SLOT(ShowWindow())); } +} + +// ================= +// PUBLIC SLOTS +// ================= +void TrayIcon::slotSingleInstance(QStringList inputs){ + //Note that this is only run for a secondary process forwarding its inputs + //qDebug() << "Single Instance Event:" << inputs << termVisible; + bool visible = termVisible; + inputs = adjustInputs(inputs); //will adjust termVisible as necessary + if(!inputs.isEmpty()){ TERM->OpenDirs(inputs); } + //Only adjust the window if there was a change in the visibility status + //qDebug() << "Set Visible:" << termVisible; + if(!visible && termVisible){ QTimer::singleShot(0, TERM, SLOT(ShowWindow())); } + else if(visible && !termVisible){ QTimer::singleShot(0, TERM, SLOT(HideWindow())); } +} + +void TrayIcon::updateIcons(){ + this->setIcon(LXDG::findIcon("utilities-terminal","")); +} + +// ================ +// PRIVATE +// ================ +QStringList TrayIcon::adjustInputs(QStringList inputs){ + bool hasHide = false; + //Look for the special CLI flags just for the tray icon and trim them out + for(int i=0; i<inputs.length(); i++){ + if(inputs[i]=="-toggle"){ hasHide = termVisible; inputs.removeAt(i); i--; } //toggle the visibility + else if(inputs[i]=="-show"){ hasHide = false; inputs.removeAt(i); i--; } //change the visibility + else if(inputs[i]=="-hide"){ hasHide = true; inputs.removeAt(i); i--; } //change the visibility + else{ + //Must be a directory - convert to an absolute path and check for existance + inputs[i] = LUtils::PathToAbsolute(inputs[i]); + QFileInfo info(inputs[i]); + if(!info.exists()){ + qDebug() << "Directory does not exist: " << inputs[i]; + inputs.removeAt(i); + i--; + }else if(!info.isDir()){ + //Must be some kind of file, open the parent directory + inputs[i] = inputs[i].section("/",0,-2); + } + } + } + termVisible = !hasHide; + return inputs; +} + +// ================ +// PRIVATE SLOTS +// ================ +void TrayIcon::startCleanup(){ + TERM->cleanup(); +} + +void TrayIcon::stopApplication(){ + QApplication::exit(0); +} + +void TrayIcon::ChangeTopBottom(bool ontop){ + TERM->setTopOfScreen(ontop); + settings->setValue("TopOfScreen",ontop); //save for later +} + +void TrayIcon::ChangeScreen(QAction *act){ + int screen = act->whatsThis().toInt(); + TERM->setCurrentScreen(screen); + settings->setValue("OnScreen",screen); + updateScreenMenu(); +} + +void TrayIcon::setupContextMenu(){ + this->contextMenu()->clear(); + this->contextMenu()->addAction(LXDG::findIcon("edit-select",""), tr("Trigger Terminal"), this, SLOT(ToggleVisibility()) ); + this->contextMenu()->addSeparator(); + QAction * act = this->contextMenu()->addAction(tr("Top of Screen"), this, SLOT(ChangeTopBottom(bool)) ); + act->setCheckable(true); + act->setChecked(settings->value("TopOfScreen",true).toBool() ); + this->contextMenu()->addMenu(ScreenMenu); + this->contextMenu()->addSeparator(); + this->contextMenu()->addAction(LXDG::findIcon("application-exit",""), tr("Close Terminal"), this, SLOT(stopApplication()) ); + updateScreenMenu(); +} + +void TrayIcon::updateScreenMenu(){ + ScreenMenu->clear(); + QDesktopWidget *desk = QApplication::desktop(); + int cscreen = settings->value("OnScreen",0).toInt(); + if(cscreen>=desk->screenCount()){ cscreen = desk->primaryScreen(); } + ScreenMenu->setTitle(tr("Move To Monitor")); + for(int i=0; i<desk->screenCount(); i++){ + if(i!=cscreen){ + QAction *act = new QAction( QString(tr("Monitor %1")).arg(QString::number(i+1)),ScreenMenu); + act->setWhatsThis(QString::number(i)); + ScreenMenu->addAction(act); + } + } + ScreenMenu->setVisible(!ScreenMenu->isEmpty()); +} + +void TrayIcon::TrayActivated(QSystemTrayIcon::ActivationReason reason){ + switch(reason){ + case QSystemTrayIcon::Context: + this->contextMenu()->popup(this->geometry().center()); + break; + default: + ToggleVisibility(); + } +} + +//Slots for the window visibility +void TrayIcon::ToggleVisibility(){ + if(termVisible){ QTimer::singleShot(0, TERM, SLOT(HideWindow())); } + else{ QTimer::singleShot(0, TERM, SLOT(ShowWindow())); } +} + +void TrayIcon::TermHidden(){ + termVisible = false; +} + +void TrayIcon::TermVisible(){ + termVisible = true; +}
\ No newline at end of file diff --git a/src-qt5/desktop-utils/lumina-terminal/TrayIcon.h b/src-qt5/desktop-utils/lumina-terminal/TrayIcon.h new file mode 100644 index 00000000..961aaa90 --- /dev/null +++ b/src-qt5/desktop-utils/lumina-terminal/TrayIcon.h @@ -0,0 +1,59 @@ +//=========================================== +// Lumina-DE source code +// Copyright (c) 2015, Ken Moore +// Available under the 3-clause BSD license +// See the LICENSE file for full details +//=========================================== +#ifndef _LUMINA_DESKTOP_UTILITIES_TERMINAL_TRAY_ICON_H +#define _LUMINA_DESKTOP_UTILITIES_TERMINAL_TRAY_ICON_H +// QT Includes +#include <QApplication> +#include <QSystemTrayIcon> +#include <QMenu> +#include <QTimer> +#include <QSettings> + +#include <LuminaXDG.h> + +#include "TermWindow.h" + +class TrayIcon : public QSystemTrayIcon { + Q_OBJECT + +public: + TrayIcon(); + ~TrayIcon(); + + //First run + void parseInputs(QStringList); //Note that this is only run on the primary process - otherwise it gets sent to the singleInstance slot below + +public slots: + void slotSingleInstance(QStringList inputs = QStringList()); + void updateIcons(); + +private: + bool termVisible; + TermWindow *TERM; + QMenu *ScreenMenu; + QStringList adjustInputs(QStringList); + QSettings *settings; +private slots: + //Action Buttons + void startCleanup(); + void stopApplication(); + void ChangeTopBottom(bool ontop); + void ChangeScreen(QAction*); + + //Tray Updates + void setupContextMenu(); + void updateScreenMenu(); + void TrayActivated(QSystemTrayIcon::ActivationReason); + + //Slots for the window visibility + void ToggleVisibility(); + void TermHidden(); + void TermVisible(); + +}; + +#endif diff --git a/src-qt5/desktop-utils/lumina-terminal/TtyProcess.cpp b/src-qt5/desktop-utils/lumina-terminal/TtyProcess.cpp new file mode 100644 index 00000000..c5844255 --- /dev/null +++ b/src-qt5/desktop-utils/lumina-terminal/TtyProcess.cpp @@ -0,0 +1,229 @@ +#include "TtyProcess.h" + +#include <QDir> +#include <QProcessEnvironment> + +TTYProcess::TTYProcess(QObject *parent) : QObject(parent){ + childProc = 0; + sn = 0; + ttyfd = 0; +} + +TTYProcess::~TTYProcess(){ + closeTTY(); //make sure everything is closed properly +} + +// === PUBLIC === +bool TTYProcess::startTTY(QString prog, QStringList args, QString workdir){ + if(workdir=="~"){ workdir = QDir::homePath(); } + QDir::setCurrent(workdir); + QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + setenv("TERM","vt100",1); //VT100 emulation support + unsetenv("TERMCAP"); + /*setenv("TERMCAP","mvterm|vv100|mvterm emulator with ANSI colors:\ + :pa#64:Co#8:AF=\E[3%dm:AB=\E[4%dm:op=\E[100m:tc=vt102:",1); //see /etc/termcap as well*/ + QStringList filter = env.keys().filter("XTERM"); + for(int i=0; i<filter.length(); i++){ unsetenv(filter[i].toLocal8Bit().data()); } + //if(env.contains("TERM")){ unsetenv("TERM"); } + //else if(env.contains + //Turn the program/arguments into C-compatible arrays + char cprog[prog.length()]; strcpy(cprog, prog.toLocal8Bit().data()); + char *cargs[args.length()+2]; + QByteArray nullarray; + for(int i=0; i<args.length()+2; i++){ + // First arg needs to be the program + if ( i == 0 ) { + cargs[i] = new char[ prog.toLocal8Bit().size()+1]; + strcpy( cargs[i], prog.toLocal8Bit().data() ); + } else if(i<args.length()){ + cargs[i] = new char[ args[i].toLocal8Bit().size()+1]; + strcpy( cargs[i], args[i].toLocal8Bit().data() ); + }else{ + cargs[i] = NULL; + } + } + qDebug() << "PTY Start:" << prog; + //Launch the process attached to a new PTY + int FD = 0; + pid_t tmp = LaunchProcess(FD, cprog, cargs); + qDebug() << " - PID:" << tmp; + qDebug() << " - FD:" << FD; + if(tmp<0){ return false; } //error + else{ + childProc = tmp; + //Load the file for close notifications + //TO-DO + //Watch the socket for activity + sn= new QSocketNotifier(FD, QSocketNotifier::Read); + sn->setEnabled(true); + connect(sn, SIGNAL(activated(int)), this, SLOT(checkStatus(int)) ); + ttyfd = FD; + qDebug() << " - PTY:" << ptsname(FD); + return true; + } +} + +void TTYProcess::closeTTY(){ + int junk; + if(0==waitpid(childProc, &junk, WNOHANG)){ + kill(childProc, SIGKILL); + } + if(ttyfd!=0 && sn!=0){ + sn->setEnabled(false); + ::close(ttyfd); + ttyfd = 0; + emit processClosed(); + } +} + +void TTYProcess::writeTTY(QByteArray output){ + //qDebug() << "Write:" << output; + ::write(ttyfd, output.data(), output.size()); +} + +QByteArray TTYProcess::readTTY(){ + QByteArray BA; + //qDebug() << "Read TTY"; + if(sn==0){ return BA; } //not setup yet + char buffer[64]; + ssize_t rtot = read(sn->socket(),&buffer,64); + //buffer[rtot]='\0'; + BA = QByteArray(buffer, rtot); + //qDebug() << " - Got Data:" << BA; + if(!fragBA.isEmpty()){ + //Have a leftover fragment, include this too + BA = BA.prepend(fragBA); + fragBA.clear(); + } + bool bad = true; + BA = CleanANSI(BA, bad); + if(bad){ + //incomplete fragent - read some more first + fragBA = BA; + return readTTY(); + }else{ + //qDebug() << "Read Data:" << BA; + return BA; + } +} + +void TTYProcess::setTerminalSize(QSize chars, QSize pixels){ + if(ttyfd==0){ return; } + + struct winsize c_sz; + c_sz.ws_row = chars.height(); + c_sz.ws_col = chars.width(); + c_sz.ws_xpixel = pixels.width(); + c_sz.ws_ypixel = pixels.height(); + if( ioctl(ttyfd, TIOCSWINSZ, &ws) ){ + qDebug() << "Error settings terminal size"; + }else{ + //qDebug() <<"Set Terminal Size:" << pixels << chars; + } +} + +bool TTYProcess::isOpen(){ + return (ttyfd!=0); +} + +QByteArray TTYProcess::CleanANSI(QByteArray raw, bool &incomplete){ + incomplete = true; + //qDebug() << "Clean ANSI Data:" << raw; + //IN_LINE TERMINAL COLOR CODES (ANSI Escape Codes) "\x1B[<colorcode>m" + // - Just remove them for now + + //Special XTERM encoding (legacy support) + int index = raw.indexOf("\x1B]"); + while(index>=0){ + //The end character of this sequence is the Bell command ("\x07") + int end = raw.indexOf("\x07"); + if(end<0){ return raw; } //incomplete raw array + raw = raw.remove(index, end-index+1); + index = raw.indexOf("\x1B]"); + } + + // GENERIC ANSI CODES ((Make sure the output is not cut off in the middle of a code) + index = raw.indexOf("\x1B"); + while(index>=0){ + //CURSOR MOVEMENT + int end = 0; + for(int i=1; i<raw.size() && end==0; i++){ + if(raw.size() < index+i){ return raw; }//cut off - go back for more data + //qDebug() << "Check Char:" << raw.at(index+i); + if( QChar(raw.at(index+i)).isLetter() ){ + end = i; //found the end of the control code + } + } + index = raw.indexOf("\x1B",index+1); //now find the next one + } + + // SYSTEM BELL + index = raw.indexOf("\x07"); + while(index>=0){ + //qDebug() << "Remove Bell:" << index; + raw = raw.remove(index,1); + index = raw.indexOf("\x07"); + } + + incomplete = false; + return raw; +} + +// === PRIVATE === +pid_t TTYProcess::LaunchProcess(int& fd, char *prog, char **child_args){ + //Returns: -1 for errors, positive value (file descriptor) for the master side of the TTY to watch + + //First open/setup a new pseudo-terminal file/device on the system (master side) + fd = posix_openpt(O_RDWR | O_NOCTTY); //open read/write + if(fd<0){ return -1; } //could not create pseudo-terminal + int rc = grantpt(fd); //set permissions + if(rc!=0){ return -1; } + rc = unlockpt(fd); //unlock file (ready for use) + if(rc!=0){ return -1; } + //Now fork, return the Master device and setup the child + pid_t PID = fork(); + if(PID==0){ + //SLAVE/child + int fds = ::open(ptsname(fd), O_RDWR | O_NOCTTY); //open slave side read/write + ::close(fd); //close the master side from the slave thread + + //Adjust the slave side mode to RAW + struct termios TSET; + rc = tcgetattr(fds, &TSET); //read the current settings + cfmakesane(&TSET); //set the RAW mode on the settings ( cfmakeraw(&TSET); ) + tcsetattr(fds, TCSANOW, &TSET); //apply the changed settings + + //Change the controlling terminal in child thread to the slave PTY + ::close(0); //close current terminal standard input + ::close(1); //close current terminal standard output + ::close(2); //close current terminal standard error + dup(fds); // Set slave PTY as standard input (0); + dup(fds); // Set slave PTY as standard output (1); + dup(fds); // Set slave PTY as standard error (2); + + setsid(); //Make current process new session leader (so we can set controlling terminal) + ioctl(0,TIOCSCTTY, 1); //Set the controlling terminal to the slave PTY + + //Execute the designated program + rc = execvp(prog, child_args); + ::close(fds); //no need to keep original file descriptor open any more + exit(rc); + } + //MASTER thread (or error) + return PID; +} + +// === PRIVATE SLOTS === +void TTYProcess::checkStatus(int sock){ + //This is run when the socket gets activated + if(sock!=ttyfd){ + + } + //Make sure the child PID is still active + int junk; + if(0!=waitpid(childProc, &junk, WNOHANG)){ + this->closeTTY(); //clean up everything else + }else{ + emit readyRead(); + } +} diff --git a/src-qt5/desktop-utils/lumina-terminal/TtyProcess.h b/src-qt5/desktop-utils/lumina-terminal/TtyProcess.h new file mode 100644 index 00000000..9b3873b0 --- /dev/null +++ b/src-qt5/desktop-utils/lumina-terminal/TtyProcess.h @@ -0,0 +1,83 @@ +//=========================================== +// Lumina-DE source code +// Copyright (c) 2015, Ken Moore +// Available under the 3-clause BSD license +// See the LICENSE file for full details +//=========================================== +// This is basically a replacement for QProcess, where all process/terminal outputs +// are redirected and not just the standard input/output channels. This allows it +// to be used for terminal-like apps (shells) which directly modify the terminal output +// rather than stick to input/output channels for communication. +//=========================================== +// IMPLEMENTATION NOTE +//====================== +// The process requires/uses ANSI control codes (\x1B[<something>) for special operations +// such as moving the cursor, erasing characters, etc.. +// It is recommended that you pair this class with the graphical "TerminalWidget.h" class +// or some other ANSI-compatible display widget. +//=========================================== +#ifndef _LUMINA_DESKTOP_UTILITIES_TERMINAL_TTY_PROCESS_WIDGET_H +#define _LUMINA_DESKTOP_UTILITIES_TERMINAL_TTY_PROCESS_WIDGET_H + +#include <QDebug> +#include <QSocketNotifier> +#include <QKeyEvent> + +//Standard C library functions for PTY access/setup +#include <stdlib.h> +#include <fcntl.h> +#include <termios.h> +#include <unistd.h> +#include <sys/ioctl.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <signal.h> + +class TTYProcess : public QObject{ + Q_OBJECT +public: + TTYProcess(QObject *parent = 0); + ~TTYProcess(); + + bool startTTY(QString prog, QStringList args = QStringList(), QString workdir = "~"); + void closeTTY(); + + //Primary read/write functions + void writeTTY(QByteArray output); + QByteArray readTTY(); + + //Setup the terminal size (characters and pixels) + void setTerminalSize(QSize chars, QSize pixels); + + //Status update checks + bool isOpen(); + + //Functions for handling ANSI escape codes (typically not used by hand) + QByteArray CleanANSI(QByteArray, bool &incomplete); + +private: + pid_t childProc; + int ttyfd; + QSocketNotifier *sn; + QByteArray fragBA; //fragment ByteArray + + //==================================== + // C Library function for setting up the PTY + // Inputs: + // int &fd: (output) file descriptor for the master PTY (positive integer if valid) + // char *prog: program to run + // char **args: program arguments + // Returns: + // -1 for errors, child process PID (positive integer) if successful + //==================================== + static pid_t LaunchProcess(int& fd, char *prog, char **child_args); + +private slots: + void checkStatus(int); + +signals: + void readyRead(); + void processClosed(); +}; + +#endif diff --git a/src-qt5/desktop-utils/lumina-terminal/lumina-terminal.pro b/src-qt5/desktop-utils/lumina-terminal/lumina-terminal.pro new file mode 100644 index 00000000..104ff33f --- /dev/null +++ b/src-qt5/desktop-utils/lumina-terminal/lumina-terminal.pro @@ -0,0 +1,96 @@ +include("$${PWD}/../../OS-detect.pri") + +QT += core gui widgets network + +TARGET = lumina-terminal +target.path = $${L_BINDIR} + +HEADERS += TrayIcon.h \ + TermWindow.h \ + TerminalWidget.h \ + TtyProcess.h + +SOURCES += main.cpp \ + TrayIcon.cpp \ + TermWindow.cpp \ + TerminalWidget.cpp \ + TtyProcess.cpp + + +LIBS += -lLuminaUtils + + +DEPENDPATH += ../../libLumina + +TRANSLATIONS = i18n/lumina-terminal_af.ts \ + i18n/lumina-terminal_ar.ts \ + i18n/lumina-terminal_az.ts \ + i18n/lumina-terminal_bg.ts \ + i18n/lumina-terminal_bn.ts \ + i18n/lumina-terminal_bs.ts \ + i18n/lumina-terminal_ca.ts \ + i18n/lumina-terminal_cs.ts \ + i18n/lumina-terminal_cy.ts \ + i18n/lumina-terminal_da.ts \ + i18n/lumina-terminal_de.ts \ + i18n/lumina-terminal_el.ts \ + i18n/lumina-terminal_en_GB.ts \ + i18n/lumina-terminal_en_ZA.ts \ + i18n/lumina-terminal_es.ts \ + i18n/lumina-terminal_et.ts \ + i18n/lumina-terminal_eu.ts \ + i18n/lumina-terminal_fa.ts \ + i18n/lumina-terminal_fi.ts \ + i18n/lumina-terminal_fr.ts \ + i18n/lumina-terminal_fr_CA.ts \ + i18n/lumina-terminal_gl.ts \ + i18n/lumina-terminal_he.ts \ + i18n/lumina-terminal_hi.ts \ + i18n/lumina-terminal_hr.ts \ + i18n/lumina-terminal_hu.ts \ + i18n/lumina-terminal_id.ts \ + i18n/lumina-terminal_is.ts \ + i18n/lumina-terminal_it.ts \ + i18n/lumina-terminal_ja.ts \ + i18n/lumina-terminal_ka.ts \ + i18n/lumina-terminal_ko.ts \ + i18n/lumina-terminal_lt.ts \ + i18n/lumina-terminal_lv.ts \ + i18n/lumina-terminal_mk.ts \ + i18n/lumina-terminal_mn.ts \ + i18n/lumina-terminal_ms.ts \ + i18n/lumina-terminal_mt.ts \ + i18n/lumina-terminal_nb.ts \ + i18n/lumina-terminal_nl.ts \ + i18n/lumina-terminal_pa.ts \ + i18n/lumina-terminal_pl.ts \ + i18n/lumina-terminal_pt.ts \ + i18n/lumina-terminal_pt_BR.ts \ + i18n/lumina-terminal_ro.ts \ + i18n/lumina-terminal_ru.ts \ + i18n/lumina-terminal_sk.ts \ + i18n/lumina-terminal_sl.ts \ + i18n/lumina-terminal_sr.ts \ + i18n/lumina-terminal_sv.ts \ + i18n/lumina-terminal_sw.ts \ + i18n/lumina-terminal_ta.ts \ + i18n/lumina-terminal_tg.ts \ + i18n/lumina-terminal_th.ts \ + i18n/lumina-terminal_tr.ts \ + i18n/lumina-terminal_uk.ts \ + i18n/lumina-terminal_uz.ts \ + i18n/lumina-terminal_vi.ts \ + i18n/lumina-terminal_zh_CN.ts \ + i18n/lumina-terminal_zh_HK.ts \ + i18n/lumina-terminal_zh_TW.ts \ + i18n/lumina-terminal_zu.ts + +dotrans.path=$${L_SHAREDIR}/Lumina-DE/i18n/ +dotrans.extra=cd i18n && $${LRELEASE} -nounfinished *.ts && cp *.qm $(INSTALL_ROOT)$${L_SHAREDIR}/Lumina-DE/i18n/ + +INSTALLS += target dotrans + +NO_I18N{ + INSTALLS -= dotrans +} + diff --git a/src-qt5/desktop-utils/lumina-terminal/main.cpp b/src-qt5/desktop-utils/lumina-terminal/main.cpp new file mode 100644 index 00000000..896f7765 --- /dev/null +++ b/src-qt5/desktop-utils/lumina-terminal/main.cpp @@ -0,0 +1,47 @@ +//=========================================== +// Lumina-DE source code +// Copyright (c) 2015, Ken Moore +// Available under the 3-clause BSD license +// See the LICENSE file for full details +//=========================================== +#include <QSystemTrayIcon> +#include <QDebug> + +#include <LuminaSingleApplication.h> +#include <LuminaThemes.h> + +#include <unistd.h> + +#include "TrayIcon.h" +int main(int argc, char *argv[]) { + LTHEME::LoadCustomEnvSettings(); + LSingleApplication a(argc, argv, "lumina-terminal"); + if( !a.isPrimaryProcess() ){ return 0; } //poked the current process instead + + //First make sure a system tray is available + /*qDebug() << "Checking for system tray"; + bool ready = false; + for(int i=0; i<60 && !ready; i++){ + ready = QSystemTrayIcon::isSystemTrayAvailable(); + if(!ready){ + //Pause for 5 seconds + sleep(5); //don't worry about stopping event handling - nothing running yet + } + } + if(!ready){ + qDebug() << "Could not find any available system tray after 5 minutes: exiting...."; + return 1; + }*/ + + //Now go ahead and setup the app + LuminaThemeEngine theme(&a); + QApplication::setQuitOnLastWindowClosed(false); + + //Now start the tray icon + TrayIcon tray; + QObject::connect(&a, SIGNAL(InputsAvailable(QStringList)), &tray, SLOT(slotSingleInstance(QStringList)) ); + QObject::connect(&theme, SIGNAL(updateIcons()), &tray, SLOT(updateIcons()) ); + tray.parseInputs(a.inputlist); + tray.show(); + return a.exec(); +} |