Convert caret range into highlight on source line
[survex.git] / src / cavernlog.cc
blobec1f15ad1dd3bf6380d4576fb356b86b7fc1965d
1 /* cavernlog.cc
2 * Run cavern inside an Aven window
4 * Copyright (C) 2005,2006,2010,2011,2012,2014,2015,2016 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 enum { LOG_REPROCESS = 1234, LOG_SAVE = 1235 };
47 static const wxString badutf8_html(
48 wxT("<span style=\"color:white;background-color:red;\">&#xfffd;</span>"));
49 static const wxString badutf8(wxUniChar(0xfffd));
51 // New event type for passing a chunk of cavern output from the worker thread
52 // to the main thread (or from the idle event handler if we're not using
53 // threads).
54 class CavernOutputEvent;
56 wxDEFINE_EVENT(wxEVT_CAVERN_OUTPUT, CavernOutputEvent);
58 class CavernOutputEvent : public wxEvent {
59 public:
60 char buf[1000];
61 int len;
62 CavernOutputEvent() : wxEvent(0, wxEVT_CAVERN_OUTPUT), len(0) { }
64 wxEvent * Clone() const {
65 CavernOutputEvent * e = new CavernOutputEvent();
66 e->len = len;
67 if (len > 0) memcpy(e->buf, buf, len);
68 return e;
72 #ifdef CAVERNLOG_USE_THREADS
73 class CavernThread : public wxThread {
74 protected:
75 virtual ExitCode Entry();
77 CavernLogWindow *handler;
79 wxInputStream * in;
81 public:
82 CavernThread(CavernLogWindow *handler_, wxInputStream * in_)
83 : wxThread(wxTHREAD_DETACHED), handler(handler_), in(in_) { }
85 ~CavernThread() {
86 wxCriticalSectionLocker enter(handler->thread_lock);
87 handler->thread = NULL;
91 wxThread::ExitCode
92 CavernThread::Entry()
94 while (true) {
95 CavernOutputEvent * e = new CavernOutputEvent();
96 in->Read(e->buf, sizeof(e->buf));
97 size_t n = in->LastRead();
98 if (n == 0 || TestDestroy()) {
99 delete e;
100 return (wxThread::ExitCode)0;
102 if (n == 1 && e->buf[0] == '\n') {
103 // Don't send an event with just a blank line in.
104 in->Read(e->buf + 1, sizeof(e->buf) - 1);
105 n += in->LastRead();
106 if (TestDestroy()) {
107 delete e;
108 return (wxThread::ExitCode)0;
111 e->len = n;
112 handler->QueueEvent(e);
116 #else
118 void
119 CavernLogWindow::OnIdle(wxIdleEvent& event)
121 if (cavern_out == NULL) return;
123 wxInputStream * in = cavern_out->GetInputStream();
125 if (!in->CanRead()) {
126 // Avoid a tight busy-loop on idle events.
127 wxMilliSleep(10);
129 if (in->CanRead()) {
130 CavernOutputEvent * e = new CavernOutputEvent();
131 in->Read(e->buf, sizeof(e->buf));
132 size_t n = in->LastRead();
133 if (n == 0) {
134 delete e;
135 return;
137 e->len = n;
138 QueueEvent(e);
141 event.RequestMore();
143 #endif
145 BEGIN_EVENT_TABLE(CavernLogWindow, wxHtmlWindow)
146 EVT_BUTTON(LOG_REPROCESS, CavernLogWindow::OnReprocess)
147 EVT_BUTTON(LOG_SAVE, CavernLogWindow::OnSave)
148 EVT_BUTTON(wxID_OK, CavernLogWindow::OnOK)
149 EVT_COMMAND(wxID_ANY, wxEVT_CAVERN_OUTPUT, CavernLogWindow::OnCavernOutput)
150 #ifdef CAVERNLOG_USE_THREADS
151 EVT_CLOSE(CavernLogWindow::OnClose)
152 #else
153 EVT_IDLE(CavernLogWindow::OnIdle)
154 #endif
155 EVT_END_PROCESS(wxID_ANY, CavernLogWindow::OnEndProcess)
156 END_EVENT_TABLE()
158 wxString escape_for_shell(wxString s, bool protect_dash)
160 #ifdef __WXMSW__
161 // Correct quoting rules are insane:
163 // http://blogs.msdn.com/b/twistylittlepassagesallalike/archive/2011/04/23/everyone-quotes-arguments-the-wrong-way.aspx
165 // Thankfully wxExecute passes the command string to CreateProcess(), so
166 // at least we don't need to quote for cmd.exe too.
167 if (s.empty() || s.find_first_of(wxT(" \"\t\n\v")) != s.npos) {
168 // Need to quote.
169 s.insert(0, wxT('"'));
170 for (size_t p = 1; p < s.size(); ++p) {
171 size_t backslashes = 0;
172 while (s[p] == wxT('\\')) {
173 ++backslashes;
174 if (++p == s.size()) {
175 // Escape all the backslashes, since they're before
176 // the closing quote we add below.
177 s.append(backslashes, wxT('\\'));
178 goto done;
182 if (s[p] == wxT('"')) {
183 // Escape any preceding backslashes and this quote.
184 s.insert(p, backslashes + 1, wxT('\\'));
185 p += backslashes + 1;
188 done:
189 s.append(wxT('"'));
191 #else
192 size_t p = 0;
193 if (protect_dash && !s.empty() && s[0u] == '-') {
194 // If the filename starts with a '-', protect it from being
195 // treated as an option by prepending "./".
196 s.insert(0, wxT("./"));
197 p = 2;
199 while (p < s.size()) {
200 // Exclude a few safe characters which are common in filenames
201 if (!isalnum(s[p]) && strchr("/._-", s[p]) == NULL) {
202 s.insert(p, 1, wxT('\\'));
203 ++p;
205 ++p;
207 #endif
208 return s;
211 wxString get_command_path(const wxChar * command_name)
213 #ifdef __WXMSW__
214 wxString cmd;
216 DWORD len = 256;
217 wchar_t *buf = NULL;
218 while (1) {
219 DWORD got;
220 buf = (wchar_t*)osrealloc(buf, len * 2);
221 got = GetModuleFileNameW(NULL, buf, len);
222 if (got < len) break;
223 len += len;
225 /* Strange Win32 nastiness - strip prefix "\\?\" if present */
226 wchar_t *start = buf;
227 if (wcsncmp(start, L"\\\\?\\", 4) == 0) start += 4;
228 wchar_t * slash = wcsrchr(start, L'\\');
229 if (slash) {
230 cmd.assign(start, slash - start + 1);
232 osfree(buf);
234 #else
235 wxString cmd = wxString(msg_exepth(), wxConvUTF8);
236 #endif
237 cmd += command_name;
238 return cmd;
241 CavernLogWindow::CavernLogWindow(MainFrm * mainfrm_, const wxString & survey_, wxWindow * parent)
242 : wxHtmlWindow(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize,
243 wxHW_DEFAULT_STYLE|wxHW_NO_SELECTION),
244 mainfrm(mainfrm_), cavern_out(NULL),
245 link_count(0), end(buf), init_done(false), survey(survey_)
246 #ifdef CAVERNLOG_USE_THREADS
247 , thread(NULL)
248 #endif
250 int fsize = parent->GetFont().GetPointSize();
251 int sizes[7] = { fsize, fsize, fsize, fsize, fsize, fsize, fsize };
252 SetFonts(wxString(), wxString(), sizes);
255 CavernLogWindow::~CavernLogWindow()
257 #ifdef CAVERNLOG_USE_THREADS
258 if (thread) stop_thread();
259 #endif
260 if (cavern_out) {
261 wxEndBusyCursor();
262 cavern_out->Detach();
266 #ifdef CAVERNLOG_USE_THREADS
267 void
268 CavernLogWindow::stop_thread()
270 // Killing the subprocess by its pid is theoretically racy, but in practice
271 // it's not going to cause issues, and it's all the wxProcess API seems to
272 // allow us to do. If we don't kill the subprocess, we need to wait for it
273 // to write out some output - there seems to be no way to do the equivalent
274 // of select() with a timeout on a a wxInputStream.
276 // The only alternative to this seems to be to do:
278 // while (!s.CanRead()) {
279 // if (TestDestroy()) return (wxThread::ExitCode)0;
280 // wxMilliSleep(N);
281 // }
283 // But that makes the log window update sluggishly, and we're using a
284 // worker thread precisely to try to avoid having to do dumb stuff like
285 // this.
286 wxProcess::Kill(cavern_out->GetPid());
289 wxCriticalSectionLocker enter(thread_lock);
290 if (thread) {
291 wxThreadError res;
292 #if wxCHECK_VERSION(2,9,2)
293 res = thread->Delete(NULL, wxTHREAD_WAIT_BLOCK);
294 #else
295 res = thread->Delete();
296 #endif
297 if (res != wxTHREAD_NO_ERROR) {
298 // FIXME
303 // Wait for thread to complete.
304 while (true) {
306 wxCriticalSectionLocker enter(thread_lock);
307 if (!thread) break;
309 wxMilliSleep(1);
313 void
314 CavernLogWindow::OnClose(wxCloseEvent &)
316 if (thread) stop_thread();
317 Destroy();
319 #endif
321 void
322 CavernLogWindow::OnLinkClicked(const wxHtmlLinkInfo &link)
324 wxString href = link.GetHref();
325 wxString title = link.GetTarget();
326 size_t colon2 = href.rfind(wxT(':'));
327 if (colon2 == wxString::npos)
328 return;
329 size_t colon = href.rfind(wxT(':'), colon2 - 1);
330 if (colon == wxString::npos)
331 return;
332 #ifdef __WXMSW__
333 wxString cmd = wxT("notepad $f");
334 #elif defined __WXMAC__
335 wxString cmd = wxT("open -t $f");
336 #else
337 wxString cmd = wxT("x-terminal-emulator -e vim +'call cursor($l,$c)' $f");
338 // wxString cmd = wxT("gedit -b $f +$l:$c $f");
339 // wxString cmd = wxT("x-terminal-emulator -e emacs +$l $f");
340 // wxString cmd = wxT("x-terminal-emulator -e nano +$l $f");
341 // wxString cmd = wxT("x-terminal-emulator -e jed -g $l $f");
342 #endif
343 wxChar * p = wxGetenv(wxT("SURVEXEDITOR"));
344 if (p) {
345 cmd = p;
346 if (!cmd.find(wxT("$f"))) {
347 cmd += wxT(" $f");
350 size_t i = 0;
351 while ((i = cmd.find(wxT('$'), i)) != wxString::npos) {
352 if (++i >= cmd.size()) break;
353 switch ((int)cmd[i]) {
354 case wxT('$'):
355 cmd.erase(i, 1);
356 break;
357 case wxT('f'): {
358 wxString f = escape_for_shell(href.substr(0, colon), true);
359 cmd.replace(i - 1, 2, f);
360 i += f.size() - 1;
361 break;
363 case wxT('t'): {
364 wxString t = escape_for_shell(title);
365 cmd.replace(i - 1, 2, t);
366 i += t.size() - 1;
367 break;
369 case wxT('l'): {
370 wxString l = escape_for_shell(href.substr(colon + 1, colon2 - colon - 1));
371 cmd.replace(i - 1, 2, l);
372 i += l.size() - 1;
373 break;
375 case wxT('c'): {
376 wxString l;
377 if (colon2 >= href.size() - 1)
378 l = wxT("0");
379 else
380 l = escape_for_shell(href.substr(colon2 + 1));
381 cmd.replace(i - 1, 2, l);
382 i += l.size() - 1;
383 break;
385 default:
386 ++i;
390 if (wxExecute(cmd, wxEXEC_ASYNC|wxEXEC_MAKE_GROUP_LEADER) >= 0)
391 return;
393 wxString m;
394 // TRANSLATORS: %s is replaced by the command we attempted to run.
395 m.Printf(wmsg(/*Couldn’t run external command: “%s”*/17), cmd.c_str());
396 m += wxT(" (");
397 m += wxString(strerror(errno), wxConvUTF8);
398 m += wxT(')');
399 wxGetApp().ReportError(m);
402 void
403 CavernLogWindow::process(const wxString &file)
405 SetPage(wxString());
406 #ifdef CAVERNLOG_USE_THREADS
407 if (thread) stop_thread();
408 #endif
409 if (cavern_out) {
410 cavern_out->Detach();
411 cavern_out = NULL;
412 } else {
413 wxBeginBusyCursor();
416 SetFocus();
417 filename = file;
419 link_count = 0;
420 cur.resize(0);
421 log_txt.resize(0);
423 #ifdef __WXMSW__
424 SetEnvironmentVariable(wxT("SURVEX_UTF8"), wxT("1"));
425 #else
426 setenv("SURVEX_UTF8", "1", 1);
427 #endif
429 wxString escaped_file = escape_for_shell(file, true);
430 wxString cmd = get_command_path(L"cavern");
431 cmd = escape_for_shell(cmd, false);
432 cmd += wxT(" -o ");
433 cmd += escaped_file;
434 cmd += wxT(' ');
435 cmd += escaped_file;
437 cavern_out = wxProcess::Open(cmd);
438 if (!cavern_out) {
439 wxString m;
440 m.Printf(wmsg(/*Couldn’t run external command: “%s”*/17), cmd.c_str());
441 m += wxT(" (");
442 m += wxString(strerror(errno), wxConvUTF8);
443 m += wxT(')');
444 wxGetApp().ReportError(m);
445 return;
448 // We want to receive the wxProcessEvent when cavern exits.
449 cavern_out->SetNextHandler(this);
451 #ifdef CAVERNLOG_USE_THREADS
452 thread = new CavernThread(this, cavern_out->GetInputStream());
453 if (thread->Run() != wxTHREAD_NO_ERROR) {
454 wxGetApp().ReportError(wxT("Thread failed to start"));
455 delete thread;
456 thread = NULL;
458 #endif
461 void
462 CavernLogWindow::OnCavernOutput(wxCommandEvent & e_)
464 CavernOutputEvent & e = (CavernOutputEvent&)e_;
466 if (e.len > 0) {
467 ssize_t n = e.len;
468 if (size_t(n) > sizeof(buf) - (end - buf)) abort();
469 memcpy(end, e.buf, n);
470 log_txt.append((const char *)end, n);
471 end += n;
473 wxString source_line;
474 const wxChar * highlight = NULL;
476 const unsigned char * p = buf;
478 while (p != end) {
479 int ch = *p++;
480 if (ch >= 0x80) {
481 // Decode multi-byte UTF-8 sequence.
482 if (ch < 0xc0) {
483 // Invalid UTF-8 sequence.
484 goto bad_utf8;
485 } else if (ch < 0xe0) {
486 /* 2 byte sequence */
487 if (p == end) {
488 // Incomplete UTF-8 sequence - try to read more.
489 break;
491 int ch1 = *p++;
492 if ((ch1 & 0xc0) != 0x80) {
493 // Invalid UTF-8 sequence.
494 goto bad_utf8;
496 ch = ((ch & 0x1f) << 6) | (ch1 & 0x3f);
497 } else if (ch < 0xf0) {
498 /* 3 byte sequence */
499 if (end - p <= 1) {
500 // Incomplete UTF-8 sequence - try to read more.
501 break;
503 int ch1 = *p++;
504 ch = ((ch & 0x1f) << 12) | ((ch1 & 0x3f) << 6);
505 if ((ch1 & 0xc0) != 0x80) {
506 // Invalid UTF-8 sequence.
507 goto bad_utf8;
509 int ch2 = *p++;
510 if ((ch2 & 0xc0) != 0x80) {
511 // Invalid UTF-8 sequence.
512 goto bad_utf8;
514 ch |= (ch2 & 0x3f);
515 } else {
516 // Overlong UTF-8 sequence.
517 goto bad_utf8;
521 if (false) {
522 bad_utf8:
523 // Resync to next byte which starts a UTF-8 sequence.
524 while (p != end) {
525 if (*p < 0x80 || (*p >= 0xc0 && *p < 0xf0)) break;
526 ++p;
528 cur += badutf8_html;
529 continue;
532 switch (ch) {
533 case '\r':
534 // Ignore.
535 break;
536 case '\n': {
537 if (cur.empty()) continue;
538 if (cur[0] == ' ') {
539 if (source_line.empty()) {
540 // Source line shown for context. Store it so we
541 // can use the caret line to highlight it.
542 swap(source_line, cur);
543 } else {
544 size_t caret = cur.rfind('^');
545 if (caret != wxString::npos) {
546 size_t tilde = cur.rfind('~');
547 if (tilde == wxString::npos || tilde < caret) {
548 tilde = caret;
550 cur = "&nbsp;";
551 // FIXME: Need to count each & entity as one character...
552 cur.append(source_line, 1, caret - 1);
553 cur.append("<b>");
554 cur.append(highlight ? highlight : wxT("<span \"color:green\">"));
555 cur.append(source_line, caret, tilde + 1 - caret);
556 cur.append("</span></b>");
557 cur.append(source_line, tilde + 1, wxString::npos);
558 } else {
559 swap(cur, source_line);
561 cur += "<br>\n";
562 AppendToPage(cur);
563 cur.clear();
564 source_line.clear();
566 continue;
568 #ifndef __WXMSW__
569 size_t colon = cur.find(':');
570 #else
571 // If the path is "C:\path\to\file.svx" then don't split at the
572 // : after the drive letter! FIXME: better to look for ": "?
573 size_t colon = cur.find(':', 2);
574 #endif
575 if (colon != wxString::npos && colon < cur.size() - 2) {
576 ++colon;
577 size_t i = colon;
578 while (i < cur.size() - 2 &&
579 cur[i] >= wxT('0') && cur[i] <= wxT('9')) {
580 ++i;
582 if (i > colon && cur[i] == wxT(':') ) {
583 colon = i;
584 // Check for column number.
585 while (++i < cur.size() - 2 &&
586 cur[i] >= wxT('0') && cur[i] <= wxT('9')) { }
587 bool have_column = (i > colon + 1 && cur[i] == wxT(':'));
588 if (have_column) {
589 colon = i;
590 } else {
591 // If there's no colon, include a trailing ':'
592 // so that we can unambiguously split the href
593 // value up into filename, line and column.
594 ++colon;
596 wxString tag = wxT("<a href=\"");
597 tag.append(cur, 0, colon);
598 while (cur[++i] == wxT(' ')) { }
599 tag += wxT("\" target=\"");
600 wxString target(cur, i, wxString::npos);
601 target.Replace(badutf8_html, badutf8);
602 tag += target;
603 tag += wxT("\">");
604 cur.insert(0, tag);
605 size_t offset = colon + tag.size();
606 cur.insert(offset, wxT("</a>"));
607 offset += 4 + 2;
609 if (!have_column) --offset;
611 static const wxString & error_marker = wmsg(/*error*/93) + ":";
612 static const wxString & warning_marker = wmsg(/*warning*/4) + ":";
614 if (cur.substr(offset, error_marker.size()) == error_marker) {
615 // Show "error" marker in red.
616 highlight = wxT("<span style=\"color:red\">");
617 cur.insert(offset, highlight);
618 offset += 24 + error_marker.size() - 1;
619 cur.insert(offset, wxT("</span>"));
620 } else if (cur.substr(offset, warning_marker.size()) == warning_marker) {
621 // Show "warning" marker in orange.
622 highlight = wxT("<span style=\"color:orange\">");
623 cur.insert(offset, highlight);
624 offset += 27 + warning_marker.size() - 1;
625 cur.insert(offset, wxT("</span>"));
626 } else {
627 highlight = NULL;
630 ++link_count;
634 // Save the scrollbar positions.
635 int scroll_x = 0, scroll_y = 0;
636 GetViewStart(&scroll_x, &scroll_y);
638 cur += wxT("<br>\n");
639 AppendToPage(cur);
641 if (!link_count) {
642 // Auto-scroll the window until we've reported a
643 // warning or error.
644 int x, y;
645 GetVirtualSize(&x, &y);
646 int xs, ys;
647 GetClientSize(&xs, &ys);
648 y -= ys;
649 int xu, yu;
650 GetScrollPixelsPerUnit(&xu, &yu);
651 Scroll(scroll_x, y / yu);
652 } else {
653 // Restore the scrollbar positions.
654 Scroll(scroll_x, scroll_y);
657 cur.clear();
658 break;
660 case '<':
661 cur += wxT("&lt;");
662 break;
663 case '>':
664 cur += wxT("&gt;");
665 break;
666 case '&':
667 cur += wxT("&amp;");
668 break;
669 case '"':
670 cur += wxT("&#22;");
671 continue;
672 default:
673 if (ch >= 128) {
674 cur += wxString::Format(wxT("&#%u;"), ch);
675 } else {
676 cur += (char)ch;
681 size_t left = end - p;
682 end = buf + left;
683 if (left) memmove(buf, p, left);
684 Update();
685 return;
688 if (e.len <= 0 && buf != end) {
689 // Truncated UTF-8 sequence.
690 cur += badutf8_html;
693 /* TRANSLATORS: Label for button in aven’s cavern log window which
694 * allows the user to save the log to a file. */
695 AppendToPage(wxString::Format(wxT("<avenbutton id=%d name=\"%s\">"),
696 (int)LOG_SAVE,
697 wmsg(/*Save Log*/446).c_str()));
698 wxEndBusyCursor();
699 delete cavern_out;
700 cavern_out = NULL;
701 if (e.len < 0) {
702 /* Negative length indicates non-zero exit status from cavern. */
703 /* TRANSLATORS: Label for button in aven’s cavern log window which
704 * causes the survey data to be reprocessed. */
705 AppendToPage(wxString::Format(wxT("<avenbutton default id=%d name=\"%s\">"),
706 (int)LOG_REPROCESS,
707 wmsg(/*Reprocess*/184).c_str()));
708 return;
710 AppendToPage(wxString::Format(wxT("<avenbutton id=%d name=\"%s\">"),
711 (int)LOG_REPROCESS,
712 wmsg(/*Reprocess*/184).c_str()));
713 AppendToPage(wxString::Format(wxT("<avenbutton default id=%d>"), (int)wxID_OK));
714 Update();
715 init_done = false;
718 wxString file3d(filename, 0, filename.length() - 3);
719 file3d.append(wxT("3d"));
720 if (!mainfrm->LoadData(file3d, survey)) {
721 return;
725 if (link_count == 0) {
726 wxCommandEvent dummy;
727 OnOK(dummy);
731 void
732 CavernLogWindow::OnEndProcess(wxProcessEvent & evt)
734 CavernOutputEvent * e = new CavernOutputEvent();
735 // Zero length indicates successful exit, negative length unsuccessful exit.
736 e->len = (evt.GetExitCode() == 0 ? 0 : -1);
737 QueueEvent(e);
740 void
741 CavernLogWindow::OnReprocess(wxCommandEvent &)
743 process(filename);
746 void
747 CavernLogWindow::OnSave(wxCommandEvent &)
749 wxString filelog(filename, 0, filename.length() - 3);
750 filelog += wxT("log");
751 AvenAllowOnTop ontop(mainfrm);
752 #ifdef __WXMOTIF__
753 wxString ext(wxT("*.log"));
754 #else
755 /* TRANSLATORS: Log files from running cavern (extension .log) */
756 wxString ext = wmsg(/*Log files*/447);
757 ext += wxT("|*.log");
758 #endif
759 wxFileDialog dlg(this, wmsg(/*Select an output filename*/319),
760 wxString(), filelog, ext,
761 wxFD_SAVE|wxFD_OVERWRITE_PROMPT);
762 if (dlg.ShowModal() != wxID_OK) return;
763 filelog = dlg.GetPath();
764 FILE * fh_log = wxFopen(filelog, wxT("w"));
765 if (!fh_log) {
766 wxGetApp().ReportError(wxString::Format(wmsg(/*Error writing to file “%s”*/110), filelog.c_str()));
767 return;
769 fwrite(log_txt.data(), log_txt.size(), 1, fh_log);
770 fclose(fh_log);
773 void
774 CavernLogWindow::OnOK(wxCommandEvent &)
776 if (init_done) {
777 mainfrm->HideLog(this);
778 } else {
779 mainfrm->InitialiseAfterLoad(filename, survey);
780 init_done = true;