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