diff --git a/CMakeLists.txt b/CMakeLists.txt index 2c89d18e..120f0fd5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -21,3 +21,4 @@ macro_optional_add_subdirectory (kcachegrind) # macro_optional_add_subdirectory (kdepimlibs) # macro_optional_add_subdirectory (kdepim-runtime) # macro_optional_add_subdirectory (kdepim) +macro_optional_add_subdirectory (kman) diff --git a/kman/CMakeLists.txt b/kman/CMakeLists.txt new file mode 100644 index 00000000..2a3596b8 --- /dev/null +++ b/kman/CMakeLists.txt @@ -0,0 +1,20 @@ +project(kman) + +if(${CMAKE_SOURCE_DIR} STREQUAL ${CMAKE_CURRENT_SOURCE_DIR}) + find_package(KDE4 4.19.0 REQUIRED) + include(KDE4Defaults) + include_directories(${KDE4_INCLUDES}) + add_definitions(${QT_DEFINITIONS} ${KDE4_DEFINITIONS}) +endif() + +set(kman_sources + main.cpp + kmanmainwindow.cpp +) + +add_executable(kman ${kman_sources}) +target_link_libraries(kman ${KDE4_KDEUI_LIBS} ${KDE4_KFILE_LIBS}) + +install(TARGETS kman DESTINATION ${KDE4_BIN_INSTALL_DIR}) +install(FILES kmanui.rc DESTINATION ${KDE4_DATA_INSTALL_DIR}/kman) +install(PROGRAMS kman.desktop DESTINATION ${KDE4_XDG_APPS_INSTALL_DIR}) diff --git a/kman/kman.desktop b/kman/kman.desktop new file mode 100644 index 00000000..39c5ce88 --- /dev/null +++ b/kman/kman.desktop @@ -0,0 +1,10 @@ +[Desktop Entry] +Icon=help-browser +Name=KMan +GenericName=KMan +Comment=Simple manual page reader for KDE +Exec=kman --icon '%i' --caption '%c' %U +Terminal=false +Type=Application +Categories=Qt;KDE;System; +MimeType=text/troff; diff --git a/kman/kman.ui b/kman/kman.ui new file mode 100644 index 00000000..c86bc86c --- /dev/null +++ b/kman/kman.ui @@ -0,0 +1,87 @@ + + + KManWindow + + + + 0 + 0 + 545 + 508 + + + + KManPageApp + + + + + + + + + + + + + 0 + 0 + + + + + 250 + 0 + + + + Search in names.. + + + + + + + + 0 + 0 + + + + + + + + + + + + 0 + 0 + + + + + 250 + 0 + + + + QAbstractItemView::NoEditTriggers + + + + + + + 24 + + + + + + + + + + diff --git a/kman/kmanmainwindow.cpp b/kman/kmanmainwindow.cpp new file mode 100644 index 00000000..5e6f5022 --- /dev/null +++ b/kman/kmanmainwindow.cpp @@ -0,0 +1,402 @@ +/* This file is part of KMan + Copyright (C) 2018 Ivailo Monev + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License version 2, as published by the Free Software Foundation. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "kmanmainwindow.h" +#include "ui_kman.h" + +static QMap s_mans; +static QFileSystemWatcher s_watcher; + +class KManLister : public QThread { + Q_OBJECT + + public: + KManLister(QObject *parent = Q_NULLPTR); + + QList m_paths; + bool m_interrupt; + + public Q_SLOTS: + void slotScan(QString path); + + Q_SIGNALS: + void resultRead(const QString path, const QString fancy); + + protected: + // reimplementation + virtual void run(); +}; + +KManLister::KManLister(QObject *parent) + : QThread(parent), + m_interrupt(false) { + QByteArray manpath = qgetenv("MANPATH"); + if (manpath.isEmpty()) { + manpath = "/usr/local/share/man:/usr/share/man:/usr/local/man:/usr/man"; + } + foreach (const QByteArray &path, manpath.split(':')) { + QFileInfo pathinfo = QFileInfo(path); + if (!pathinfo.exists()) { + continue; + } + m_paths << path; + kDebug() << "watching" << path; + s_watcher.addPath(path); + } + + connect(&s_watcher, SIGNAL(directoryChanged(QString)), this, SLOT(slotScan(QString))); +} + +void KManLister::slotScan(QString path) { + kDebug() << "rescanning" << path; + if (isRunning()) { + m_interrupt = true; + wait(); + } + start(); +} + +void KManLister::run() { + setPriority(QThread::LowPriority); + + s_mans.clear(); + m_interrupt = false; + + foreach (const QByteArray &path, m_paths) { + kDebug() << "scanning" << path; + + QDirIterator iterator(path, QDirIterator::Subdirectories); + while (iterator.hasNext()) { + if (m_interrupt) { + return; + } + + const QString manpage = QDir::cleanPath(iterator.next()); + const QFileInfo info = iterator.fileInfo(); + if (info.isDir()) { + continue; + } + + const QString section = info.path().remove(0, path.size() + 1); + QString fancypage = section + "/" + info.fileName(); + const QString suffix = info.completeSuffix(); + if (!suffix.isEmpty()) { + fancypage = fancypage.left(fancypage.size() - suffix.size() - 1); + } + + s_mans.insert(manpage, fancypage); + emit resultRead(path, fancypage); + } + } +} + +KManMainWindow::KManMainWindow(QWidget *parent, Qt::WindowFlags flags) + : KXmlGuiWindow(parent, flags), m_kmanui(new Ui_KManWindow), m_lister(new KManLister(this)) +{ + m_kmanui->setupUi(this); + + m_actionopen = actionCollection()->addAction("file_open", this, SLOT(slotOpenAction())); + m_actionopen->setText(i18n("Open")); + m_actionopen->setIcon(KIcon("document-open")); + m_actionopen->setShortcut(KStandardShortcut::open()); + m_actionopen->setWhatsThis(i18n("Open manual page.")); + + m_actionquit = actionCollection()->addAction("file_quit", this, SLOT(slotQuitAction())); + m_actionquit->setText(i18n("Quit")); + m_actionquit->setIcon(KIcon("application-exit")); + m_actionquit->setShortcut(KStandardShortcut::quit()); + m_actionquit->setWhatsThis(i18n("Close the application.")); + + m_actionnext = actionCollection()->addAction("view_next", this, SLOT(slotNextAction())); + m_actionnext->setText(i18n("Next")); + m_actionnext->setIcon(KIcon("go-next")); + m_actionnext->setShortcut(KStandardShortcut::forward()); + m_actionnext->setWhatsThis(i18n("Switch to the next manual page in the list.")); + + m_actionprevious = actionCollection()->addAction("view_previous", this, SLOT(slotPreviousAction())); + m_actionprevious->setText(i18n("Previous")); + m_actionprevious->setIcon(KIcon("go-previous")); + m_actionprevious->setShortcut(KStandardShortcut::back()); + m_actionprevious->setWhatsThis(i18n("Switch to the previous manual page in the list.")); + + m_actionfind = actionCollection()->addAction("find_find", this, SLOT(slotFindAction())); + m_actionfind->setText(i18n("Find")); + m_actionfind->setIcon(KIcon("edit-find")); + m_actionfind->setShortcut(KStandardShortcut::find()); + m_actionfind->setWhatsThis(i18n("Find in the manual page.")); + + m_actionfindnext = actionCollection()->addAction("find_next", this, SLOT(slotFindNextAction())); + m_actionfindnext->setText(i18n("Find Next")); + m_actionfindnext->setIcon(KIcon("go-next")); + m_actionfindnext->setShortcut(KStandardShortcut::findNext()); + m_actionfindnext->setWhatsThis(i18n("Find next match in the manual page.")); + + m_actionfindprevious = actionCollection()->addAction("find_previous", this, SLOT(slotFindPreviousAction())); + m_actionfindprevious->setText(i18n("Find Previous")); + m_actionfindprevious->setIcon(KIcon("go-previous")); + m_actionfindprevious->setShortcut(KStandardShortcut::findPrev()); + m_actionfindprevious->setWhatsThis(i18n("Find previous match in the manual page.")); + + setupGUI(); + setAutoSaveSettings(); + + setWindowIcon(KIcon("help-browser")); + + connect(m_kmanui->searchEdit, SIGNAL(textChanged(const QString &)), + this, SLOT(slotSearchChanged(const QString &))); + connect(m_kmanui->listWidget, SIGNAL(currentTextChanged(const QString &)), + this, SLOT(slotListChanged(const QString &))); + + connect(m_lister, SIGNAL(started()), this, SLOT(slotBusyStart())); + connect(m_lister, SIGNAL(resultRead(const QString, const QString)), + this, SLOT(slotListResult(const QString, const QString))); + connect(m_lister, SIGNAL(finished()), this, SLOT(slotBusyFinish())); + + listManPages(); +} + +KManMainWindow::~KManMainWindow() +{ + saveAutoSaveSettings(); + m_lister->terminate(); + delete m_lister; + delete m_kmanui; +} + +void KManMainWindow::changePath(const QString path) { + if (path.isEmpty()) { + return; + } + + m_path = path; + + const QString content = manContent(path); + m_kmanui->manView->setHtml(content); + + statusBar()->showMessage(path); + + // TODO: m_kmanui->menuFind->setEnabled(!content.isEmpty()); + + const QString fancypage = s_mans.value(path); + if (!fancypage.isEmpty()) { + QList items = m_kmanui->listWidget->findItems(fancypage, Qt::MatchExactly); + if (items.count() > 0) { + m_kmanui->listWidget->setCurrentItem(items.first()); + } else { + QMessageBox::warning(this, windowTitle(), i18n("not in list %1", fancypage)); + } + } else { + kDebug() << "not in cache" << path; + } + + m_actionnext->setEnabled(false); + m_actionprevious->setEnabled(false); + const int position = m_kmanui->listWidget->currentRow(); + if ((position + 1) < m_kmanui->listWidget->count()) { + m_actionnext->setEnabled(true); + } + if (position >= 1) { + m_actionprevious->setEnabled(true); + } +} + +void KManMainWindow::slotOpenAction() { + const QString path = KFileDialog::getOpenFileName(KUrl(), QLatin1String("text/troff"), this, i18n("Manual page path")); + if (!path.isEmpty()) { + changePath(path); + } +} + +void KManMainWindow::slotQuitAction() { + qApp->quit(); +} + +void KManMainWindow::slotNextAction() { + const int position = m_kmanui->listWidget->currentRow(); + if ((position + 1) < m_kmanui->listWidget->count()) { + m_kmanui->listWidget->setCurrentRow(position + 1); + } +} + +void KManMainWindow::slotPreviousAction() { + const int position = m_kmanui->listWidget->currentRow(); + if (position >= 1) { + m_kmanui->listWidget->setCurrentRow(position - 1); + } +} + +void KManMainWindow::slotFindAction() { + const QString text = QInputDialog::getText(this, windowTitle(), i18n("Find:")); + if (text.isEmpty()) { + return; + } + m_search = text; + m_actionfindnext->setEnabled(true); + m_actionfindprevious->setEnabled(true); + + if (!m_kmanui->manView->find(text)) { + QMessageBox::information(this, windowTitle(), i18n("No match was found")); + } +} + +void KManMainWindow::slotFindNextAction() { + if (m_search.isEmpty()) { + return; + } + + if (!m_kmanui->manView->find(m_search)) { + QMessageBox::information(this, windowTitle(), i18n("No match was found")); + } +} + +void KManMainWindow::slotFindPreviousAction() { + if (m_search.isEmpty()) { + return; + } + + if (!m_kmanui->manView->find(m_search, QTextDocument::FindBackward)) { + QMessageBox::information(this, windowTitle(), i18n("No match was found")); + } +} + +void KManMainWindow::slotSearchChanged(const QString &text) { + m_kmanui->listWidget->clear(); + foreach (const QString &man, s_mans.values()) { + if (text.isEmpty() || man.indexOf(text) >= 0) { + QListWidgetItem* manitem = new QListWidgetItem(KIcon("application-x-troff-man"), man, m_kmanui->listWidget); + m_kmanui->listWidget->addItem(manitem); + } + } + m_kmanui->listWidget->sortItems(); +} + +void KManMainWindow::slotListChanged(const QString &text) { + const QString manpath = s_mans.key(text); + if (!manpath.isEmpty()) { + changePath(manpath); + } else { + kDebug() << "not in cache" << text; + } +} + +void KManMainWindow::slotListResult(const QString path, const QString fancy) { + QListWidgetItem* manitem = new QListWidgetItem(KIcon("application-x-troff-man"), fancy, m_kmanui->listWidget); + m_kmanui->listWidget->addItem(manitem); +} + +void KManMainWindow::slotBusyStart() { + // TODO: m_kmanui->menuFile->setEnabled(false); + // TODO: m_kmanui->menuAction->setEnabled(false); + // TODO: m_kmanui->menuFind->setEnabled(false); + m_kmanui->searchEdit->setEnabled(false); + m_kmanui->listWidget->setEnabled(false); + m_kmanui->progressBar->setRange(0, 0); + m_kmanui->progressBar->setVisible(true); +} + +void KManMainWindow::slotBusyFinish() { + m_kmanui->listWidget->sortItems(); + + + m_kmanui->progressBar->setRange(0, 1); + m_kmanui->progressBar->setVisible(false); + m_kmanui->listWidget->setEnabled(true); + m_kmanui->searchEdit->setEnabled(true); + // TODO: m_kmanui->menuAction->setEnabled(true); + // TODO: m_kmanui->menuFile->setEnabled(true); +} + +void KManMainWindow::listManPages() { + if (s_mans.isEmpty()) { + m_lister->start(); + } +} + +QString KManMainWindow::manContent(const QString path) { + QString result; + + if (QStandardPaths::findExecutable("groff").isEmpty()) { + QMessageBox::warning(this, windowTitle(), i18n("groff is not installed")); + return result; + } + + QFile pathfile(path); + if (pathfile.size() > 100000) { + QMessageBox::warning(this, windowTitle(), i18n("manual page has size greatar than 10Mb")); + return result; + } + + QByteArray content; + if (path.endsWith(".gz") || path.endsWith(".bz2") || path.endsWith(".xz")) { + QIODevice *archivedevice = KFilterDev::deviceForFile(path); + if (archivedevice && archivedevice->open(QFile::ReadOnly)) { + content = archivedevice->readAll(); + } + archivedevice->deleteLater(); + } else { + if (pathfile.open(QFile::ReadOnly)) { + content = pathfile.readAll(); + } + pathfile.close(); + } + + if (content.isEmpty()) { + QMessageBox::warning(this, windowTitle(), i18n("could not read %1", path)); + return result; + } + + slotBusyStart(); + + QProcess groff; + groff.setWorkingDirectory(QDir::tempPath()); + groff.start("groff", QStringList() << "-mandoc" << "-c" << "-Thtml"); + groff.waitForStarted(); + groff.write(content); + groff.closeWriteChannel(); + while (groff.state() == QProcess::Running) { + QCoreApplication::processEvents(); + } + if (groff.exitCode() != 0 ) { + QMessageBox::warning(this, windowTitle(), groff.readAllStandardError()); + } else { + result = groff.readAll(); + } + + slotBusyFinish(); + + return result; +} + +#include "kmanmainwindow.moc" diff --git a/kman/kmanmainwindow.h b/kman/kmanmainwindow.h new file mode 100644 index 00000000..5f93edde --- /dev/null +++ b/kman/kmanmainwindow.h @@ -0,0 +1,76 @@ +/* This file is part of KMan + Copyright (C) 2018 Ivailo Monev + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License version 2, as published by the Free Software Foundation. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#ifndef KMANWINDOW_H +#define KMANWINDOW_H + +#include +#include + +QT_BEGIN_NAMESPACE +class Ui_KManWindow; +QT_END_NAMESPACE + +class KManLister; + +class KManMainWindow: public KXmlGuiWindow +{ + Q_OBJECT +public: + KManMainWindow(QWidget *parent = 0, Qt::WindowFlags flags = 0); + ~KManMainWindow(); + + void changePath(const QString path); + +public slots: + void slotOpenAction(); + void slotQuitAction(); + void slotNextAction(); + void slotPreviousAction(); + void slotFindAction(); + void slotFindNextAction(); + void slotFindPreviousAction(); + void slotSearchChanged(const QString &text); + void slotListChanged(const QString &text); + +private slots: + void slotListResult(const QString path, const QString fancy); + void slotBusyStart(); + void slotBusyFinish(); + +private: + void listManPages(); + QString manContent(const QString path); + +private: + Ui_KManWindow *m_kmanui; + + KAction *m_actionopen; + KAction *m_actionquit; + KAction *m_actionnext; + KAction *m_actionprevious; + KAction *m_actionfind; + KAction *m_actionfindnext; + KAction *m_actionfindprevious; + + QString m_path; + QString m_search; + KManLister *m_lister; +}; + +#endif // KMANWINDOW_H \ No newline at end of file diff --git a/kman/kmanui.rc b/kman/kmanui.rc new file mode 100644 index 00000000..313a849f --- /dev/null +++ b/kman/kmanui.rc @@ -0,0 +1,25 @@ + + + + &File + + + + + + &View + + + + + &Find + + + + + + + + Main Toolbar + + diff --git a/kman/main.cpp b/kman/main.cpp new file mode 100644 index 00000000..22e20d31 --- /dev/null +++ b/kman/main.cpp @@ -0,0 +1,55 @@ +/* This file is part of KMan + Copyright (C) 2018 Ivailo Monev + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License version 2, as published by the Free Software Foundation. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include +#include +#include +#include +#include "kmanmainwindow.h" + +int main(int argc, char** argv) +{ + KAboutData aboutData("kman", 0, ki18n("KMan"), + "1.0.0", ki18n("Simple manual page reader for KDE."), + KAboutData::License_GPL_V2, + ki18n("(c) 2018 Ivailo Monev"), + KLocalizedString(), + "http://github.com/fluxer/katana" + ); + + aboutData.addAuthor(ki18n("Ivailo Monev"), + ki18n("Maintainer"), + "xakepa10@gmail.com"); + aboutData.setProgramIconName(QLatin1String("help-browser")); + + KCmdLineArgs::init(argc, argv, &aboutData); + KCmdLineOptions option; + option.add("+[url]", ki18n("URL to be opened")); + KCmdLineArgs::addCmdLineOptions(option); + + KUniqueApplication *kmanapp = new KUniqueApplication(); + KManMainWindow *kmanwin = new KManMainWindow(); + kmanwin->show(); + + KCmdLineArgs *args = KCmdLineArgs::parsedArgs(); + for (int pos = 0; pos < args->count(); ++pos) { + kmanwin->changePath(args->url(pos).prettyUrl()); + } + + return kmanapp->exec(); +}