1 #include "terminal.hpp"
2 #include "config.hpp"
3 #include "filesystem.hpp"
4 #include "info.hpp"
5 #include "notebook.hpp"
6 #include "project.hpp"
7 #include "utility.hpp"
8 #include <future>
9 #include <iostream>
10 #include <regex>
11 #include <thread>
12
Terminal()13 Terminal::Terminal() : Source::CommonView() {
14 get_style_context()->add_class("juci_terminal");
15
16 set_editable(false);
17
18 bold_tag = get_buffer()->create_tag();
19 bold_tag->property_weight() = Pango::WEIGHT_ULTRAHEAVY;
20
21 red_tag = get_buffer()->create_tag();
22 green_tag = get_buffer()->create_tag();
23 yellow_tag = get_buffer()->create_tag();
24 blue_tag = get_buffer()->create_tag();
25 magenta_tag = get_buffer()->create_tag();
26 cyan_tag = get_buffer()->create_tag();
27 gray_tag = get_buffer()->create_tag();
28
29 link_tag = get_buffer()->create_tag();
30 link_tag->property_underline() = Pango::Underline::UNDERLINE_SINGLE;
31
32 invisible_tag = get_buffer()->create_tag();
33 invisible_tag->property_invisible() = true;
34
35 link_mouse_cursor = Gdk::Cursor::create(Gdk::CursorType::HAND1);
36 default_mouse_cursor = Gdk::Cursor::create(Gdk::CursorType::XTERM);
37
38 class DetectPossibleLink {
39 bool delimiter_found = false, dot_found = false;
40
41 public:
42 bool operator()(char chr) {
43 if(chr == '\n') {
44 auto all_found = delimiter_found && dot_found;
45 delimiter_found = dot_found = false;
46 return all_found;
47 }
48 else if(chr == '/' || chr == '\\')
49 delimiter_found = true;
50 else if(chr == '.')
51 dot_found = true;
52 return false;
53 }
54 };
55 class ParseAnsiEscapeSequence {
56 enum class State {
57 none = 1,
58 escaped,
59 parameter_bytes,
60 intermediate_bytes
61 };
62 State state = State::none;
63 std::string parameters;
64 size_t length = 0;
65
66 public:
67 struct Sequence {
68 std::string arguments;
69 size_t length;
70 char command;
71 };
72
73 boost::optional<Sequence> operator()(char chr) {
74 if(chr == '\e') {
75 state = State::escaped;
76 parameters = {};
77 length = 1;
78 }
79 else if(state != State::none) {
80 ++length;
81 if(chr == '[') {
82 if(state == State::escaped)
83 state = State::parameter_bytes;
84 else
85 state = State::none;
86 }
87 else if(chr >= 0x30 && chr <= 0x3f) {
88 if(state == State::parameter_bytes)
89 parameters += chr;
90 else
91 state = State::none;
92 }
93 else if(chr >= 0x20 && chr <= 0x2f) {
94 if(state == State::parameter_bytes)
95 state = State::intermediate_bytes;
96 else if(state != State::intermediate_bytes)
97 state = State::none;
98 }
99 else if(chr >= 0x40 && chr <= 0x7e) {
100 if(state == State::parameter_bytes || state == State::intermediate_bytes) {
101 state = State::none;
102 return Sequence{std::move(parameters), length, chr};
103 }
104 else
105 state = State::none;
106 }
107 else
108 state = State::none;
109 }
110 return {};
111 }
112 };
113 get_buffer()->signal_insert().connect([this, detect_possible_link = DetectPossibleLink(), parse_ansi_escape_sequence = ParseAnsiEscapeSequence(), last_color = -1, last_color_sequence_mark = std::shared_ptr<Source::Mark>()](const Gtk::TextIter &iter, const Glib::ustring &text_, int /*bytes*/) mutable {
114 boost::optional<Gtk::TextIter> start_of_text;
115 int line_nr_offset = 0;
116 auto get_line_nr = [&] {
117 if(!start_of_text) {
118 start_of_text = iter;
119 start_of_text->backward_chars(text_.size());
120 }
121 return start_of_text->get_line() + line_nr_offset;
122 };
123 const auto &text = text_.raw();
124 for(size_t i = 0; i < text.size(); ++i) {
125 if(detect_possible_link(text[i])) {
126 auto start = get_buffer()->get_iter_at_line(get_line_nr());
127 auto end = start;
128 if(!end.ends_line())
129 end.forward_to_line_end();
130 if(auto link = find_link(get_buffer()->get_text(start, end, false).raw())) { // Apply link tags
131 auto link_start = start;
132 if(link_start.has_tag(invisible_tag))
133 link_start.forward_visible_cursor_position();
134 auto link_end = link_start;
135 link_start.forward_visible_cursor_positions(link->start_pos);
136 link_end.forward_visible_cursor_positions(link->end_pos);
137 get_buffer()->apply_tag(link_tag, link_start, link_end);
138 }
139 }
140 if(auto sequence = parse_ansi_escape_sequence(text[i])) {
141 auto end = iter;
142 end.backward_chars(utf8_character_count(text, i + 1));
143 auto start = end;
144 start.backward_chars(sequence->length);
145 get_buffer()->apply_tag(invisible_tag, start, end);
146 if(sequence->command == 'm') {
147 int color = -1;
148 if(sequence->arguments.empty())
149 color = 0;
150 else {
151 size_t pos = 0;
152 size_t start_pos = pos;
153 while(true) {
154 pos = sequence->arguments.find(";", pos);
155 try {
156 auto code = std::stoi(sequence->arguments.substr(start_pos, pos != std::string::npos ? pos - start_pos : pos));
157 if(code == 39)
158 color = 0;
159 else if(code == 38) {
160 color = 0;
161 break; // Do not read next arguments
162 }
163 else if(code == 48 || code == 58)
164 break; // Do not read next arguments
165 else if(code == 0 || code == 2 || code == 22 || (code >= 30 && code <= 37))
166 color = code;
167 }
168 catch(...) {
169 }
170 if(pos == std::string::npos)
171 break;
172 pos += 1;
173 start_pos = pos;
174 }
175 }
176 if(last_color >= 0) {
177 if(last_color == 31)
178 get_buffer()->apply_tag(red_tag, (*last_color_sequence_mark)->get_iter(), start);
179 else if(last_color == 32)
180 get_buffer()->apply_tag(green_tag, (*last_color_sequence_mark)->get_iter(), start);
181 else if(last_color == 33)
182 get_buffer()->apply_tag(yellow_tag, (*last_color_sequence_mark)->get_iter(), start);
183 else if(last_color == 34)
184 get_buffer()->apply_tag(blue_tag, (*last_color_sequence_mark)->get_iter(), start);
185 else if(last_color == 35)
186 get_buffer()->apply_tag(magenta_tag, (*last_color_sequence_mark)->get_iter(), start);
187 else if(last_color == 36)
188 get_buffer()->apply_tag(cyan_tag, (*last_color_sequence_mark)->get_iter(), start);
189 else if(last_color == 37 || last_color == 2)
190 get_buffer()->apply_tag(gray_tag, (*last_color_sequence_mark)->get_iter(), start);
191 }
192
193 if(color >= 0) {
194 last_color = color;
195 last_color_sequence_mark = std::make_shared<Source::Mark>(end);
196 }
197 }
198 }
199 if(text[i] == '\n')
200 ++line_nr_offset;
201 }
202 });
203 }
204
process(const std::string & command,const boost::filesystem::path & path,bool use_pipes)205 int Terminal::process(const std::string &command, const boost::filesystem::path &path, bool use_pipes) {
206 std::unique_ptr<TinyProcessLib::Process> process;
207 if(use_pipes)
208 process = std::make_unique<TinyProcessLib::Process>(
209 command, path.string(),
210 [this](const char *bytes, size_t n) {
211 async_print(std::string(bytes, n));
212 },
213 [this](const char *bytes, size_t n) {
214 async_print(std::string(bytes, n), true);
215 });
216 else
217 process = std::make_unique<TinyProcessLib::Process>(command, path.string());
218
219 if(process->get_id() <= 0) {
220 async_print("\e[31mError\e[m: failed to run command: " + command + "\n", true);
221 return -1;
222 }
223
224 return process->get_exit_status();
225 }
226
process(std::istream & stdin_stream,std::ostream & stdout_stream,const std::string & command,const boost::filesystem::path & path,std::ostream * stderr_stream)227 int Terminal::process(std::istream &stdin_stream, std::ostream &stdout_stream, const std::string &command, const boost::filesystem::path &path, std::ostream *stderr_stream) {
228 TinyProcessLib::Process process(
229 command, path.string(),
230 [&stdout_stream](const char *bytes, size_t n) {
231 Glib::ustring umessage(std::string(bytes, n));
232 Glib::ustring::iterator iter;
233 while(!umessage.validate(iter)) {
234 auto next_char_iter = iter;
235 next_char_iter++;
236 umessage.replace(iter, next_char_iter, "?");
237 }
238 stdout_stream.write(umessage.data(), n);
239 },
240 [this, stderr_stream](const char *bytes, size_t n) {
241 if(stderr_stream)
242 stderr_stream->write(bytes, n);
243 else
244 async_print(std::string(bytes, n), true);
245 },
246 true);
247
248 if(process.get_id() <= 0) {
249 async_print("\e[31mError\e[m: failed to run command: " + command + "\n", true);
250 return -1;
251 }
252
253 char buffer[131072];
254 for(;;) {
255 stdin_stream.readsome(buffer, 131072);
256 auto read_n = stdin_stream.gcount();
257 if(read_n == 0)
258 break;
259 if(!process.write(buffer, read_n)) {
260 break;
261 }
262 }
263 process.close_stdin();
264
265 return process.get_exit_status();
266 }
267
async_process(const std::string & command,const boost::filesystem::path & path,std::function<void (int exit_status)> callback,bool quiet)268 std::shared_ptr<TinyProcessLib::Process> Terminal::async_process(const std::string &command, const boost::filesystem::path &path, std::function<void(int exit_status)> callback, bool quiet) {
269 if(scroll_to_bottom)
270 scroll_to_bottom();
271 stdin_buffer.clear();
272
273 #if !defined(__APPLE__) && !defined(_WIN32)
274 static auto stdbuf = filesystem::find_executable("stdbuf").string();
275
276 std::string cd_path_and_command;
277 if(!path.empty()) {
278 auto path_escaped = path.string();
279 size_t pos = 0;
280 // Based on https://www.reddit.com/r/cpp/comments/3vpjqg/a_new_platform_independent_process_library_for_c11/cxsxyb7
281 while((pos = path_escaped.find('\'', pos)) != std::string::npos) {
282 path_escaped.replace(pos, 1, "'\\''");
283 pos += 4;
284 }
285 cd_path_and_command = "cd '" + path_escaped + "' && " + command; // To avoid resolving symbolic links
286 }
287 else
288 cd_path_and_command = command;
289 #endif
290
291 auto process = std::make_shared<TinyProcessLib::Process>(
292 #if defined(__APPLE__)
293 "STDBUF1=L " + command,
294 path.string(),
295 #elif defined(_WIN32)
296 command,
297 path.string(),
298 #else
299 !stdbuf.empty() ? std::vector<std::string>{stdbuf, "-oL", "/bin/sh", "-c", cd_path_and_command} : std::vector<std::string>{"/bin/sh", "-c", cd_path_and_command},
300 "",
301 #endif
302 [this, quiet](const char *bytes, size_t n) {
303 if(!quiet) {
304 // Print stdout message sequentially to avoid the GUI becoming unresponsive
305 std::promise<void> message_printed;
306 dispatcher.post([message = std::string(bytes, n), &message_printed]() mutable {
307 Terminal::get().print(std::move(message));
308 message_printed.set_value();
309 });
310 message_printed.get_future().get();
311 }
312 },
313 [this, quiet](const char *bytes, size_t n) {
314 if(!quiet) {
315 // Print stderr message sequentially to avoid the GUI becoming unresponsive
316 std::promise<void> message_printed;
317 dispatcher.post([message = std::string(bytes, n), &message_printed]() mutable {
318 Terminal::get().print(std::move(message), true);
319 message_printed.set_value();
320 });
321 message_printed.get_future().get();
322 }
323 },
324 true);
325
326 auto pid = process->get_id();
327 if(pid <= 0) {
328 async_print("\e[31mError\e[m: failed to run command: " + command + "\n", true);
329 if(callback)
330 callback(-1);
331 return process;
332 }
333 else {
334 LockGuard lock(processes_mutex);
335 processes.emplace_back(process);
336 }
337
338 std::thread([this, process, pid, callback = std::move(callback)]() mutable {
339 auto exit_status = process->get_exit_status();
340 {
341 LockGuard lock(processes_mutex);
342 for(auto it = processes.begin(); it != processes.end(); it++) {
343 if((*it)->get_id() == pid) {
344 processes.erase(it);
345 break;
346 }
347 }
348 }
349 if(callback) {
350 dispatcher.post([callback = std::move(callback), exit_status] {
351 callback(exit_status);
352 });
353 }
354 }).detach();
355
356 return process;
357 }
358
kill_last_async_process(bool force)359 void Terminal::kill_last_async_process(bool force) {
360 LockGuard lock(processes_mutex);
361 if(processes.empty())
362 Info::get().print("No running processes");
363 else
364 processes.back()->kill(force);
365 }
366
kill_async_processes(bool force)367 void Terminal::kill_async_processes(bool force) {
368 LockGuard lock(processes_mutex);
369 for(auto &process : processes)
370 process->kill(force);
371 }
372
on_motion_notify_event(GdkEventMotion * event)373 bool Terminal::on_motion_notify_event(GdkEventMotion *event) {
374 Gtk::TextIter iter;
375 int location_x, location_y;
376 window_to_buffer_coords(Gtk::TextWindowType::TEXT_WINDOW_TEXT, event->x, event->y, location_x, location_y);
377 get_iter_at_location(iter, location_x, location_y);
378 if(iter.has_tag(link_tag))
379 get_window(Gtk::TextWindowType::TEXT_WINDOW_TEXT)->set_cursor(link_mouse_cursor);
380 else
381 get_window(Gtk::TextWindowType::TEXT_WINDOW_TEXT)->set_cursor(default_mouse_cursor);
382
383 return Source::CommonView::on_motion_notify_event(event);
384 }
385
find_link(const std::string & line)386 boost::optional<Terminal::Link> Terminal::find_link(const std::string &line) {
387 if(line.size() >= 1000) // Due to https://gcc.gnu.org/bugzilla/show_bug.cgi?id=86164
388 return {};
389
390 const static std::regex link_regex("^([A-Z]:)?([^:]+):([0-9]+):([0-9]+): .*$|" // C/C++ compile warning/error/rename usages
391 "^In file included from ([A-Z]:)?([^:]+):([0-9]+)[:,]$|" // C/C++ extra compile warning/error info
392 "^ from ([A-Z]:)?([^:]+):([0-9]+)[:,]$|" // C/C++ extra compile warning/error info (gcc)
393 "^ +--> ([A-Z]:)?([^:]+):([0-9]+):([0-9]+)$|" // Rust
394 "^Assertion failed: .*file ([A-Z]:)?([^:]+), line ([0-9]+)\\.$|" // clang assert()
395 "^[^:]*: ([A-Z]:)?([^:]+):([0-9]+): .* Assertion .* failed\\.$|" // gcc assert()
396 "^ERROR:([A-Z]:)?([^:]+):([0-9]+):.*$|" // g_assert (glib.h)
397 "^([A-Z]:)?([\\\\/][^:]+):([0-9]+)$|" // Node.js
398 "^ +at .*?\\(([A-Z]:)?([^:]+):([0-9]+):([0-9]+)\\).*$|" // Node.js stack trace
399 "^ +at ([A-Z]:)?([^:]+):([0-9]+):([0-9]+).*$|" // Node.js stack trace
400 "^ File \"([A-Z]:)?([^\"]+)\", line ([0-9]+), in .*$|" // Python
401 "^.*?([A-Z]:)?([a-zA-Z0-9._\\\\/~][a-zA-Z0-9._\\-\\\\/]*):([0-9]+):([0-9]+).*$", // Posix path:line:column
402 std::regex::optimize);
403 std::smatch sm;
404 if(std::regex_match(line, sm, link_regex)) {
405 for(size_t sub = 1; sub < link_regex.mark_count();) {
406 size_t subs = (sub == 1 ||
407 sub == 1 + 4 + 3 + 3 ||
408 sub == 1 + 4 + 3 + 3 + 4 + 3 + 3 + 3 + 3 ||
409 sub == 1 + 4 + 3 + 3 + 4 + 3 + 3 + 3 + 3 + 4 ||
410 sub == 1 + 4 + 3 + 3 + 4 + 3 + 3 + 3 + 3 + 4 + 4 + 3)
411 ? 4
412 : 3;
413 if(sm.length(sub + 1)) {
414 auto start_pos = static_cast<int>(sm.position(sub + 1) - sm.length(sub));
415 auto end_pos = static_cast<int>(sm.position(sub + subs - 1) + sm.length(sub + subs - 1));
416 int start_pos_utf8 = utf8_character_count(line, 0, start_pos);
417 int end_pos_utf8 = start_pos_utf8 + utf8_character_count(line, start_pos, end_pos - start_pos);
418 std::string path;
419 if(sm.length(sub))
420 path = sm[sub].str();
421 path += sm[sub + 1].str();
422 try {
423 auto line_number = std::stoi(sm[sub + 2].str());
424 auto line_offset = std::stoi(subs == 4 ? sm[sub + 3].str() : "1");
425 return Link{start_pos_utf8, end_pos_utf8, path, line_number, line_offset};
426 }
427 catch(...) {
428 return {};
429 }
430 }
431 sub += subs;
432 }
433 }
434 const static std::regex uri_regex("^.*(https?://[\\w\\-.~:/?#%\\[\\]@!$&'()*+,;=]+[\\w\\-~/#@$*+;=]).*$", std::regex::optimize);
435 if(std::regex_match(line, sm, uri_regex)) {
436 auto start_pos = static_cast<int>(sm.position(1));
437 auto end_pos = static_cast<int>(start_pos + sm.length(1));
438 int start_pos_utf8 = utf8_character_count(line, 0, start_pos);
439 int end_pos_utf8 = start_pos_utf8 + utf8_character_count(line, start_pos, end_pos - start_pos);
440 return Link{start_pos_utf8, end_pos_utf8, sm[1].str(), 0, 0};
441 }
442 return {};
443 }
444
print(std::string message,bool bold)445 void Terminal::print(std::string message, bool bold) {
446 if(message.empty())
447 return;
448
449 if(auto parent = get_parent()) {
450 if(!parent->is_visible())
451 parent->show();
452 }
453
454 Glib::ustring umessage = std::move(message);
455
456 Glib::ustring::iterator iter;
457 while(!umessage.validate(iter)) {
458 auto next_char_iter = iter;
459 next_char_iter++;
460 umessage.replace(iter, next_char_iter, "?");
461 }
462
463 if(bold)
464 get_buffer()->insert_with_tag(get_buffer()->end(), umessage, bold_tag);
465 else
466 get_buffer()->insert(get_buffer()->end(), umessage);
467
468 auto excess_lines = get_buffer()->get_line_count() - Config::get().terminal.history_size;
469 if(excess_lines > 0)
470 get_buffer()->erase(get_buffer()->begin(), get_buffer()->get_iter_at_line(excess_lines));
471 }
472
async_print(std::string message,bool bold)473 void Terminal::async_print(std::string message, bool bold) {
474 dispatcher.post([message = std::move(message), bold]() mutable {
475 Terminal::get().print(std::move(message), bold);
476 });
477 }
478
configure()479 void Terminal::configure() {
480 link_tag->property_foreground_rgba() = get_style_context()->get_color(Gtk::StateFlags::STATE_FLAG_LINK);
481
482 auto normal_color = get_style_context()->get_color(Gtk::StateFlags::STATE_FLAG_NORMAL);
483 auto light_theme = (normal_color.get_red() + normal_color.get_green() + normal_color.get_blue()) / 3 < 0.5;
484
485 Gdk::RGBA rgba;
486 rgba.set_rgba(1.0, 0.0, 0.0);
487 double factor = light_theme ? 0.5 : 0.35;
488 rgba.set_red(normal_color.get_red() + factor * (rgba.get_red() - normal_color.get_red()));
489 rgba.set_green(normal_color.get_green() + factor * (rgba.get_green() - normal_color.get_green()));
490 rgba.set_blue(normal_color.get_blue() + factor * (rgba.get_blue() - normal_color.get_blue()));
491 red_tag->property_foreground_rgba() = rgba;
492
493 rgba.set_rgba(0.0, 1.0, 0.0);
494 factor = 0.4;
495 rgba.set_red(normal_color.get_red() + factor * (rgba.get_red() - normal_color.get_red()));
496 rgba.set_green(normal_color.get_green() + factor * (rgba.get_green() - normal_color.get_green()));
497 rgba.set_blue(normal_color.get_blue() + factor * (rgba.get_blue() - normal_color.get_blue()));
498 green_tag->property_foreground_rgba() = rgba;
499
500 rgba.set_rgba(1.0, 1.0, 0.2);
501 factor = 0.5;
502 rgba.set_red(normal_color.get_red() + factor * (rgba.get_red() - normal_color.get_red()));
503 rgba.set_green(normal_color.get_green() + factor * (rgba.get_green() - normal_color.get_green()));
504 rgba.set_blue(normal_color.get_blue() + factor * (rgba.get_blue() - normal_color.get_blue()));
505 yellow_tag->property_foreground_rgba() = rgba;
506
507 rgba.set_rgba(0.0, 0.0, 1.0);
508 factor = light_theme ? 0.8 : 0.2;
509 rgba.set_red(normal_color.get_red() + factor * (rgba.get_red() - normal_color.get_red()));
510 rgba.set_green(normal_color.get_green() + factor * (rgba.get_green() - normal_color.get_green()));
511 rgba.set_blue(normal_color.get_blue() + factor * (rgba.get_blue() - normal_color.get_blue()));
512 blue_tag->property_foreground_rgba() = rgba;
513
514 rgba.set_rgba(1.0, 0.0, 1.0);
515 factor = light_theme ? 0.45 : 0.25;
516 rgba.set_red(normal_color.get_red() + factor * (rgba.get_red() - normal_color.get_red()));
517 rgba.set_green(normal_color.get_green() + factor * (rgba.get_green() - normal_color.get_green()));
518 rgba.set_blue(normal_color.get_blue() + factor * (rgba.get_blue() - normal_color.get_blue()));
519 magenta_tag->property_foreground_rgba() = rgba;
520
521 rgba.set_rgba(0.0, 1.0, 1.0);
522 factor = light_theme ? 0.35 : 0.35;
523 rgba.set_red(normal_color.get_red() + factor * (rgba.get_red() - normal_color.get_red()));
524 rgba.set_green(normal_color.get_green() + factor * (rgba.get_green() - normal_color.get_green()));
525 rgba.set_blue(normal_color.get_blue() + factor * (rgba.get_blue() - normal_color.get_blue()));
526 cyan_tag->property_foreground_rgba() = rgba;
527
528 rgba.set_rgba(0.5, 0.5, 0.5);
529 factor = light_theme ? 0.6 : 0.4;
530 rgba.set_red(normal_color.get_red() + factor * (rgba.get_red() - normal_color.get_red()));
531 rgba.set_green(normal_color.get_green() + factor * (rgba.get_green() - normal_color.get_green()));
532 rgba.set_blue(normal_color.get_blue() + factor * (rgba.get_blue() - normal_color.get_blue()));
533 gray_tag->property_foreground_rgba() = rgba;
534
535 // Set search match style:
536 get_buffer()->get_tag_table()->foreach([](const Glib::RefPtr<Gtk::TextTag> &tag) {
537 if(tag->property_background_set()) {
538 auto scheme = Source::StyleSchemeManager::get_default()->get_scheme(Config::get().source.style);
539 if(scheme) {
540 auto style = scheme->get_style("search-match");
541 if(style) {
542 if(style->property_background_set())
543 tag->property_background() = style->property_background();
544 if(style->property_foreground_set())
545 tag->property_foreground() = style->property_foreground();
546 }
547 }
548 }
549 });
550 }
551
clear()552 void Terminal::clear() {
553 get_buffer()->set_text("");
554 }
555
on_button_press_event(GdkEventButton * button_event)556 bool Terminal::on_button_press_event(GdkEventButton *button_event) {
557 //open clicked link in terminal
558 if(button_event->type == GDK_BUTTON_PRESS && button_event->button == GDK_BUTTON_PRIMARY) {
559 Gtk::TextIter iter;
560 int location_x, location_y;
561 window_to_buffer_coords(Gtk::TextWindowType::TEXT_WINDOW_TEXT, button_event->x, button_event->y, location_x, location_y);
562 get_iter_at_location(iter, location_x, location_y);
563 if(iter.has_tag(link_tag)) {
564 auto start = get_buffer()->get_iter_at_line(iter.get_line());
565 auto end = start;
566 if(!end.ends_line())
567 end.forward_to_line_end();
568 if(auto link = find_link(get_buffer()->get_text(start, end, false).raw())) {
569 if(starts_with(link->path, "http://") || starts_with(link->path, "https://")) {
570 Notebook::get().open_uri(link->path);
571 return true;
572 }
573
574 auto path = filesystem::get_long_path(link->path);
575
576 if(path.is_relative()) {
577 auto project = Project::current;
578 if(!project)
579 project = Project::create();
580 if(project) {
581 boost::system::error_code ec;
582 if(boost::filesystem::exists(project->build->get_default_path() / path, ec))
583 path = project->build->get_default_path() / path;
584 else if(boost::filesystem::exists(project->build->get_debug_path() / path, ec))
585 path = project->build->get_debug_path() / path;
586 else if(boost::filesystem::exists(project->build->project_path / path, ec))
587 path = project->build->project_path / path;
588 else
589 return Gtk::TextView::on_button_press_event(button_event);
590 }
591 else
592 return Gtk::TextView::on_button_press_event(button_event);
593 }
594 boost::system::error_code ec;
595 if(boost::filesystem::is_regular_file(path, ec)) {
596 if(Notebook::get().open(path)) {
597 auto view = Notebook::get().get_current_view();
598 view->place_cursor_at_line_index(link->line - 1, link->line_index - 1);
599 view->scroll_to_cursor_delayed(true, true);
600 return true;
601 }
602 }
603 }
604 }
605 }
606 return Gtk::TextView::on_button_press_event(button_event);
607 }
608
on_key_press_event(GdkEventKey * event)609 bool Terminal::on_key_press_event(GdkEventKey *event) {
610 if(event->keyval == GDK_KEY_Home || event->keyval == GDK_KEY_End ||
611 event->keyval == GDK_KEY_Page_Up || event->keyval == GDK_KEY_Page_Down ||
612 event->keyval == GDK_KEY_Up || event->keyval == GDK_KEY_Down ||
613 event->keyval == GDK_KEY_Left || event->keyval == GDK_KEY_Right)
614 return Source::CommonView::on_key_press_event(event);
615
616 LockGuard lock(processes_mutex);
617 bool debug_is_running = false;
618 #ifdef JUCI_ENABLE_DEBUG
619 debug_is_running = Project::current ? Project::current->debug_is_running() : false;
620 #endif
621 if(processes.size() > 0 || debug_is_running) {
622 auto unicode = gdk_keyval_to_unicode(event->keyval);
623 if(unicode >= 32 && unicode != 126 && unicode != 0) {
624 if(scroll_to_bottom)
625 scroll_to_bottom();
626 get_buffer()->place_cursor(get_buffer()->end());
627 stdin_buffer += unicode;
628 get_buffer()->insert_at_cursor(Glib::ustring() + unicode);
629 }
630 else if(event->keyval == GDK_KEY_BackSpace) {
631 if(scroll_to_bottom)
632 scroll_to_bottom();
633 get_buffer()->place_cursor(get_buffer()->end());
634 if(stdin_buffer.size() > 0 && get_buffer()->get_char_count() > 0) {
635 auto iter = get_buffer()->end();
636 iter.backward_char();
637 stdin_buffer.erase(stdin_buffer.size() - 1);
638 get_buffer()->erase(iter, get_buffer()->end());
639 }
640 }
641 else if(event->keyval == GDK_KEY_Return || event->keyval == GDK_KEY_KP_Enter) {
642 if(scroll_to_bottom)
643 scroll_to_bottom();
644 get_buffer()->place_cursor(get_buffer()->end());
645 stdin_buffer += '\n';
646 get_buffer()->insert_at_cursor("\n");
647 if(debug_is_running) {
648 #ifdef JUCI_ENABLE_DEBUG
649 Project::current->debug_write(stdin_buffer.raw());
650 #endif
651 }
652 else
653 processes.back()->write(stdin_buffer.raw());
654 stdin_buffer.clear();
655 }
656 }
657 return true;
658 }
659
paste()660 void Terminal::paste() {
661 std::string text = Gtk::Clipboard::get()->wait_for_text();
662 if(text.empty())
663 return;
664
665 // Replace carriage returns (which leads to crash) with newlines
666 for(size_t c = 0; c < text.size(); c++) {
667 if(text[c] == '\r') {
668 if((c + 1) < text.size() && text[c + 1] == '\n')
669 text.replace(c, 2, "\n");
670 else
671 text.replace(c, 1, "\n");
672 }
673 }
674
675 std::string after_last_newline_str;
676 auto last_newline = text.rfind('\n');
677
678 LockGuard lock(processes_mutex);
679 bool debug_is_running = false;
680 #ifdef JUCI_ENABLE_DEBUG
681 debug_is_running = Project::current ? Project::current->debug_is_running() : false;
682 #endif
683 if(processes.size() > 0 || debug_is_running) {
684 if(scroll_to_bottom)
685 scroll_to_bottom();
686 get_buffer()->place_cursor(get_buffer()->end());
687 get_buffer()->insert_at_cursor(text);
688 if(last_newline != std::string::npos) {
689 if(debug_is_running) {
690 #ifdef JUCI_ENABLE_DEBUG
691 Project::current->debug_write(stdin_buffer.raw() + text.substr(0, last_newline + 1));
692 #endif
693 }
694 else
695 processes.back()->write(stdin_buffer.raw() + text.substr(0, last_newline + 1));
696 stdin_buffer = text.substr(last_newline + 1);
697 }
698 else
699 stdin_buffer += text;
700 }
701 }
702