Don't pass signed char values to ctype.h functions
[survex.git] / src / cavernlog.cc
blob69f135a71a3cae8c8e9df6c0864e89ae34b01ee9
1 /* cavernlog.cc
2 * Run cavern inside an Aven window
4 * Copyright (C) 2005-2022 Olly Betts
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, write to the Free Software
18 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
21 #ifdef HAVE_CONFIG_H
22 # include <config.h>
23 #endif
25 #include "aven.h"
26 #include "cavernlog.h"
27 #include "filename.h"
28 #include "mainfrm.h"
29 #include "message.h"
31 #include <errno.h>
32 #include <stdio.h>
33 #include <stdlib.h>
35 // For select():
36 #ifdef HAVE_SYS_SELECT_H
37 #include <sys/select.h>
38 #endif
39 #include <sys/time.h>
40 #include <sys/types.h>
41 #include <unistd.h>
43 #include <wx/process.h>
45 #define GVIM_COMMAND "gvim +'call cursor($l,$c)' $f"
46 #define VIM_COMMAND "x-terminal-emulator -e vim +'call cursor($l,$c)' $f"
47 #define NVIM_COMMAND "x-terminal-emulator -e nvim +'call cursor($l,$c)' $f"
48 #define GEDIT_COMMAND "gedit $f +$l:$c"
49 // Pluma currently ignores the column, but include it assuming some future
50 // version will add support.
51 #define PLUMA_COMMAND "pluma +$l:$c $f"
52 #define EMACS_COMMAND "x-terminal-emulator -e emacs +$l:$c $f"
53 #define NANO_COMMAND "x-terminal-emulator -e nano +$l,$c $f"
54 #define JED_COMMAND "x-terminal-emulator -e jed $f -g $l"
55 #define KATE_COMMAND "kate -l $l -c $c $f"
57 #ifdef __WXMSW__
58 # define DEFAULT_EDITOR_COMMAND "notepad $f"
59 #elif defined __WXMAC__
60 # define DEFAULT_EDITOR_COMMAND "open -t $f"
61 #else
62 # define DEFAULT_EDITOR_COMMAND VIM_COMMAND
63 #endif
65 enum { LOG_REPROCESS = 1234, LOG_SAVE = 1235 };
67 static const wxString badutf8_html(
68 wxT("<span style=\"color:white;background-color:red;\">&#xfffd;</span>"));
69 static const wxString badutf8(wxUniChar(0xfffd));
71 // New event type for passing a chunk of cavern output from the worker thread
72 // to the main thread (or from the idle event handler if we're not using
73 // threads).
74 class CavernOutputEvent;
76 wxDEFINE_EVENT(wxEVT_CAVERN_OUTPUT, CavernOutputEvent);
78 class CavernOutputEvent : public wxEvent {
79 public:
80 char buf[1000];
81 int len;
82 CavernOutputEvent() : wxEvent(0, wxEVT_CAVERN_OUTPUT), len(0) { }
84 wxEvent * Clone() const {
85 CavernOutputEvent * e = new CavernOutputEvent();
86 e->len = len;
87 if (len > 0) memcpy(e->buf, buf, len);
88 return e;
92 #ifdef CAVERNLOG_USE_THREADS
93 class CavernThread : public wxThread {
94 protected:
95 virtual ExitCode Entry();
97 CavernLogWindow *handler;
99 wxInputStream * in;
101 public:
102 CavernThread(CavernLogWindow *handler_, wxInputStream * in_)
103 : wxThread(wxTHREAD_DETACHED), handler(handler_), in(in_) { }
105 ~CavernThread() {
106 wxCriticalSectionLocker enter(handler->thread_lock);
107 handler->thread = NULL;
111 wxThread::ExitCode
112 CavernThread::Entry()
114 while (true) {
115 CavernOutputEvent * e = new CavernOutputEvent();
116 in->Read(e->buf, sizeof(e->buf));
117 size_t n = in->LastRead();
118 if (n == 0 || TestDestroy()) {
119 delete e;
120 return (wxThread::ExitCode)0;
122 if (n == 1 && e->buf[0] == '\n') {
123 // Don't send an event with just a blank line in.
124 in->Read(e->buf + 1, sizeof(e->buf) - 1);
125 n += in->LastRead();
126 if (TestDestroy()) {
127 delete e;
128 return (wxThread::ExitCode)0;
131 e->len = n;
132 handler->QueueEvent(e);
136 #else
138 void
139 CavernLogWindow::OnIdle(wxIdleEvent& event)
141 if (cavern_out == NULL) return;
143 wxInputStream * in = cavern_out->GetInputStream();
145 if (!in->CanRead()) {
146 // Avoid a tight busy-loop on idle events.
147 wxMilliSleep(10);
149 if (in->CanRead()) {
150 CavernOutputEvent * e = new CavernOutputEvent();
151 in->Read(e->buf, sizeof(e->buf));
152 size_t n = in->LastRead();
153 if (n == 0) {
154 delete e;
155 return;
157 e->len = n;
158 QueueEvent(e);
161 event.RequestMore();
163 #endif
165 BEGIN_EVENT_TABLE(CavernLogWindow, wxHtmlWindow)
166 EVT_BUTTON(LOG_REPROCESS, CavernLogWindow::OnReprocess)
167 EVT_BUTTON(LOG_SAVE, CavernLogWindow::OnSave)
168 EVT_BUTTON(wxID_OK, CavernLogWindow::OnOK)
169 EVT_COMMAND(wxID_ANY, wxEVT_CAVERN_OUTPUT, CavernLogWindow::OnCavernOutput)
170 #ifdef CAVERNLOG_USE_THREADS
171 EVT_CLOSE(CavernLogWindow::OnClose)
172 #else
173 EVT_IDLE(CavernLogWindow::OnIdle)
174 #endif
175 EVT_END_PROCESS(wxID_ANY, CavernLogWindow::OnEndProcess)
176 END_EVENT_TABLE()
178 wxString escape_for_shell(wxString s, bool protect_dash)
180 #ifdef __WXMSW__
181 // Correct quoting rules are insane:
183 // http://blogs.msdn.com/b/twistylittlepassagesallalike/archive/2011/04/23/everyone-quotes-arguments-the-wrong-way.aspx
185 // Thankfully wxExecute passes the command string to CreateProcess(), so
186 // at least we don't need to quote for cmd.exe too.
187 if (s.empty() || s.find_first_of(wxT(" \"\t\n\v")) != s.npos) {
188 // Need to quote.
189 s.insert(0, wxT('"'));
190 for (size_t p = 1; p < s.size(); ++p) {
191 size_t backslashes = 0;
192 while (s[p] == wxT('\\')) {
193 ++backslashes;
194 if (++p == s.size()) {
195 // Escape all the backslashes, since they're before
196 // the closing quote we add below.
197 s.append(backslashes, wxT('\\'));
198 goto done;
202 if (s[p] == wxT('"')) {
203 // Escape any preceding backslashes and this quote.
204 s.insert(p, backslashes + 1, wxT('\\'));
205 p += backslashes + 1;
208 done:
209 s.append(wxT('"'));
211 #else
212 size_t p = 0;
213 if (protect_dash && !s.empty() && s[0u] == '-') {
214 // If the filename starts with a '-', protect it from being
215 // treated as an option by prepending "./".
216 s.insert(0, wxT("./"));
217 p = 2;
219 while (p < s.size()) {
220 // Exclude a few safe characters which are common in filenames
221 if (!isalnum((unsigned char)s[p]) && strchr("/._-", s[p]) == NULL) {
222 s.insert(p, 1, wxT('\\'));
223 ++p;
225 ++p;
227 #endif
228 return s;
231 wxString get_command_path(const wxChar * command_name)
233 #ifdef __WXMSW__
234 wxString cmd;
236 DWORD len = 256;
237 wchar_t *buf = NULL;
238 while (1) {
239 DWORD got;
240 buf = (wchar_t*)osrealloc(buf, len * 2);
241 got = GetModuleFileNameW(NULL, buf, len);
242 if (got < len) break;
243 len += len;
245 /* Strange Win32 nastiness - strip prefix "\\?\" if present */
246 wchar_t *start = buf;
247 if (wcsncmp(start, L"\\\\?\\", 4) == 0) start += 4;
248 wchar_t * slash = wcsrchr(start, L'\\');
249 if (slash) {
250 cmd.assign(start, slash - start + 1);
252 osfree(buf);
254 #else
255 wxString cmd = wxString(msg_exepth(), wxConvUTF8);
256 #endif
257 cmd += command_name;
258 return cmd;
261 CavernLogWindow::CavernLogWindow(MainFrm * mainfrm_, const wxString & survey_, wxWindow * parent)
262 : wxHtmlWindow(parent),
263 mainfrm(mainfrm_),
264 end(buf), survey(survey_)
266 int fsize = parent->GetFont().GetPointSize();
267 int sizes[7] = { fsize, fsize, fsize, fsize, fsize, fsize, fsize };
268 SetFonts(wxString(), wxString(), sizes);
271 CavernLogWindow::~CavernLogWindow()
273 #ifdef CAVERNLOG_USE_THREADS
274 if (thread) stop_thread();
275 #endif
276 if (cavern_out) {
277 wxEndBusyCursor();
278 cavern_out->Detach();
282 #ifdef CAVERNLOG_USE_THREADS
283 void
284 CavernLogWindow::stop_thread()
286 // Killing the subprocess by its pid is theoretically racy, but in practice
287 // it's not going to cause issues, and it's all the wxProcess API seems to
288 // allow us to do. If we don't kill the subprocess, we need to wait for it
289 // to write out some output - there seems to be no way to do the equivalent
290 // of select() with a timeout on a wxInputStream.
292 // The only alternative to this seems to be to do:
294 // while (!s.CanRead()) {
295 // if (TestDestroy()) return (wxThread::ExitCode)0;
296 // wxMilliSleep(N);
297 // }
299 // But that makes the log window update sluggishly, and we're using a
300 // worker thread precisely to try to avoid having to do dumb stuff like
301 // this.
302 wxProcess::Kill(cavern_out->GetPid());
305 wxCriticalSectionLocker enter(thread_lock);
306 if (thread) {
307 wxThreadError res;
308 res = thread->Delete(NULL, wxTHREAD_WAIT_BLOCK);
309 if (res != wxTHREAD_NO_ERROR) {
310 // FIXME
315 // Wait for thread to complete.
316 while (true) {
318 wxCriticalSectionLocker enter(thread_lock);
319 if (!thread) break;
321 wxMilliSleep(1);
325 void
326 CavernLogWindow::OnClose(wxCloseEvent &)
328 if (thread) stop_thread();
329 Destroy();
331 #endif
333 void
334 CavernLogWindow::OnLinkClicked(const wxHtmlLinkInfo &link)
336 wxString href = link.GetHref();
337 wxString title = link.GetTarget();
338 size_t colon2 = href.rfind(wxT(':'));
339 if (colon2 == wxString::npos)
340 return;
341 size_t colon = href.rfind(wxT(':'), colon2 - 1);
342 if (colon == wxString::npos)
343 return;
344 wxString cmd;
345 wxChar * p = wxGetenv(wxT("SURVEXEDITOR"));
346 if (p) {
347 cmd = p;
348 if (!cmd.find(wxT("$f"))) {
349 cmd += wxT(" $f");
351 } else {
352 p = wxGetenv(wxT("VISUAL"));
353 if (!p) p = wxGetenv(wxT("EDITOR"));
354 if (!p) {
355 cmd = wxT(DEFAULT_EDITOR_COMMAND);
356 } else {
357 cmd = p;
358 if (cmd == "gvim") {
359 cmd = wxT(GVIM_COMMAND);
360 } else if (cmd == "vim") {
361 cmd = wxT(VIM_COMMAND);
362 } else if (cmd == "nvim") {
363 cmd = wxT(NVIM_COMMAND);
364 } else if (cmd == "gedit") {
365 cmd = wxT(GEDIT_COMMAND);
366 } else if (cmd == "pluma") {
367 cmd = wxT(PLUMA_COMMAND);
368 } else if (cmd == "emacs") {
369 cmd = wxT(EMACS_COMMAND);
370 } else if (cmd == "nano") {
371 cmd = wxT(NANO_COMMAND);
372 } else if (cmd == "jed") {
373 cmd = wxT(JED_COMMAND);
374 } else if (cmd == "kate") {
375 cmd = wxT(KATE_COMMAND);
376 } else {
377 // Escape any $.
378 cmd.Replace(wxT("$"), wxT("$$"));
379 cmd += wxT(" $f");
383 size_t i = 0;
384 while ((i = cmd.find(wxT('$'), i)) != wxString::npos) {
385 if (++i >= cmd.size()) break;
386 switch ((int)cmd[i]) {
387 case wxT('$'):
388 cmd.erase(i, 1);
389 break;
390 case wxT('f'): {
391 wxString f = escape_for_shell(href.substr(0, colon), true);
392 cmd.replace(i - 1, 2, f);
393 i += f.size() - 1;
394 break;
396 case wxT('t'): {
397 wxString t = escape_for_shell(title);
398 cmd.replace(i - 1, 2, t);
399 i += t.size() - 1;
400 break;
402 case wxT('l'): {
403 wxString l = escape_for_shell(href.substr(colon + 1, colon2 - colon - 1));
404 cmd.replace(i - 1, 2, l);
405 i += l.size() - 1;
406 break;
408 case wxT('c'): {
409 wxString l;
410 if (colon2 >= href.size() - 1)
411 l = wxT("0");
412 else
413 l = escape_for_shell(href.substr(colon2 + 1));
414 cmd.replace(i - 1, 2, l);
415 i += l.size() - 1;
416 break;
418 default:
419 ++i;
423 if (wxExecute(cmd, wxEXEC_ASYNC|wxEXEC_MAKE_GROUP_LEADER) >= 0)
424 return;
426 wxString m;
427 // TRANSLATORS: %s is replaced by the command we attempted to run.
428 m.Printf(wmsg(/*Couldn’t run external command: “%s”*/17), cmd.c_str());
429 m += wxT(" (");
430 m += wxString(strerror(errno), wxConvUTF8);
431 m += wxT(')');
432 wxGetApp().ReportError(m);
435 void
436 CavernLogWindow::process(const wxString &file)
438 SetPage(wxString());
439 #ifdef CAVERNLOG_USE_THREADS
440 if (thread) stop_thread();
441 #endif
442 if (cavern_out) {
443 cavern_out->Detach();
444 cavern_out = NULL;
445 } else {
446 wxBeginBusyCursor();
449 SetFocus();
450 filename = file;
452 info_count = 0;
453 link_count = 0;
454 cur.resize(0);
455 log_txt.resize(0);
457 #ifdef __WXMSW__
458 SetEnvironmentVariable(wxT("SURVEX_UTF8"), wxT("1"));
459 #else
460 setenv("SURVEX_UTF8", "1", 1);
461 #endif
463 wxString escaped_file = escape_for_shell(file, true);
464 wxString cmd = get_command_path(L"cavern");
465 cmd = escape_for_shell(cmd, false);
466 cmd += wxT(" -o ");
467 cmd += escaped_file;
468 cmd += wxT(' ');
469 cmd += escaped_file;
471 cavern_out = wxProcess::Open(cmd);
472 if (!cavern_out) {
473 wxString m;
474 m.Printf(wmsg(/*Couldn’t run external command: “%s”*/17), cmd.c_str());
475 m += wxT(" (");
476 m += wxString(strerror(errno), wxConvUTF8);
477 m += wxT(')');
478 wxGetApp().ReportError(m);
479 return;
482 // We want to receive the wxProcessEvent when cavern exits.
483 cavern_out->SetNextHandler(this);
485 #ifdef CAVERNLOG_USE_THREADS
486 thread = new CavernThread(this, cavern_out->GetInputStream());
487 if (thread->Run() != wxTHREAD_NO_ERROR) {
488 wxGetApp().ReportError(wxT("Thread failed to start"));
489 delete thread;
490 thread = NULL;
492 #endif
495 void
496 CavernLogWindow::OnCavernOutput(wxCommandEvent & e_)
498 CavernOutputEvent & e = (CavernOutputEvent&)e_;
500 if (e.len > 0) {
501 ssize_t n = e.len;
502 if (size_t(n) > sizeof(buf) - (end - buf)) abort();
503 memcpy(end, e.buf, n);
504 log_txt.append((const char *)end, n);
505 end += n;
507 const unsigned char * p = buf;
509 while (p != end) {
510 int ch = *p;
511 if (ch >= 0x80) {
512 // Decode multi-byte UTF-8 sequence.
513 if (ch < 0xc0) {
514 // Invalid UTF-8 sequence.
515 goto bad_utf8;
516 } else if (ch < 0xe0) {
517 /* 2 byte sequence */
518 if (end - p < 2) {
519 // Incomplete UTF-8 sequence - try to read more.
520 break;
522 int ch1 = *++p;
523 if ((ch1 & 0xc0) != 0x80) {
524 // Invalid UTF-8 sequence.
525 goto bad_utf8;
527 ch = ((ch & 0x1f) << 6) | (ch1 & 0x3f);
528 } else if (ch < 0xf0) {
529 /* 3 byte sequence */
530 if (end - p < 3) {
531 // Incomplete UTF-8 sequence - try to read more.
532 break;
534 int ch1 = *++p;
535 ch = ((ch & 0x1f) << 12) | ((ch1 & 0x3f) << 6);
536 if ((ch1 & 0xc0) != 0x80) {
537 // Invalid UTF-8 sequence.
538 goto bad_utf8;
540 int ch2 = *++p;
541 if ((ch2 & 0xc0) != 0x80) {
542 // Invalid UTF-8 sequence.
543 goto bad_utf8;
545 ch |= (ch2 & 0x3f);
546 } else {
547 // Overlong UTF-8 sequence.
548 goto bad_utf8;
551 ++p;
553 if (false) {
554 bad_utf8:
555 // Resync to next byte which starts a UTF-8 sequence.
556 while (p != end) {
557 if (*p < 0x80 || (*p >= 0xc0 && *p < 0xf0)) break;
558 ++p;
560 cur += badutf8_html;
561 continue;
564 switch (ch) {
565 case '\r':
566 // Ignore.
567 break;
568 case '\n': {
569 if (cur.empty()) continue;
570 if (cur[0] == ' ') {
571 if (source_line.empty()) {
572 // Source line shown for context. Store it so we
573 // can use the caret line to highlight it.
574 swap(source_line, cur);
575 } else {
576 size_t caret = cur.rfind('^');
577 if (caret != wxString::npos) {
578 size_t tilde = cur.rfind('~');
579 if (tilde == wxString::npos || tilde < caret) {
580 tilde = caret;
582 cur = "&nbsp;";
583 // FIXME: Need to count each & entity as one character...
584 cur.append(source_line, 1, caret - 1);
585 if (caret < source_line.size()) {
586 cur.append("<b>");
587 cur.append(highlight ? highlight : wxT("<span \"color:blue\">"));
588 cur.append(source_line, caret, tilde + 1 - caret);
589 cur.append("</span></b>");
591 if (tilde + 1 < source_line.size()) {
592 cur.append(source_line, tilde + 1, wxString::npos);
594 } else {
595 // No caret in second line - just output both.
596 source_line.replace(0, 1, "&nbsp;");
597 source_line += "<br>\n&nbsp;";
598 source_line.append(cur, 1, wxString::npos);
599 swap(cur, source_line);
601 cur += "<br>\n";
602 AppendToPage(cur);
603 cur.clear();
604 source_line.clear();
606 continue;
609 if (!source_line.empty()) {
610 // Previous line was a source line without column info
611 // so just show it.
612 source_line.replace(0, 1, "&nbsp;");
613 source_line += "<br>\n";
614 AppendToPage(source_line);
615 source_line.clear();
617 #ifndef __WXMSW__
618 size_t colon = cur.find(':');
619 #else
620 // If the path is "C:\path\to\file.svx" then don't split at the
621 // : after the drive letter! FIXME: better to look for ": "?
622 size_t colon = cur.find(':', 2);
623 #endif
624 if (colon != wxString::npos && colon < cur.size() - 2) {
625 ++colon;
626 size_t i = colon;
627 while (i < cur.size() - 2 &&
628 cur[i] >= wxT('0') && cur[i] <= wxT('9')) {
629 ++i;
631 if (i > colon && cur[i] == wxT(':') ) {
632 colon = i;
633 // Check for column number.
634 while (++i < cur.size() - 2 &&
635 cur[i] >= wxT('0') && cur[i] <= wxT('9')) { }
636 bool have_column = (i > colon + 1 && cur[i] == wxT(':'));
637 if (have_column) {
638 colon = i;
639 } else {
640 // If there's no colon, include a trailing ':'
641 // so that we can unambiguously split the href
642 // value up into filename, line and column.
643 ++colon;
645 wxString tag = wxT("<a href=\"");
646 tag.append(cur, 0, colon);
647 while (cur[++i] == wxT(' ')) { }
648 tag += wxT("\" target=\"");
649 wxString target(cur, i, wxString::npos);
650 target.Replace(badutf8_html, badutf8);
651 tag += target;
652 tag += wxT("\">");
653 cur.insert(0, tag);
654 size_t offset = colon + tag.size();
655 cur.insert(offset, wxT("</a>"));
656 offset += 4 + 2;
658 if (!have_column) --offset;
660 static const wxString & error_marker = wmsg(/*error*/93) + ":";
661 static const wxString & warning_marker = wmsg(/*warning*/4) + ":";
662 static const wxString & info_marker = wmsg(/*info*/485) + ":";
664 if (cur.substr(offset, error_marker.size()) == error_marker) {
665 // Show "error" marker in red.
666 highlight = wxT("<span style=\"color:red\">");
667 cur.insert(offset, highlight);
668 offset += 24 + error_marker.size() - 1;
669 cur.insert(offset, wxT("</span>"));
670 } else if (cur.substr(offset, warning_marker.size()) == warning_marker) {
671 // Show "warning" marker in orange.
672 highlight = wxT("<span style=\"color:orange\">");
673 cur.insert(offset, highlight);
674 offset += 27 + warning_marker.size() - 1;
675 cur.insert(offset, wxT("</span>"));
676 } else if (cur.substr(offset, info_marker.size()) == info_marker) {
677 // Show "info" marker in blue.
678 ++info_count;
679 highlight = wxT("<span style=\"color:blue\">");
680 cur.insert(offset, highlight);
681 offset += 25 + info_marker.size() - 1;
682 cur.insert(offset, wxT("</span>"));
683 } else {
684 highlight = NULL;
687 ++link_count;
691 // Save the scrollbar positions.
692 int scroll_x = 0, scroll_y = 0;
693 GetViewStart(&scroll_x, &scroll_y);
695 cur += wxT("<br>\n");
696 AppendToPage(cur);
698 if (!link_count) {
699 // Auto-scroll the window until we've reported a
700 // warning or error.
701 int x, y;
702 GetVirtualSize(&x, &y);
703 int xs, ys;
704 GetClientSize(&xs, &ys);
705 y -= ys;
706 int xu, yu;
707 GetScrollPixelsPerUnit(&xu, &yu);
708 Scroll(scroll_x, y / yu);
709 } else {
710 // Restore the scrollbar positions.
711 Scroll(scroll_x, scroll_y);
714 cur.clear();
715 break;
717 case '<':
718 cur += wxT("&lt;");
719 break;
720 case '>':
721 cur += wxT("&gt;");
722 break;
723 case '&':
724 cur += wxT("&amp;");
725 break;
726 case '"':
727 cur += wxT("&#34;");
728 continue;
729 default:
730 #ifdef wxUSE_UNICODE
731 cur += wxChar(ch);
732 #else
733 // This approach means that highlighting of "error" or
734 // "warning" won't work in translations where they contain
735 // non-ASCII characters, but wxWidgets >= 3.0 in always
736 // Unicode, so this corner case is already very uncommon,
737 // and will become irrelevant with time.
738 if (ch >= 128) {
739 cur += wxString::Format(wxT("&#%u;"), ch);
740 } else {
741 cur += (char)ch;
743 #endif
747 size_t left = end - p;
748 end = buf + left;
749 if (left) memmove(buf, p, left);
750 Update();
751 return;
754 if (!source_line.empty()) {
755 // Previous line was a source line without column info
756 // so just show it.
757 source_line.replace(0, 1, "&nbsp;");
758 source_line += "<br>\n";
759 AppendToPage(source_line);
760 source_line.clear();
763 if (e.len <= 0 && buf != end) {
764 // Truncated UTF-8 sequence.
765 cur += badutf8_html;
767 if (!cur.empty()) {
768 cur += "<br>\n";
769 AppendToPage("<hr>" + cur);
772 /* TRANSLATORS: Label for button in aven’s cavern log window which
773 * allows the user to save the log to a file. */
774 AppendToPage(wxString::Format(wxT("<avenbutton id=%d name=\"%s\">"),
775 (int)LOG_SAVE,
776 wmsg(/*&Save Log*/446).c_str()));
777 wxEndBusyCursor();
778 delete cavern_out;
779 cavern_out = NULL;
780 if (e.len < 0) {
781 /* Negative length indicates non-zero exit status from cavern. */
782 /* TRANSLATORS: Label for button in aven’s cavern log window which
783 * causes the survey data to be reprocessed. */
784 AppendToPage(wxString::Format(wxT("<avenbutton default id=%d name=\"%s\">"),
785 (int)LOG_REPROCESS,
786 wmsg(/*&Reprocess*/184).c_str()));
787 return;
789 AppendToPage(wxString::Format(wxT("<avenbutton id=%d name=\"%s\">"),
790 (int)LOG_REPROCESS,
791 wmsg(/*&Reprocess*/184).c_str()));
792 AppendToPage(wxString::Format(wxT("<avenbutton default id=%d>"), (int)wxID_OK));
793 Update();
794 init_done = false;
797 wxString file3d(filename, 0, filename.length() - 3);
798 file3d.append(wxT("3d"));
799 if (!mainfrm->LoadData(file3d, survey)) {
800 return;
804 // Don't stay on log if there there are only "info" diagnostics.
805 if (link_count == info_count) {
806 wxCommandEvent dummy;
807 OnOK(dummy);
811 void
812 CavernLogWindow::OnEndProcess(wxProcessEvent & evt)
814 CavernOutputEvent * e = new CavernOutputEvent();
815 // Zero length indicates successful exit, negative length unsuccessful exit.
816 e->len = (evt.GetExitCode() == 0 ? 0 : -1);
817 QueueEvent(e);
820 void
821 CavernLogWindow::OnReprocess(wxCommandEvent &)
823 process(filename);
826 void
827 CavernLogWindow::OnSave(wxCommandEvent &)
829 wxString filelog(filename, 0, filename.length() - 3);
830 filelog += wxT("log");
831 #ifdef __WXMOTIF__
832 wxString ext(wxT("*.log"));
833 #else
834 /* TRANSLATORS: Log files from running cavern (extension .log) */
835 wxString ext = wmsg(/*Log files*/447);
836 ext += wxT("|*.log");
837 #endif
838 wxFileDialog dlg(this, wmsg(/*Select an output filename*/319),
839 wxString(), filelog, ext,
840 wxFD_SAVE|wxFD_OVERWRITE_PROMPT);
841 if (dlg.ShowModal() != wxID_OK) return;
842 filelog = dlg.GetPath();
843 FILE * fh_log = wxFopen(filelog, wxT("w"));
844 if (!fh_log) {
845 wxGetApp().ReportError(wxString::Format(wmsg(/*Error writing to file “%s”*/110), filelog.c_str()));
846 return;
848 fwrite(log_txt.data(), log_txt.size(), 1, fh_log);
849 fclose(fh_log);
852 void
853 CavernLogWindow::OnOK(wxCommandEvent &)
855 if (init_done) {
856 mainfrm->HideLog(this);
857 } else {
858 mainfrm->InitialiseAfterLoad(filename, survey);
859 init_done = true;