1 #include "app.h"
2 
3 #include <QDir>
4 #include <QFileInfo>
5 #include <QFileOpenEvent>
6 #include <QSettings>
7 #include <QTextStream>
8 
9 #include "arguments.h"
10 #include "mainwindow.h"
11 #include "printinfo.h"
12 #include "version.h"
13 
14 namespace NeovimQt {
15 
16 static ShellOptions GetShellOptionsFromQSettings() noexcept;
17 
18 /// A log handler for Qt messages, all messages are dumped into the file
19 /// passed via the NVIM_QT_LOG variable. Some information is only available
20 /// in debug builds (e.g. qDebug is only called in debug builds).
21 ///
22 /// In UNIX Qt prints messages to the console output, but in Windows this is
23 /// the only way to get Qt's debug/warning messages.
logger(QtMsgType type,const QMessageLogContext & ctx,const QString & msg)24 void logger(QtMsgType type, const QMessageLogContext& ctx, const QString& msg)
25 {
26 	QFile logFile(qgetenv("NVIM_QT_LOG"));
27 	if (logFile.open(QIODevice::Append | QIODevice::Text)) {
28 		QTextStream stream(&logFile);
29 		stream << msg << "\n";
30 	}
31 }
32 
33 #ifdef Q_OS_MAC
getLoginEnvironment(const QString & path)34 bool getLoginEnvironment(const QString& path)
35 {
36 	QProcess proc;
37 	proc.start(path, {"-l", "-c", "env", "-i"});
38 	if (!proc.waitForFinished()) {
39 		qDebug() << "Failed to execute shell to get environemnt" << path;
40 		return false;
41 	}
42 
43 	QByteArray out = proc.readAllStandardOutput();
44 	foreach(const QByteArray& item, out.split('\n')) {
45 		int index = item.indexOf('=');
46 		if (index > 0) {
47 			qputenv(item.mid(0, index), item.mid(index+1));
48 			qDebug() << item.mid(0, index) << item.mid(index+1);
49 		}
50 	}
51 	return true;
52 }
53 #endif
54 
App(int & argc,char ** argv)55 App::App(int &argc, char ** argv) noexcept
56 :QApplication(argc, argv)
57 {
58 	setWindowIcon(QIcon(":/neovim.svg"));
59 	setApplicationDisplayName("Neovim");
60 
61 #ifdef Q_OS_MAC
62 	QByteArray shellPath = qgetenv("SHELL");
63 	if (!getLoginEnvironment(shellPath)) {
64 		getLoginEnvironment("/bin/bash");
65 	}
66 #endif
67 
68 	if (!qgetenv("NVIM_QT_LOG").isEmpty()) {
69 		qInstallMessageHandler(logger);
70 	}
71 
72 	QByteArray stylesheet_path = qgetenv("NVIM_QT_STYLESHEET");
73 	if (!stylesheet_path.isEmpty()) {
74 		QFile qssfile(stylesheet_path);
75 		if (qssfile.open(QIODevice::ReadOnly)) {
76 			setStyleSheet(qssfile.readAll());
77 		} else {
78 			qWarning("Unable to open stylesheet from $NVIM_QT_STYLESHEET");
79 		}
80 	}
81 
82 	processCommandlineOptions(m_parser, arguments());
83 }
84 
event(QEvent * event)85 bool App::event(QEvent *event) noexcept
86 {
87 	if( event->type()  == QEvent::FileOpen) {
88 		QFileOpenEvent * fileOpenEvent = static_cast<QFileOpenEvent *>(event);
89 		if(fileOpenEvent) {
90 			emit openFilesTriggered({fileOpenEvent->url()});
91 		}
92 	}
93 	return QApplication::event(event);
94 }
95 
showUi()96 void App::showUi() noexcept
97 {
98 	ShellOptions opts{ GetShellOptionsFromQSettings() };
99 
100 	if (m_parser.isSet("no-ext-tabline")) {
101 		opts.enable_ext_tabline = false;
102 	}
103 
104 	if (m_parser.isSet("no-ext-popupmenu")) {
105 		opts.enable_ext_popupmenu = false;
106 	}
107 
108 #ifdef NEOVIMQT_GUI_WIDGET
109 	NeovimQt::Shell *win = new NeovimQt::Shell(c);
110 	win->show();
111 	if (m_parser.isSet("fullscreen")) {
112 		win->showFullScreen();
113 	} else if (m_parser.isSet("maximized")) {
114 		win->showMaximized();
115 	} else {
116 		win->show();
117 	}
118 #else
119 	NeovimQt::MainWindow *win = new NeovimQt::MainWindow(m_connector.get(), opts);
120 
121 	QObject::connect(instance(), SIGNAL(openFilesTriggered(const QList<QUrl>)),
122 		win->shell(), SLOT(openFiles(const QList<QUrl>)));
123 
124 	// Window geometry should be restored only when the user does not specify
125 	// one of the following command line arguments. Argument "maximized" can
126 	// be safely ignored, as the loaded geometry only takes effect after the
127 	// user un-maximizes the window; this behavior is desirable. Function
128 	// `isSet` will issue qWarning() messages if the argument doesn't exist.
129 	// Do not call isSet(...) for arguments which may not exist.
130 	if (!m_parser.isSet("fullscreen") &&
131 		(!hasGeometryArg() || !m_parser.isSet("geometry")) &&
132 		(!hasQWindowGeometryArg() || !m_parser.isSet("qwindowgeometry")))
133 	{
134 		win->restoreWindowGeometry();
135 	}
136 
137 	if (m_parser.isSet("fullscreen")) {
138 		win->delayedShow(NeovimQt::MainWindow::DelayedShow::FullScreen);
139 	} else if (m_parser.isSet("maximized")) {
140 		win->delayedShow(NeovimQt::MainWindow::DelayedShow::Maximized);
141 	} else {
142 		win->delayedShow();
143 	}
144 #endif
145 }
146 
147 /// Initialize CLI parser with all the nvim-qt options, process the
148 /// provided arguments and check for errors.
149 ///
150 /// When appropriate this function will call QCommandLineParser::showHelp()
151 /// terminating the program.
processCommandlineOptions(QCommandLineParser & parser,QStringList arguments)152 void App::processCommandlineOptions(QCommandLineParser& parser, QStringList arguments) noexcept
153 {
154 	parser.addOption(QCommandLineOption("nvim",
155 				QCoreApplication::translate("main", "nvim executable path"),
156 				QCoreApplication::translate("main", "nvim_path"),
157 				"nvim"));
158 	parser.addOption(QCommandLineOption("timeout",
159 				QCoreApplication::translate("main", "Error if nvim does not responde after count milliseconds"),
160 				QCoreApplication::translate("main", "ms"),
161 				"20000"));
162 
163 	// Some platforms use --qwindowgeometry, while other platforms use the --geometry.
164 	// Make the correct help message is displayed.
165 	if (hasGeometryArg()) {
166 		parser.addOption(QCommandLineOption("geometry",
167 			QCoreApplication::translate("main", "Set initial window geometry"),
168 			QCoreApplication::translate("main", "width>x<height")));
169 	}
170 	if (hasQWindowGeometryArg()) {
171 		parser.addOption(QCommandLineOption("qwindowgeometry",
172 			QCoreApplication::translate("main", "Set initial window geometry"),
173 			QCoreApplication::translate("main", "width>x<height")));
174 	}
175 
176 	parser.addOption(QCommandLineOption("stylesheet",
177 				QCoreApplication::translate("main", "Apply qss stylesheet from file"),
178 				QCoreApplication::translate("main", "stylesheet")));
179 	parser.addOption(QCommandLineOption("maximized",
180 				QCoreApplication::translate("main", "Maximize the window on startup")));
181 	parser.addOption(QCommandLineOption("no-ext-tabline",
182 				QCoreApplication::translate("main", "Disable the external GUI tabline")));
183 	parser.addOption(QCommandLineOption("no-ext-popupmenu",
184 				QCoreApplication::translate("main", "Disable the external GUI popup menu")));
185 	parser.addOption(QCommandLineOption("fullscreen",
186 				QCoreApplication::translate("main", "Open the window in fullscreen on startup")));
187 	parser.addOption(QCommandLineOption("embed",
188 				QCoreApplication::translate("main", "Communicate with Neovim over stdin/out")));
189 	parser.addOption(QCommandLineOption("server",
190 				QCoreApplication::translate("main", "Connect to existing Neovim instance"),
191 				QCoreApplication::translate("main", "addr")));
192 	parser.addOption(QCommandLineOption("spawn",
193 				QCoreApplication::translate("main", "Treat positional arguments as the nvim argv")));
194 	parser.addOption(QCommandLineOption({ "v", "version" },
195 				QCoreApplication::translate("main", "Displays version information.")));
196 
197 	parser.addHelpOption();
198 
199 #ifdef Q_OS_UNIX
200 	parser.addOption(QCommandLineOption("nofork",
201 				QCoreApplication::translate("main", "Run in foreground")));
202 #endif
203 
204 	parser.addPositionalArgument("file",
205 			QCoreApplication::translate("main", "Edit specified file(s)"), "[file...]");
206 	parser.addPositionalArgument("...", "Additional arguments are forwarded to Neovim", "[-- ...]");
207 
208 	parser.process(arguments);
209 }
210 
checkArgumentsMayTerminate(QCommandLineParser & parser)211 void App::checkArgumentsMayTerminate(QCommandLineParser& parser) noexcept
212 {
213 	if (parser.isSet("help")) {
214 		parser.showHelp();
215 	}
216 
217 	if (parser.isSet("version")) {
218 		showVersionInfo(parser);
219 		::exit(0);
220 	}
221 
222 	int exclusive = parser.isSet("server") + parser.isSet("embed") + parser.isSet("spawn");
223 	if (exclusive > 1) {
224 		qWarning() << "Options --server, --spawn and --embed are mutually exclusive\n";
225 		::exit(-1);
226 	}
227 
228 	if (!parser.positionalArguments().isEmpty() &&
229 			(parser.isSet("embed") || parser.isSet("server"))) {
230 		qWarning() << "--embed and --server do not accept positional arguments\n";
231 		::exit(-1);
232 	}
233 
234 	if (parser.positionalArguments().isEmpty() && parser.isSet("spawn")) {
235 		qWarning() << "--spawn requires at least one positional argument\n";
236 		::exit(-1);
237 	}
238 
239 	bool valid_timeout;
240 	int timeout_opt = parser.value("timeout").toInt(&valid_timeout);
241 	if (!valid_timeout || timeout_opt <= 0) {
242 		qWarning() << "Invalid argument for --timeout" << parser.value("timeout");
243 		::exit(-1);
244 	}
245 }
246 
setupRequestTimeout()247 void App::setupRequestTimeout() noexcept
248 {
249 	if (!m_connector)
250 	{
251 		return;
252 	}
253 
254 	m_connector->setRequestTimeout(m_parser.value("timeout").toInt());
255 }
256 
getNeovimArgs()257 QStringList App::getNeovimArgs() noexcept
258 {
259 	QStringList neovimArgs{ "--cmd","set termguicolors" };
260 
261 	QString runtimePath{ getRuntimePath() };
262 	if (runtimePath.isEmpty()) {
263 		return { "--cmd","set termguicolors" };
264 	}
265 
266 	return { "--cmd", QString{ "let &rtp.=',%1'" }.arg(runtimePath),
267 		"--cmd","set termguicolors" };
268 }
269 
connectToRemoteNeovim()270 void App::connectToRemoteNeovim() noexcept
271 {
272 	if (m_parser.isSet("embed")) {
273 		m_connector = std::unique_ptr<NeovimConnector>{ NeovimQt::NeovimConnector::fromStdinOut() };
274 		setupRequestTimeout();
275 		return;
276 	}
277 
278 	if (m_parser.isSet("server")) {
279 		QString server = m_parser.value("server");
280 		m_connector = std::unique_ptr<NeovimConnector>{ NeovimQt::NeovimConnector::connectToNeovim(server) };
281 		setupRequestTimeout();
282 		return;
283 	}
284 
285 	if (m_parser.isSet("spawn") && !m_parser.positionalArguments().isEmpty()) {
286 		const QStringList& args = m_parser.positionalArguments();
287 		m_connector = std::unique_ptr<NeovimConnector> { NeovimQt::NeovimConnector::spawn(args.mid(1), args.at(0)) };
288 		setupRequestTimeout();
289 		return;
290 	}
291 
292 	QStringList neovimArgs{ getNeovimArgs() };
293 
294 	// Append positional file arguments to nvim.
295 	neovimArgs.append(m_parser.positionalArguments());
296 
297 	m_connector = std::unique_ptr<NeovimConnector>{  NeovimQt::NeovimConnector::spawn(neovimArgs, m_parser.value("nvim")) };
298 	setupRequestTimeout();
299 	return;
300 }
301 
GetShellOptionsFromQSettings()302 static ShellOptions GetShellOptionsFromQSettings() noexcept
303 {
304 	ShellOptions opts{};
305 	QSettings settings("nvim-qt", "nvim-qt");
306 
307 	QVariant ext_linegrid{ settings.value("ext_linegrid", opts.enable_ext_linegrid) };
308 	QVariant ext_popupmenu{ settings.value("ext_popupmenu", opts.enable_ext_popupmenu) };
309 	QVariant ext_tabline{ settings.value("ext_tabline", opts.enable_ext_popupmenu) };
310 
311 	if (ext_linegrid.canConvert<bool>())
312 	{
313 		opts.enable_ext_linegrid = ext_linegrid.toBool();
314 	}
315 
316 	if (ext_popupmenu.canConvert<bool>())
317 	{
318 		opts.enable_ext_popupmenu = ext_popupmenu.toBool();
319 	}
320 
321 	if (ext_tabline.canConvert<bool>())
322 	{
323 		opts.enable_ext_tabline= ext_tabline.toBool();
324 	}
325 
326 	return opts;
327 }
328 
GetNeovimVersionInfo(const QString & nvim)329 static QString GetNeovimVersionInfo(const QString& nvim) noexcept
330 {
331 	QProcess nvimproc;
332 	nvimproc.start(nvim, { "--version" });
333 	if (!nvimproc.waitForFinished(2000 /*msec*/)) {
334 		return QCoreApplication::translate("main", "Neovim Not Found!");
335 	}
336 
337 	return nvimproc.readAllStandardOutput();
338 }
339 
showVersionInfo(QCommandLineParser & parser)340 void App::showVersionInfo(QCommandLineParser& parser) noexcept
341 {
342 	QString versionInfo;
343 	QTextStream out{ &versionInfo };
344 
345 	const QString nvimExecutable { (parser.isSet("nvim")) ?
346 		parser.value("nvim") : "nvim" };
347 
348 	out << "NVIM-QT v" << PROJECT_VERSION << endl;
349 	out << "Build type: " << CMAKE_BUILD_TYPE << endl;
350 	out << "Compilation:" << CMAKE_CXX_FLAGS << endl;
351 	out << "Qt Version: " << QT_VERSION_STR << endl;
352 	out << "Environment: " << endl;
353 	out << "  nvim: " << nvimExecutable << endl;
354 	out << "  args: " << getNeovimArgs().join(" ") << endl;
355 	out << "  runtime: " << getRuntimePath() << endl;
356 
357 	out << endl;
358 
359 	out << GetNeovimVersionInfo(nvimExecutable) << endl;
360 
361 	PrintInfo(versionInfo);
362 }
363 
364 } // namespace NeovimQt
365