2 * Run cavern inside an Aven window
4 * Copyright (C) 2005-2024 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
24 #include "cavernlog.h"
35 #include <sys/types.h>
38 #include <wx/process.h>
40 #define GVIM_COMMAND "gvim +'call cursor($l,$c)' $f"
41 #define VIM_COMMAND "x-terminal-emulator -e vim +'call cursor($l,$c)' $f"
42 #define NVIM_COMMAND "x-terminal-emulator -e nvim +'call cursor($l,$c)' $f"
43 #define GEDIT_COMMAND "gedit $f +$l:$c"
44 // Pluma currently ignores the column, but include it assuming some future
45 // version will add support.
46 #define PLUMA_COMMAND "pluma +$l:$c $f"
47 #define EMACS_COMMAND "x-terminal-emulator -e emacs +$l:$c $f"
48 #define NANO_COMMAND "x-terminal-emulator -e nano +$l,$c $f"
49 #define JED_COMMAND "x-terminal-emulator -e jed $f -g $l"
50 #define KATE_COMMAND "kate -l $l -c $c $f"
53 # define DEFAULT_EDITOR_COMMAND "notepad $f"
54 #elif defined __WXMAC__
55 # define DEFAULT_EDITOR_COMMAND "open -t $f"
57 # define DEFAULT_EDITOR_COMMAND VIM_COMMAND
60 enum { LOG_REPROCESS
= 1234, LOG_SAVE
= 1235 };
62 static const wxString
badutf8_html(
63 wxT("<span style=\"color:white;background-color:red;\">�</span>"));
64 static const wxString
badutf8(wxUniChar(0xfffd));
66 // New event type for signalling cavern output to process.
67 wxDEFINE_EVENT(EVT_CAVERN_OUTPUT
, wxCommandEvent
);
70 CavernLogWindow::CheckForOutput(bool immediate
)
73 if (cavern_out
== NULL
) return;
75 wxInputStream
* in
= cavern_out
->GetInputStream();
82 size_t real_size
= log_txt
.size();
84 log_txt
.resize(real_size
+ allow
);
85 in
->Read(&log_txt
[real_size
], allow
);
86 size_t n
= in
->LastRead();
87 log_txt
.resize(real_size
+ n
);
90 ProcessCavernOutput();
92 QueueEvent(new wxCommandEvent(EVT_CAVERN_OUTPUT
));
98 CavernLogWindow::OnPaintButton(wxButton
* b
, int x
)
102 const wxSize
& bsize
= b
->GetSize();
104 b
->SetSize(x
, 4, bsize
.x
, bsize
.y
);
111 CavernLogWindow::OnPaint(wxPaintEvent
&)
114 wxFont font
= dc
.GetFont();
115 wxFont bold_font
= font
.Bold();
116 wxFont underlined_font
= font
.Underlined();
117 const wxRegion
& region
= GetUpdateRegion();
118 const wxRect
& rect
= region
.GetBox();
119 int scroll_x
= 0, scroll_y
= 0;
120 GetViewStart(&scroll_x
, &scroll_y
);
121 int fsize
= dc
.GetFont().GetPixelSize().GetHeight();
122 int limit
= min((rect
.y
+ rect
.height
+ fsize
- 1) / fsize
+ scroll_y
, int(line_info
.size()) - 1);
123 for (int i
= max(rect
.y
/ fsize
, scroll_y
); i
<= limit
; ++i
) {
124 LineInfo
& info
= line_info
[i
];
125 // Leave a small margin to the left.
126 int x
= fsize
/ 2 - scroll_x
* fsize
;
127 int y
= (i
- scroll_y
) * fsize
;
128 unsigned offset
= info
.start_offset
;
129 unsigned len
= info
.len
;
131 dc
.SetFont(underlined_font
);
132 dc
.SetTextForeground(wxColour(192, 0, 192));
133 wxString link
= wxString::FromUTF8(&log_txt
[offset
], info
.link_len
);
134 offset
+= info
.link_len
;
135 len
-= info
.link_len
;
136 dc
.DrawText(link
, x
, y
);
137 x
+= info
.link_pixel_width
;
140 if (info
.colour_len
) {
141 dc
.SetTextForeground(*wxBLACK
);
143 size_t s_len
= info
.start_offset
+ info
.colour_start
- offset
;
144 wxString s
= wxString::FromUTF8(&log_txt
[offset
], s_len
);
147 dc
.DrawText(s
, x
, y
);
148 x
+= dc
.GetTextExtent(s
).GetWidth();
150 switch (info
.colour
) {
152 dc
.SetTextForeground(*wxRED
);
155 dc
.SetTextForeground(wxColour(0xf2, 0x8C, 0x28));
158 dc
.SetTextForeground(*wxBLUE
);
161 dc
.SetFont(bold_font
);
162 wxString d
= wxString::FromUTF8(&log_txt
[offset
], info
.colour_len
);
163 offset
+= info
.colour_len
;
164 len
-= info
.colour_len
;
165 dc
.DrawText(d
, x
, y
);
166 x
+= dc
.GetTextExtent(d
).GetWidth();
169 dc
.SetTextForeground(*wxBLACK
);
170 dc
.DrawText(wxString::FromUTF8(&log_txt
[offset
], len
), x
, y
);
172 int x
= GetClientSize().x
;
173 x
= OnPaintButton(ok_button
, x
);
174 x
= OnPaintButton(reprocess_button
, x
);
175 OnPaintButton(save_button
, x
);
178 BEGIN_EVENT_TABLE(CavernLogWindow
, wxScrolledWindow
)
179 EVT_BUTTON(LOG_REPROCESS
, CavernLogWindow::OnReprocess
)
180 EVT_BUTTON(LOG_SAVE
, CavernLogWindow::OnSave
)
181 EVT_BUTTON(wxID_OK
, CavernLogWindow::OnOK
)
182 EVT_COMMAND(wxID_ANY
, EVT_CAVERN_OUTPUT
, CavernLogWindow::OnCavernOutput
)
183 EVT_IDLE(CavernLogWindow::OnIdle
)
184 EVT_TIMER(wxID_ANY
, CavernLogWindow::OnTimer
)
185 EVT_PAINT(CavernLogWindow::OnPaint
)
186 EVT_MOTION(CavernLogWindow::OnMouseMove
)
187 EVT_LEFT_UP(CavernLogWindow::OnLinkClicked
)
188 EVT_END_PROCESS(wxID_ANY
, CavernLogWindow::OnEndProcess
)
191 wxString
escape_for_shell(wxString s
, bool protect_dash
)
194 // Correct quoting rules are insane:
196 // http://blogs.msdn.com/b/twistylittlepassagesallalike/archive/2011/04/23/everyone-quotes-arguments-the-wrong-way.aspx
198 // Thankfully wxExecute passes the command string to CreateProcess(), so
199 // at least we don't need to quote for cmd.exe too.
200 if (s
.empty() || s
.find_first_of(wxT(" \"\t\n\v")) != s
.npos
) {
202 s
.insert(0, wxT('"'));
203 for (size_t p
= 1; p
< s
.size(); ++p
) {
204 size_t backslashes
= 0;
205 while (s
[p
] == wxT('\\')) {
207 if (++p
== s
.size()) {
208 // Escape all the backslashes, since they're before
209 // the closing quote we add below.
210 s
.append(backslashes
, wxT('\\'));
215 if (s
[p
] == wxT('"')) {
216 // Escape any preceding backslashes and this quote.
217 s
.insert(p
, backslashes
+ 1, wxT('\\'));
218 p
+= backslashes
+ 1;
226 if (protect_dash
&& !s
.empty() && s
[0u] == '-') {
227 // If the filename starts with a '-', protect it from being
228 // treated as an option by prepending "./".
229 s
.insert(0, wxT("./"));
232 while (p
< s
.size()) {
233 // Exclude a few safe characters which are common in filenames
234 if (!isalnum((unsigned char)s
[p
]) && strchr("/._-", s
[p
]) == NULL
) {
235 s
.insert(p
, 1, wxT('\\'));
244 wxString
get_command_path(const wxChar
* command_name
)
253 buf
= (wchar_t*)osrealloc(buf
, len
* 2);
254 got
= GetModuleFileNameW(NULL
, buf
, len
);
255 if (got
< len
) break;
258 /* Strange Win32 nastiness - strip prefix "\\?\" if present */
259 wchar_t *start
= buf
;
260 if (wcsncmp(start
, L
"\\\\?\\", 4) == 0) start
+= 4;
261 wchar_t * slash
= wcsrchr(start
, L
'\\');
263 cmd
.assign(start
, slash
- start
+ 1);
268 wxString cmd
= wxString::FromUTF8(msg_exepth());
274 CavernLogWindow::CavernLogWindow(MainFrm
* mainfrm_
, const wxString
& survey_
, wxWindow
* parent
)
275 : wxScrolledWindow(parent
, wxID_ANY
, wxDefaultPosition
, wxDefaultSize
,
276 wxFULL_REPAINT_ON_RESIZE
),
283 CavernLogWindow::~CavernLogWindow()
288 cavern_out
->Detach();
293 CavernLogWindow::OnMouseMove(wxMouseEvent
& e
)
295 const auto& pos
= e
.GetPosition();
296 int fsize
= GetFont().GetPixelSize().GetHeight();
297 int scroll_x
= 0, scroll_y
= 0;
298 GetViewStart(&scroll_x
, &scroll_y
);
299 unsigned line
= pos
.y
/ fsize
+ scroll_y
;
300 unsigned x
= pos
.x
+ scroll_x
* fsize
- fsize
/ 2;
301 if (line
< line_info
.size() && x
<= line_info
[line
].link_pixel_width
) {
302 SetCursor(wxCursor(wxCURSOR_HAND
));
304 SetCursor(wxNullCursor
);
309 CavernLogWindow::OnLinkClicked(wxMouseEvent
& e
)
311 const auto& pos
= e
.GetPosition();
312 int fsize
= GetFont().GetPixelSize().GetHeight();
313 int scroll_x
= 0, scroll_y
= 0;
314 GetViewStart(&scroll_x
, &scroll_y
);
315 unsigned line
= pos
.y
/ fsize
+ scroll_y
;
316 unsigned x
= pos
.x
+ scroll_x
* fsize
- fsize
/ 2;
317 if (!(line
< line_info
.size() && x
<= line_info
[line
].link_pixel_width
))
320 const char* cur
= &log_txt
[line_info
[line
].start_offset
];
321 size_t link_len
= line_info
[line
].link_len
;
322 size_t colon
= link_len
;
323 while (colon
> 1 && (unsigned)(cur
[--colon
] - '0') <= 9) { }
324 size_t colon2
= colon
;
325 while (colon
> 1 && (unsigned)(cur
[--colon
] - '0') <= 9) { }
326 if (cur
[colon
] != ':') {
332 wxChar
* p
= wxGetenv(wxT("SURVEXEDITOR"));
335 if (!cmd
.find(wxT("$f"))) {
339 p
= wxGetenv(wxT("VISUAL"));
340 if (!p
) p
= wxGetenv(wxT("EDITOR"));
342 cmd
= wxT(DEFAULT_EDITOR_COMMAND
);
346 cmd
= wxT(GVIM_COMMAND
);
347 } else if (cmd
== "vim") {
348 cmd
= wxT(VIM_COMMAND
);
349 } else if (cmd
== "nvim") {
350 cmd
= wxT(NVIM_COMMAND
);
351 } else if (cmd
== "gedit") {
352 cmd
= wxT(GEDIT_COMMAND
);
353 } else if (cmd
== "pluma") {
354 cmd
= wxT(PLUMA_COMMAND
);
355 } else if (cmd
== "emacs") {
356 cmd
= wxT(EMACS_COMMAND
);
357 } else if (cmd
== "nano") {
358 cmd
= wxT(NANO_COMMAND
);
359 } else if (cmd
== "jed") {
360 cmd
= wxT(JED_COMMAND
);
361 } else if (cmd
== "kate") {
362 cmd
= wxT(KATE_COMMAND
);
365 cmd
.Replace(wxT("$"), wxT("$$"));
371 while ((i
= cmd
.find(wxT('$'), i
)) != wxString::npos
) {
372 if (++i
>= cmd
.size()) break;
373 switch ((int)cmd
[i
]) {
378 wxString f
= escape_for_shell(wxString(cur
, colon
), true);
379 cmd
.replace(i
- 1, 2, f
);
384 wxString l
= escape_for_shell(wxString(cur
+ colon
+ 1, colon2
- colon
- 1));
385 cmd
.replace(i
- 1, 2, l
);
391 if (colon2
== link_len
)
394 l
= escape_for_shell(wxString(cur
+ colon2
+ 1, link_len
- colon2
- 1));
395 cmd
.replace(i
- 1, 2, l
);
404 if (wxExecute(cmd
, wxEXEC_ASYNC
|wxEXEC_MAKE_GROUP_LEADER
) >= 0)
408 // TRANSLATORS: %s is replaced by the command we attempted to run.
409 m
.Printf(wmsg(/*Couldn’t run external command: “%s”*/17), cmd
.c_str());
411 m
+= wxString::FromUTF8(strerror(errno
));
413 wxGetApp().ReportError(m
);
417 CavernLogWindow::process(const wxString
&file
)
421 cavern_out
->Detach();
434 // Reserve enough that we won't need to grow the allocations in normal cases.
435 log_txt
.reserve(16384);
436 line_info
.reserve(256);
438 save_button
= nullptr;
439 reprocess_button
= nullptr;
442 SetVirtualSize(0, 0);
445 SetEnvironmentVariable(wxT("SURVEX_UTF8"), wxT("1"));
447 setenv("SURVEX_UTF8", "1", 1);
450 wxString escaped_file
= escape_for_shell(file
, true);
451 wxString cmd
= get_command_path(L
"cavern");
452 cmd
= escape_for_shell(cmd
, false);
458 cavern_out
= wxProcess::Open(cmd
);
461 m
.Printf(wmsg(/*Couldn’t run external command: “%s”*/17), cmd
.c_str());
463 m
+= wxString::FromUTF8(strerror(errno
));
465 wxGetApp().ReportError(m
);
469 // We want to receive the wxProcessEvent when cavern exits.
470 cavern_out
->SetNextHandler(this);
472 // Check for output after 500ms if we don't get an idle event sooner.
473 timer
.StartOnce(500);
477 CavernLogWindow::ProcessCavernOutput()
479 // ptr gives the start of the first line we've not yet processed.
482 while ((nl
= log_txt
.find('\n', ptr
)) != std::string::npos
) {
483 if (nl
== ptr
|| (nl
- ptr
== 1 && log_txt
[ptr
] == '\r')) {
484 // Don't show empty lines in the window.
488 size_t line_len
= nl
- ptr
- (log_txt
[nl
- 1] == '\r');
489 // FIXME: Avoid copy, use string_view?
490 string
cur(log_txt
, ptr
, line_len
);
491 if (log_txt
[ptr
] == ' ') {
492 if (expecting_caret_line
) {
493 // FIXME: Check the line is only space, `^` and `~`?
494 // Otherwise an error without caret info followed
495 // by an error which contains a '^' gets
497 size_t caret
= cur
.rfind('^');
498 if (caret
!= wxString::npos
) {
499 size_t tilde
= cur
.rfind('~');
500 if (tilde
== wxString::npos
|| tilde
< caret
) {
503 line_info
.back().colour
= line_info
[line_info
.size() - 2].colour
;
504 line_info
.back().colour_start
= caret
;
505 line_info
.back().colour_len
= tilde
- caret
+ 1;
506 expecting_caret_line
= false;
511 expecting_caret_line
= true;
513 line_info
.emplace_back(ptr
);
514 line_info
.back().len
= line_len
;
515 size_t colon
= cur
.find(": ");
516 if (colon
!= wxString::npos
) {
517 size_t link_len
= colon
;
518 while (colon
> 1 && (unsigned)(cur
[--colon
] - '0') <= 9) { }
519 if (cur
[colon
] == ':') {
520 line_info
.back().link_len
= link_len
;
522 static string info_marker
= string(msg(/*info*/485)) + ':';
523 static string warning_marker
= string(msg(/*warning*/4)) + ':';
524 static string error_marker
= string(msg(/*error*/93)) + ':';
526 size_t offset
= link_len
+ 2;
527 if (cur
.compare(offset
, info_marker
.size(), info_marker
) == 0) {
528 // Show "info" marker in blue.
530 line_info
.back().colour
= LOG_INFO
;
531 line_info
.back().colour_start
= offset
;
532 line_info
.back().colour_len
= info_marker
.size() - 1;
533 } else if (cur
.compare(offset
, warning_marker
.size(), warning_marker
) == 0) {
534 // Show "warning" marker in orange.
535 line_info
.back().colour
= LOG_WARNING
;
536 line_info
.back().colour_start
= offset
;
537 line_info
.back().colour_len
= warning_marker
.size() - 1;
538 } else if (cur
.compare(offset
, error_marker
.size(), error_marker
) == 0) {
539 // Show "error" marker in red.
540 line_info
.back().colour
= LOG_ERROR
;
541 line_info
.back().colour_start
= offset
;
542 line_info
.back().colour_len
= error_marker
.size() - 1;
548 int fsize
= GetFont().GetPixelSize().GetHeight();
549 SetScrollRate(fsize
, fsize
);
551 auto& info
= line_info
.back();
552 info
.link_pixel_width
= GetTextExtent(wxString(&log_txt
[ptr
], info
.link_len
)).GetWidth();
553 auto rest_pixel_width
= GetTextExtent(wxString(&log_txt
[ptr
+ info
.link_len
], info
.len
- info
.link_len
)).GetWidth();
554 int width
= max(GetVirtualSize().GetWidth(),
555 int(fsize
+ info
.link_pixel_width
+ rest_pixel_width
));
556 int height
= line_info
.size();
557 SetVirtualSize(width
, height
* fsize
);
559 // Auto-scroll until the first diagnostic.
560 int scroll_x
= 0, scroll_y
= 0;
561 GetViewStart(&scroll_x
, &scroll_y
);
563 GetClientSize(&xs
, &ys
);
564 Scroll(scroll_x
, line_info
.size() * fsize
- ys
);
571 CavernLogWindow::OnEndProcess(wxProcessEvent
& evt
)
573 bool cavern_success
= evt
.GetExitCode() == 0;
575 // Read and process any remaining buffered output.
576 wxInputStream
* in
= cavern_out
->GetInputStream();
578 CheckForOutput(true);
586 // Initially place buttons off the right of the window - they get moved to
587 // the desired position by OnPaintButton().
588 wxPoint
off_right(GetSize().x
, 0);
589 /* TRANSLATORS: Label for button in aven’s cavern log window which
590 * allows the user to save the log to a file. */
591 save_button
= new wxButton(this, LOG_SAVE
, wmsg(/*&Save Log*/446), off_right
);
593 /* TRANSLATORS: Label for button in aven’s cavern log window which
594 * causes the survey data to be reprocessed. */
595 reprocess_button
= new wxButton(this, LOG_REPROCESS
, wmsg(/*&Reprocess*/184), off_right
);
597 if (cavern_success
) {
598 ok_button
= new wxButton(this, wxID_OK
, wxString(), off_right
);
599 ok_button
->SetDefault();
603 if (!cavern_success
) {
610 wxString
file3d(filename
, 0, filename
.length() - 3);
611 file3d
.append(wxT("3d"));
612 if (!mainfrm
->LoadData(file3d
, survey
)) {
617 // Don't stay on log if there there are only "info" diagnostics.
618 if (link_count
== info_count
) {
619 wxCommandEvent dummy
;
625 CavernLogWindow::OnReprocess(wxCommandEvent
&)
631 CavernLogWindow::OnSave(wxCommandEvent
&)
633 wxString
filelog(filename
, 0, filename
.length() - 3);
635 // We need to consistently use `\` here.
636 filelog
.Replace("/", "\\");
638 filelog
+= wxT("log");
640 wxString
ext(wxT("*.log"));
642 /* TRANSLATORS: Log files from running cavern (extension .log) */
643 wxString ext
= wmsg(/*Log files*/447);
644 ext
+= wxT("|*.log");
646 wxFileDialog
dlg(this, wmsg(/*Select an output filename*/319),
647 wxString(), filelog
, ext
,
648 wxFD_SAVE
|wxFD_OVERWRITE_PROMPT
);
649 if (dlg
.ShowModal() != wxID_OK
) return;
650 filelog
= dlg
.GetPath();
651 FILE * fh_log
= wxFopen(filelog
, wxT("w"));
653 wxGetApp().ReportError(wxString::Format(wmsg(/*Error writing to file “%s”*/110), filelog
.c_str()));
656 fwrite(log_txt
.data(), log_txt
.size(), 1, fh_log
);
661 CavernLogWindow::OnOK(wxCommandEvent
&)
664 mainfrm
->HideLog(this);
666 mainfrm
->InitialiseAfterLoad(filename
, survey
);