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 @@
+
+
+
+
+
+
+
+
+
+
+ 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();
+}