xref: /haiku/src/apps/terminal/TermViewStates.cpp (revision 9f3bdf3d039430b5172c424def20ce5d9f7367d4)
1 /*
2  * Copyright 2001-2015, Haiku, Inc.
3  * Copyright 2003-2004 Kian Duffy, myob@users.sourceforge.net
4  * Parts Copyright 1998-1999 Kazuho Okui and Takashi Murai.
5  * All rights reserved. Distributed under the terms of the MIT license.
6  *
7  * Authors:
8  *		Stefano Ceccherini, stefano.ceccherini@gmail.com
9  *		Kian Duffy, myob@users.sourceforge.net
10  *		Y.Hayakawa, hida@sawada.riec.tohoku.ac.jp
11  *		Simon South, simon@simonsouth.net
12  *		Ingo Weinhold, ingo_weinhold@gmx.de
13  *		Clemens Zeidler, haiku@Clemens-Zeidler.de
14  *		Siarzhuk Zharski, zharik@gmx.li
15  */
16 
17 
18 #include "TermViewStates.h"
19 
20 #include <stdio.h>
21 #include <stdlib.h>
22 #include <sys/stat.h>
23 
24 #include <Catalog.h>
25 #include <Clipboard.h>
26 #include <Cursor.h>
27 #include <FindDirectory.h>
28 #include <LayoutBuilder.h>
29 #include <MessageRunner.h>
30 #include <Path.h>
31 #include <PopUpMenu.h>
32 #include <ScrollBar.h>
33 #include <UTF8.h>
34 #include <Window.h>
35 
36 #include <Array.h>
37 
38 #include "ActiveProcessInfo.h"
39 #include "Shell.h"
40 #include "TermConst.h"
41 #include "TerminalBuffer.h"
42 #include "VTkeymap.h"
43 #include "VTKeyTbl.h"
44 
45 
46 #undef B_TRANSLATION_CONTEXT
47 #define B_TRANSLATION_CONTEXT "Terminal TermView"
48 
49 
50 // selection granularity
51 enum {
52 	SELECT_CHARS,
53 	SELECT_WORDS,
54 	SELECT_LINES
55 };
56 
57 static const uint32 kAutoScroll = 'AScr';
58 
59 static const uint32 kMessageOpenLink = 'OLnk';
60 static const uint32 kMessageCopyLink = 'CLnk';
61 static const uint32 kMessageCopyAbsolutePath = 'CAbs';
62 static const uint32 kMessageMenuClosed = 'MClo';
63 
64 
65 static const char* const kKnownURLProtocols = "http:https:ftp:mailto";
66 
67 
68 // #pragma mark - State
69 
70 
71 TermView::State::State(TermView* view)
72 	:
73 	fView(view)
74 {
75 }
76 
77 
78 TermView::State::~State()
79 {
80 }
81 
82 
83 void
84 TermView::State::Entered()
85 {
86 }
87 
88 
89 void
90 TermView::State::Exited()
91 {
92 }
93 
94 
95 bool
96 TermView::State::MessageReceived(BMessage* message)
97 {
98 	return false;
99 }
100 
101 
102 void
103 TermView::State::ModifiersChanged(int32 oldModifiers, int32 modifiers)
104 {
105 }
106 
107 
108 void
109 TermView::State::KeyDown(const char* bytes, int32 numBytes)
110 {
111 }
112 
113 
114 void
115 TermView::State::MouseDown(BPoint where, int32 buttons, int32 modifiers)
116 {
117 }
118 
119 
120 void
121 TermView::State::MouseMoved(BPoint where, uint32 transit,
122 	const BMessage* message, int32 modifiers)
123 {
124 }
125 
126 
127 void
128 TermView::State::MouseUp(BPoint where, int32 buttons)
129 {
130 }
131 
132 
133 void
134 TermView::State::WindowActivated(bool active)
135 {
136 }
137 
138 
139 void
140 TermView::State::VisibleTextBufferChanged()
141 {
142 }
143 
144 
145 // #pragma mark - StandardBaseState
146 
147 
148 TermView::StandardBaseState::StandardBaseState(TermView* view)
149 	:
150 	State(view)
151 {
152 }
153 
154 
155 bool
156 TermView::StandardBaseState::_StandardMouseMoved(BPoint where, int32 modifiers)
157 {
158 	if (!fView->fReportAnyMouseEvent && !fView->fReportButtonMouseEvent)
159 		return false;
160 
161 	TermPos clickPos = fView->_ConvertToTerminal(where);
162 
163 	if (fView->fReportButtonMouseEvent || fView->fEnableExtendedMouseCoordinates) {
164 		if (fView->fPrevPos.x != clickPos.x
165 			|| fView->fPrevPos.y != clickPos.y) {
166 			fView->_SendMouseEvent(fView->fMouseButtons, modifiers,
167 				clickPos.x, clickPos.y, true);
168 		}
169 		fView->fPrevPos = clickPos;
170 	} else {
171 		fView->_SendMouseEvent(fView->fMouseButtons, modifiers, clickPos.x,
172 			clickPos.y, true);
173 	}
174 
175 	return true;
176 }
177 
178 
179 // #pragma mark - DefaultState
180 
181 
182 TermView::DefaultState::DefaultState(TermView* view)
183 	:
184 	StandardBaseState(view)
185 {
186 }
187 
188 
189 void
190 TermView::DefaultState::ModifiersChanged(int32 oldModifiers, int32 modifiers)
191 {
192 	_CheckEnterHyperLinkState(modifiers);
193 }
194 
195 
196 void
197 TermView::DefaultState::KeyDown(const char* bytes, int32 numBytes)
198 {
199 	int32 key;
200 	int32 mod;
201 	int32 rawChar;
202 	BMessage* currentMessage = fView->Looper()->CurrentMessage();
203 	if (currentMessage == NULL)
204 		return;
205 
206 	currentMessage->FindInt32("modifiers", &mod);
207 	currentMessage->FindInt32("key", &key);
208 	currentMessage->FindInt32("raw_char", &rawChar);
209 
210 	fView->_ActivateCursor(true);
211 
212 	// Handle the Option key when used as Meta
213 	if ((mod & B_LEFT_OPTION_KEY) != 0 && fView->fUseOptionAsMetaKey
214 		&& (fView->fInterpretMetaKey || fView->fMetaKeySendsEscape)) {
215 		const char* bytes;
216 		int8 numBytes;
217 
218 		// Determine the character produced by the same keypress without the
219 		// Option key
220 		mod &= B_SHIFT_KEY | B_CAPS_LOCK | B_CONTROL_KEY;
221 		const int32 (*keymapTable)[128] = (mod == 0)
222 			? NULL
223 			: fView->fKeymapTableForModifiers.Get(mod);
224 		if (keymapTable == NULL) {
225 			bytes = (const char*)&rawChar;
226 			numBytes = 1;
227 		} else {
228 			bytes = &fView->fKeymapChars[(*keymapTable)[key]];
229 			numBytes = *(bytes++);
230 		}
231 
232 		if (numBytes <= 0)
233 			return;
234 
235 		fView->_ScrollTo(0, true);
236 
237 		char outputBuffer[2];
238 		const char* toWrite = bytes;
239 
240 		if (fView->fMetaKeySendsEscape) {
241 			fView->fShell->Write("\e", 1);
242 		} else if (numBytes == 1) {
243 			char byte = *bytes | 0x80;
244 
245 			// The eighth bit has special meaning in UTF-8, so if that encoding
246 			// is in use recode the output (as xterm does)
247 			if (fView->fEncoding == M_UTF8) {
248 				outputBuffer[0] = 0xc0 | ((byte >> 6) & 0x03);
249 				outputBuffer[1] = 0x80 | (byte & 0x3f);
250 				numBytes = 2;
251 			} else {
252 				outputBuffer[0] = byte;
253 				numBytes = 1;
254 			}
255 			toWrite = outputBuffer;
256 		}
257 
258 		fView->fShell->Write(toWrite, numBytes);
259 		return;
260 	}
261 
262 	// handle multi-byte chars
263 	if (numBytes > 1) {
264 		if (fView->fEncoding != M_UTF8) {
265 			char destBuffer[16];
266 			int32 destLen = sizeof(destBuffer);
267 			int32 state = 0;
268 			convert_from_utf8(fView->fEncoding, bytes, &numBytes, destBuffer,
269 				&destLen, &state, '?');
270 			fView->_ScrollTo(0, true);
271 			fView->fShell->Write(destBuffer, destLen);
272 			return;
273 		}
274 
275 		fView->_ScrollTo(0, true);
276 		fView->fShell->Write(bytes, numBytes);
277 		return;
278 	}
279 
280 	// Terminal filters RET, ENTER, F1...F12, and ARROW key code.
281 	const char *toWrite = NULL;
282 
283 	switch (*bytes) {
284 		case B_RETURN:
285 			if (rawChar == B_RETURN)
286 				toWrite = "\r";
287 			break;
288 
289 		case B_DELETE:
290 			toWrite = DELETE_KEY_CODE;
291 			break;
292 
293 		case B_BACKSPACE:
294 			// Translate only the actual backspace key to the backspace
295 			// code. CTRL-H shall just be echoed.
296 			if (!((mod & B_CONTROL_KEY) && rawChar == 'h'))
297 				toWrite = BACKSPACE_KEY_CODE;
298 			break;
299 
300 		case B_LEFT_ARROW:
301 			if (rawChar == B_LEFT_ARROW) {
302 				if ((mod & B_SHIFT_KEY) != 0)
303 					toWrite = SHIFT_LEFT_ARROW_KEY_CODE;
304 				else if ((mod & B_CONTROL_KEY) != 0)
305 					toWrite = CTRL_LEFT_ARROW_KEY_CODE;
306 				else
307 					toWrite = LEFT_ARROW_KEY_CODE;
308 			}
309 			break;
310 
311 		case B_RIGHT_ARROW:
312 			if (rawChar == B_RIGHT_ARROW) {
313 				if ((mod & B_SHIFT_KEY) != 0)
314 					toWrite = SHIFT_RIGHT_ARROW_KEY_CODE;
315 				else if ((mod & B_CONTROL_KEY) != 0)
316 					toWrite = CTRL_RIGHT_ARROW_KEY_CODE;
317 				else
318 					toWrite = RIGHT_ARROW_KEY_CODE;
319 			}
320 			break;
321 
322 		case B_UP_ARROW:
323 			if ((mod & B_CONTROL_KEY) && (mod & B_SHIFT_KEY)) {
324 				fView->_ScrollTo(fView->fScrollOffset - fView->fFontHeight, true);
325 				return;
326 			}
327 
328 			if (rawChar == B_UP_ARROW) {
329 				if ((mod & B_SHIFT_KEY) != 0)
330 					toWrite = SHIFT_UP_ARROW_KEY_CODE;
331 				else if (mod & B_CONTROL_KEY)
332 					toWrite = CTRL_UP_ARROW_KEY_CODE;
333 				else
334 					toWrite = UP_ARROW_KEY_CODE;
335 			}
336 			break;
337 
338 		case B_DOWN_ARROW:
339 			if ((mod & B_CONTROL_KEY) && (mod & B_SHIFT_KEY)) {
340 				fView->_ScrollTo(fView->fScrollOffset + fView->fFontHeight, true);
341 				return;
342 			}
343 
344 			if (rawChar == B_DOWN_ARROW) {
345 				if ((mod & B_SHIFT_KEY) != 0)
346 					toWrite = SHIFT_DOWN_ARROW_KEY_CODE;
347 				else if (mod & B_CONTROL_KEY)
348 					toWrite = CTRL_DOWN_ARROW_KEY_CODE;
349 				else
350 					toWrite = DOWN_ARROW_KEY_CODE;
351 			}
352 			break;
353 
354 		case B_INSERT:
355 			if (rawChar == B_INSERT)
356 				toWrite = INSERT_KEY_CODE;
357 			break;
358 
359 		case B_HOME:
360 			if (rawChar == B_HOME) {
361 				if ((mod & B_SHIFT_KEY) != 0)
362 					toWrite = SHIFT_HOME_KEY_CODE;
363 				else
364 					toWrite = HOME_KEY_CODE;
365 			}
366 			break;
367 
368 		case B_END:
369 			if (rawChar == B_END) {
370 				if ((mod & B_SHIFT_KEY) != 0)
371 					toWrite = SHIFT_END_KEY_CODE;
372 				else
373 					toWrite = END_KEY_CODE;
374 			}
375 			break;
376 
377 		case B_PAGE_UP:
378 			if (mod & B_SHIFT_KEY) {
379 				fView->_ScrollTo(fView->fScrollOffset - fView->fFontHeight * fView->fRows, true);
380 				return;
381 			}
382 			if (rawChar == B_PAGE_UP)
383 				toWrite = PAGE_UP_KEY_CODE;
384 			break;
385 
386 		case B_PAGE_DOWN:
387 			if (mod & B_SHIFT_KEY) {
388 				fView->_ScrollTo(fView->fScrollOffset + fView->fFontHeight * fView->fRows, true);
389 				return;
390 			}
391 			if (rawChar == B_PAGE_DOWN)
392 				toWrite = PAGE_DOWN_KEY_CODE;
393 			break;
394 
395 		case B_FUNCTION_KEY:
396 			for (int32 i = 0; i < 12; i++) {
397 				if (key == function_keycode_table[i]) {
398 					toWrite = function_key_char_table[i];
399 					break;
400 				}
401 			}
402 			break;
403 	}
404 
405 	// If the above code proposed an alternative string to write, we get it's
406 	// length. Otherwise we write exactly the bytes passed to this method.
407 	size_t toWriteLen;
408 	if (toWrite != NULL) {
409 		toWriteLen = strlen(toWrite);
410 	} else {
411 		toWrite = bytes;
412 		toWriteLen = numBytes;
413 	}
414 
415 	fView->_ScrollTo(0, true);
416 	fView->fShell->Write(toWrite, toWriteLen);
417 }
418 
419 
420 void
421 TermView::DefaultState::MouseDown(BPoint where, int32 buttons, int32 modifiers)
422 {
423 	if (fView->fReportAnyMouseEvent || fView->fReportButtonMouseEvent
424 		|| fView->fReportNormalMouseEvent || fView->fReportX10MouseEvent) {
425 		TermPos clickPos = fView->_ConvertToTerminal(where);
426 		fView->_SendMouseEvent(buttons, modifiers, clickPos.x, clickPos.y,
427 			false, false);
428 		return;
429 	}
430 
431 	// paste button
432 	if ((buttons & (B_SECONDARY_MOUSE_BUTTON | B_TERTIARY_MOUSE_BUTTON)) != 0) {
433 		fView->Paste(fView->fMouseClipboard);
434 		return;
435 	}
436 
437 	// select region
438 	if (buttons == B_PRIMARY_MOUSE_BUTTON) {
439 		fView->fSelectState->Prepare(where, modifiers);
440 		fView->_NextState(fView->fSelectState);
441 	}
442 }
443 
444 
445 void
446 TermView::DefaultState::MouseMoved(BPoint where, uint32 transit,
447 	const BMessage* dragMessage, int32 modifiers)
448 {
449 	if (_CheckEnterHyperLinkState(modifiers))
450 		return;
451 
452 	_StandardMouseMoved(where, modifiers);
453 }
454 
455 
456 void
457 TermView::DefaultState::MouseUp(BPoint where, int32 buttons)
458 {
459 	if (fView->fReportAnyMouseEvent || fView->fReportButtonMouseEvent
460 		|| fView->fReportNormalMouseEvent || fView->fReportX10MouseEvent) {
461 		TermPos clickPos = fView->_ConvertToTerminal(where);
462 		fView->_SendMouseEvent(buttons, 0, clickPos.x, clickPos.y,
463 			false, true);
464 	}
465 }
466 
467 
468 void
469 TermView::DefaultState::WindowActivated(bool active)
470 {
471 	if (active)
472 		_CheckEnterHyperLinkState(fView->fModifiers);
473 }
474 
475 
476 bool
477 TermView::DefaultState::_CheckEnterHyperLinkState(int32 modifiers)
478 {
479 	if ((modifiers & B_COMMAND_KEY) != 0 && fView->Window()->IsActive()) {
480 		fView->_NextState(fView->fHyperLinkState);
481 		return true;
482 	}
483 
484 	return false;
485 }
486 
487 
488 // #pragma mark - SelectState
489 
490 
491 TermView::SelectState::SelectState(TermView* view)
492 	:
493 	StandardBaseState(view),
494 	fSelectGranularity(SELECT_CHARS),
495 	fCheckMouseTracking(false),
496 	fMouseTracking(false)
497 {
498 }
499 
500 
501 void
502 TermView::SelectState::Prepare(BPoint where, int32 modifiers)
503 {
504 	int32 clicks;
505 	fView->Window()->CurrentMessage()->FindInt32("clicks", &clicks);
506 
507 	if (fView->_HasSelection()) {
508 		TermPos inPos = fView->_ConvertToTerminal(where);
509 		if (fView->fSelection.RangeContains(inPos)) {
510 			if (modifiers & B_CONTROL_KEY) {
511 				BPoint p;
512 				uint32 bt;
513 				do {
514 					fView->GetMouse(&p, &bt);
515 
516 					if (bt == 0) {
517 						fView->_Deselect();
518 						return;
519 					}
520 
521 					snooze(40000);
522 
523 				} while (abs((int)(where.x - p.x)) < 4
524 					&& abs((int)(where.y - p.y)) < 4);
525 
526 				fView->InitiateDrag();
527 				return;
528 			}
529 		}
530 	}
531 
532 	// If mouse has moved too much, disable double/triple click.
533 	if (fView->_MouseDistanceSinceLastClick(where) > 8)
534 		clicks = 1;
535 
536 	fView->SetMouseEventMask(B_POINTER_EVENTS | B_KEYBOARD_EVENTS,
537 		B_NO_POINTER_HISTORY | B_LOCK_WINDOW_FOCUS);
538 
539 	TermPos clickPos = fView->_ConvertToTerminal(where);
540 
541 	if (modifiers & B_SHIFT_KEY) {
542 		fView->fInitialSelectionStart = clickPos;
543 		fView->fInitialSelectionEnd = clickPos;
544 		fView->_ExtendSelection(fView->fInitialSelectionStart, true, false);
545 	} else {
546 		fView->_Deselect();
547 		fView->fInitialSelectionStart = clickPos;
548 		fView->fInitialSelectionEnd = clickPos;
549 	}
550 
551 	// If clicks larger than 3, reset mouse click counter.
552 	clicks = (clicks - 1) % 3 + 1;
553 
554 	switch (clicks) {
555 		case 1:
556 			fCheckMouseTracking = true;
557 			fSelectGranularity = SELECT_CHARS;
558 			break;
559 
560 		case 2:
561 			fView->_SelectWord(where, (modifiers & B_SHIFT_KEY) != 0, false);
562 			fMouseTracking = true;
563 			fSelectGranularity = SELECT_WORDS;
564 			break;
565 
566 		case 3:
567 			fView->_SelectLine(where, (modifiers & B_SHIFT_KEY) != 0, false);
568 			fMouseTracking = true;
569 			fSelectGranularity = SELECT_LINES;
570 			break;
571 	}
572 }
573 
574 
575 bool
576 TermView::SelectState::MessageReceived(BMessage* message)
577 {
578 	if (message->what == kAutoScroll) {
579 		_AutoScrollUpdate();
580 		return true;
581 	}
582 
583 	return false;
584 }
585 
586 
587 void
588 TermView::SelectState::MouseMoved(BPoint where, uint32 transit,
589 	const BMessage* message, int32 modifiers)
590 {
591 	if (_StandardMouseMoved(where, modifiers))
592 		return;
593 
594 	if (fCheckMouseTracking) {
595 		if (fView->_MouseDistanceSinceLastClick(where) > 9)
596 			fMouseTracking = true;
597 	}
598 	if (!fMouseTracking)
599 		return;
600 
601 	bool doAutoScroll = false;
602 
603 	if (where.y < 0) {
604 		doAutoScroll = true;
605 		fView->fAutoScrollSpeed = where.y;
606 		where.x = 0;
607 		where.y = 0;
608 	}
609 
610 	BRect bounds(fView->Bounds());
611 	if (where.y > bounds.bottom) {
612 		doAutoScroll = true;
613 		fView->fAutoScrollSpeed = where.y - bounds.bottom;
614 		where.x = bounds.right;
615 		where.y = bounds.bottom;
616 	}
617 
618 	if (doAutoScroll) {
619 		if (fView->fAutoScrollRunner == NULL) {
620 			BMessage message(kAutoScroll);
621 			fView->fAutoScrollRunner = new (std::nothrow) BMessageRunner(
622 				BMessenger(fView), &message, 10000);
623 		}
624 	} else {
625 		delete fView->fAutoScrollRunner;
626 		fView->fAutoScrollRunner = NULL;
627 	}
628 
629 	switch (fSelectGranularity) {
630 		case SELECT_CHARS:
631 		{
632 			// If we just start selecting, we first select the initially
633 			// hit char, so that we get a proper initial selection -- the char
634 			// in question, which will thus always be selected, regardless of
635 			// whether selecting forward or backward.
636 			if (fView->fInitialSelectionStart == fView->fInitialSelectionEnd) {
637 				fView->_Select(fView->fInitialSelectionStart,
638 					fView->fInitialSelectionEnd, true, true);
639 			}
640 
641 			fView->_ExtendSelection(fView->_ConvertToTerminal(where), true,
642 				true);
643 			break;
644 		}
645 		case SELECT_WORDS:
646 			fView->_SelectWord(where, true, true);
647 			break;
648 		case SELECT_LINES:
649 			fView->_SelectLine(where, true, true);
650 			break;
651 	}
652 }
653 
654 
655 void
656 TermView::SelectState::MouseUp(BPoint where, int32 buttons)
657 {
658 	fCheckMouseTracking = false;
659 	fMouseTracking = false;
660 
661 	if (fView->fAutoScrollRunner != NULL) {
662 		delete fView->fAutoScrollRunner;
663 		fView->fAutoScrollRunner = NULL;
664 	}
665 
666 	// When releasing the first mouse button, we copy the selected text to the
667 	// clipboard.
668 
669 	if (fView->fReportAnyMouseEvent || fView->fReportButtonMouseEvent
670 		|| fView->fReportNormalMouseEvent) {
671 		TermPos clickPos = fView->_ConvertToTerminal(where);
672 		fView->_SendMouseEvent(0, 0, clickPos.x, clickPos.y, false);
673 	} else if ((buttons & B_PRIMARY_MOUSE_BUTTON) == 0
674 		&& (fView->fMouseButtons & B_PRIMARY_MOUSE_BUTTON) != 0) {
675 		fView->Copy(fView->fMouseClipboard);
676 	}
677 
678 	fView->_NextState(fView->fDefaultState);
679 }
680 
681 
682 void
683 TermView::SelectState::_AutoScrollUpdate()
684 {
685 	if (fMouseTracking && fView->fAutoScrollRunner != NULL
686 		&& fView->fScrollBar != NULL) {
687 		float value = fView->fScrollBar->Value();
688 		fView->_ScrollTo(value + fView->fAutoScrollSpeed, true);
689 		if (fView->fAutoScrollSpeed < 0) {
690 			fView->_ExtendSelection(
691 				fView->_ConvertToTerminal(BPoint(0, 0)), true, true);
692 		} else {
693 			fView->_ExtendSelection(
694 				fView->_ConvertToTerminal(fView->Bounds().RightBottom()), true,
695 				true);
696 		}
697 	}
698 }
699 
700 
701 // #pragma mark - HyperLinkState
702 
703 
704 TermView::HyperLinkState::HyperLinkState(TermView* view)
705 	:
706 	State(view),
707 	fURLCharClassifier(kURLAdditionalWordCharacters),
708 	fPathComponentCharClassifier(
709 		BString(kDefaultAdditionalWordCharacters).RemoveFirst("/")),
710 	fCurrentDirectory(),
711 	fHighlight(),
712 	fHighlightActive(false)
713 {
714 	fHighlight.SetHighlighter(this);
715 }
716 
717 
718 void
719 TermView::HyperLinkState::Entered()
720 {
721 	ActiveProcessInfo activeProcessInfo;
722 	if (fView->GetActiveProcessInfo(activeProcessInfo))
723 		fCurrentDirectory = activeProcessInfo.CurrentDirectory();
724 	else
725 		fCurrentDirectory.Truncate(0);
726 
727 	_UpdateHighlight();
728 }
729 
730 
731 void
732 TermView::HyperLinkState::Exited()
733 {
734 	_DeactivateHighlight();
735 }
736 
737 
738 void
739 TermView::HyperLinkState::ModifiersChanged(int32 oldModifiers, int32 modifiers)
740 {
741 	if ((modifiers & B_COMMAND_KEY) == 0)
742 		fView->_NextState(fView->fDefaultState);
743 	else
744 		_UpdateHighlight();
745 }
746 
747 
748 void
749 TermView::HyperLinkState::MouseDown(BPoint where, int32 buttons,
750 	int32 modifiers)
751 {
752 	TermPos start;
753 	TermPos end;
754 	HyperLink link;
755 
756 	bool pathPrefixOnly = (modifiers & B_SHIFT_KEY) != 0;
757 	if (!_GetHyperLinkAt(where, pathPrefixOnly, link, start, end))
758 		return;
759 
760 	if ((buttons & B_PRIMARY_MOUSE_BUTTON) != 0) {
761 		link.Open();
762 	} else if ((buttons & B_SECONDARY_MOUSE_BUTTON) != 0) {
763 		fView->fHyperLinkMenuState->Prepare(where, link);
764 		fView->_NextState(fView->fHyperLinkMenuState);
765 	}
766 }
767 
768 
769 void
770 TermView::HyperLinkState::MouseMoved(BPoint where, uint32 transit,
771 	const BMessage* message, int32 modifiers)
772 {
773 	_UpdateHighlight(where, modifiers);
774 }
775 
776 
777 void
778 TermView::HyperLinkState::WindowActivated(bool active)
779 {
780 	if (!active)
781 		fView->_NextState(fView->fDefaultState);
782 }
783 
784 
785 void
786 TermView::HyperLinkState::VisibleTextBufferChanged()
787 {
788 	_UpdateHighlight();
789 }
790 
791 
792 rgb_color
793 TermView::HyperLinkState::ForegroundColor()
794 {
795 	return make_color(0, 0, 255);
796 }
797 
798 
799 rgb_color
800 TermView::HyperLinkState::BackgroundColor()
801 {
802 	return fView->fTextBackColor;
803 }
804 
805 
806 uint32
807 TermView::HyperLinkState::AdjustTextAttributes(uint32 attributes)
808 {
809 	return attributes | UNDERLINE;
810 }
811 
812 
813 bool
814 TermView::HyperLinkState::_GetHyperLinkAt(BPoint where, bool pathPrefixOnly,
815 	HyperLink& _link, TermPos& _start, TermPos& _end)
816 {
817 	TerminalBuffer* textBuffer = fView->fTextBuffer;
818 	BAutolock textBufferLocker(textBuffer);
819 
820 	TermPos pos = fView->_ConvertToTerminal(where);
821 
822 	// try to get a URL first
823 	BString text;
824 	if (!textBuffer->FindWord(pos, &fURLCharClassifier, false, _start, _end))
825 		return false;
826 
827 	text.Truncate(0);
828 	textBuffer->GetStringFromRegion(text, _start, _end);
829 	text.Trim();
830 
831 	// We're only happy, if it has a protocol part which we know.
832 	int32 colonIndex = text.FindFirst(':');
833 	if (colonIndex >= 0) {
834 		BString protocol(text, colonIndex);
835 		if (strstr(kKnownURLProtocols, protocol) != NULL) {
836 			_link = HyperLink(text, HyperLink::TYPE_URL);
837 			return true;
838 		}
839 	}
840 
841 	// no obvious URL -- try file name
842 	if (!textBuffer->FindWord(pos, fView->fCharClassifier, false, _start, _end))
843 		return false;
844 
845 	// In path-prefix-only mode we determine the end position anew by omitting
846 	// the '/' in the allowed word chars.
847 	if (pathPrefixOnly) {
848 		TermPos componentStart;
849 		TermPos componentEnd;
850 		if (textBuffer->FindWord(pos, &fPathComponentCharClassifier, false,
851 				componentStart, componentEnd)) {
852 			_end = componentEnd;
853 		} else {
854 			// That means pos points to a '/'. We simply use the previous
855 			// position.
856 			_end = pos;
857 			if (_start == _end) {
858 				// Well, must be just "/". Advance to the next position.
859 				if (!textBuffer->NextLinePos(_end, false))
860 					return false;
861 			}
862 		}
863 	}
864 
865 	text.Truncate(0);
866 	textBuffer->GetStringFromRegion(text, _start, _end);
867 	text.Trim();
868 	if (text.IsEmpty())
869 		return false;
870 
871 	// Collect a list of colons in the string and their respective positions in
872 	// the text buffer. We do this up-front so we can unlock the text buffer
873 	// while we're doing all the entry existence tests.
874 	typedef Array<CharPosition> ColonList;
875 	ColonList colonPositions;
876 	TermPos searchPos = _start;
877 	for (int32 index = 0; (index = text.FindFirst(':', index)) >= 0;) {
878 		TermPos foundStart;
879 		TermPos foundEnd;
880 		if (!textBuffer->Find(":", searchPos, true, true, false, foundStart,
881 				foundEnd)) {
882 			return false;
883 		}
884 
885 		CharPosition colonPosition;
886 		colonPosition.index = index;
887 		colonPosition.position = foundStart;
888 		if (!colonPositions.Add(colonPosition))
889 			return false;
890 
891 		index++;
892 		searchPos = foundEnd;
893 	}
894 
895 	textBufferLocker.Unlock();
896 
897 	// Since we also want to consider ':' a potential path delimiter, in two
898 	// nested loops we chop off components from the beginning respective the
899 	// end.
900 	BString originalText = text;
901 	TermPos originalStart = _start;
902 	TermPos originalEnd = _end;
903 
904 	int32 colonCount = colonPositions.Count();
905 	for (int32 startColonIndex = -1; startColonIndex < colonCount;
906 			startColonIndex++) {
907 		int32 startIndex;
908 		if (startColonIndex < 0) {
909 			startIndex = 0;
910 			_start = originalStart;
911 		} else {
912 			startIndex = colonPositions[startColonIndex].index + 1;
913 			_start = colonPositions[startColonIndex].position;
914 			if (_start >= pos)
915 				break;
916 			_start.x++;
917 				// Note: This is potentially a non-normalized position (i.e.
918 				// the end of a soft-wrapped line). While not that nice, it
919 				// works anyway.
920 		}
921 
922 		for (int32 endColonIndex = colonCount; endColonIndex > startColonIndex;
923 				endColonIndex--) {
924 			int32 endIndex;
925 			if (endColonIndex == colonCount) {
926 				endIndex = originalText.Length();
927 				_end = originalEnd;
928 			} else {
929 				endIndex = colonPositions[endColonIndex].index;
930 				_end = colonPositions[endColonIndex].position;
931 				if (_end <= pos)
932 					break;
933 			}
934 
935 			originalText.CopyInto(text, startIndex, endIndex - startIndex);
936 			if (text.IsEmpty())
937 				continue;
938 
939 			// check, whether the file exists
940 			BString actualPath;
941 			if (_EntryExists(text, actualPath)) {
942 				_link = HyperLink(text, actualPath, HyperLink::TYPE_PATH);
943 				return true;
944 			}
945 
946 			// As such this isn't an existing path. We also want to recognize:
947 			// * "<path>:<line>"
948 			// * "<path>:<line>:<column>"
949 
950 			BString path = text;
951 
952 			for (int32 i = 0; i < 2; i++) {
953 				int32 colonIndex = path.FindLast(':');
954 				if (colonIndex <= 0 || colonIndex == path.Length() - 1)
955 					break;
956 
957 				char* numberEnd;
958 				strtol(path.String() + colonIndex + 1, &numberEnd, 0);
959 				if (*numberEnd != '\0')
960 					break;
961 
962 				path.Truncate(colonIndex);
963 				if (_EntryExists(path, actualPath)) {
964 					BString address = path == actualPath
965 						? text
966 						: BString(actualPath) << (text.String() + colonIndex);
967 					_link = HyperLink(text, address,
968 						i == 0
969 							? HyperLink::TYPE_PATH_WITH_LINE
970 							: HyperLink::TYPE_PATH_WITH_LINE_AND_COLUMN);
971 					return true;
972 				}
973 			}
974 		}
975 	}
976 
977 	return false;
978 }
979 
980 
981 bool
982 TermView::HyperLinkState::_EntryExists(const BString& path,
983 	BString& _actualPath) const
984 {
985 	if (path.IsEmpty())
986 		return false;
987 
988 	if (path[0] == '/' || fCurrentDirectory.IsEmpty()) {
989 		_actualPath = path;
990 	} else if (path == "~" || path.StartsWith("~/")) {
991 		// Replace '~' with the user's home directory. We don't handle "~user"
992 		// here yet.
993 		BPath homeDirectory;
994 		if (find_directory(B_USER_DIRECTORY, &homeDirectory) != B_OK)
995 			return false;
996 		_actualPath = homeDirectory.Path();
997 		_actualPath << path.String() + 1;
998 	} else {
999 		_actualPath.Truncate(0);
1000 		_actualPath << fCurrentDirectory << '/' << path;
1001 	}
1002 
1003 	struct stat st;
1004 	return lstat(_actualPath, &st) == 0;
1005 }
1006 
1007 
1008 void
1009 TermView::HyperLinkState::_UpdateHighlight()
1010 {
1011 	BPoint where;
1012 	uint32 buttons;
1013 	fView->GetMouse(&where, &buttons, false);
1014 	_UpdateHighlight(where, fView->fModifiers);
1015 }
1016 
1017 
1018 void
1019 TermView::HyperLinkState::_UpdateHighlight(BPoint where, int32 modifiers)
1020 {
1021 	TermPos start;
1022 	TermPos end;
1023 	HyperLink link;
1024 
1025 	bool pathPrefixOnly = (modifiers & B_SHIFT_KEY) != 0;
1026 	if (_GetHyperLinkAt(where, pathPrefixOnly, link, start, end))
1027 		_ActivateHighlight(start, end);
1028 	else
1029 		_DeactivateHighlight();
1030 }
1031 
1032 
1033 void
1034 TermView::HyperLinkState::_ActivateHighlight(const TermPos& start,
1035 	const TermPos& end)
1036 {
1037 	if (fHighlightActive) {
1038 		if (fHighlight.Start() == start && fHighlight.End() == end)
1039 			return;
1040 
1041 		_DeactivateHighlight();
1042 	}
1043 
1044 	fHighlight.SetRange(start, end);
1045 	fView->_AddHighlight(&fHighlight);
1046 	BCursor cursor(B_CURSOR_ID_FOLLOW_LINK);
1047 	fView->SetViewCursor(&cursor);
1048 	fHighlightActive = true;
1049 }
1050 
1051 
1052 void
1053 TermView::HyperLinkState::_DeactivateHighlight()
1054 {
1055 	if (fHighlightActive) {
1056 		fView->_RemoveHighlight(&fHighlight);
1057 		BCursor cursor(B_CURSOR_ID_SYSTEM_DEFAULT);
1058 		fView->SetViewCursor(&cursor);
1059 		fHighlightActive = false;
1060 	}
1061 }
1062 
1063 
1064 // #pragma mark - HyperLinkMenuState
1065 
1066 
1067 class TermView::HyperLinkMenuState::PopUpMenu : public BPopUpMenu {
1068 public:
1069 	PopUpMenu(const BMessenger& messageTarget)
1070 		:
1071 		BPopUpMenu("open hyperlink"),
1072 		fMessageTarget(messageTarget)
1073 	{
1074 		SetAsyncAutoDestruct(true);
1075 	}
1076 
1077 	~PopUpMenu()
1078 	{
1079 		fMessageTarget.SendMessage(kMessageMenuClosed);
1080 	}
1081 
1082 private:
1083 	BMessenger	fMessageTarget;
1084 };
1085 
1086 
1087 TermView::HyperLinkMenuState::HyperLinkMenuState(TermView* view)
1088 	:
1089 	State(view),
1090 	fLink()
1091 {
1092 }
1093 
1094 
1095 void
1096 TermView::HyperLinkMenuState::Prepare(BPoint point, const HyperLink& link)
1097 {
1098 	fLink = link;
1099 
1100 	// open context menu
1101 	PopUpMenu* menu = new PopUpMenu(fView);
1102 	BLayoutBuilder::Menu<> menuBuilder(menu);
1103 	switch (link.GetType()) {
1104 		case HyperLink::TYPE_URL:
1105 			menuBuilder
1106 				.AddItem(B_TRANSLATE("Open link"), kMessageOpenLink)
1107 				.AddItem(B_TRANSLATE("Copy link location"), kMessageCopyLink);
1108 			break;
1109 
1110 		case HyperLink::TYPE_PATH:
1111 		case HyperLink::TYPE_PATH_WITH_LINE:
1112 		case HyperLink::TYPE_PATH_WITH_LINE_AND_COLUMN:
1113 			menuBuilder.AddItem(B_TRANSLATE("Open path"), kMessageOpenLink);
1114 			menuBuilder.AddItem(B_TRANSLATE("Copy path"), kMessageCopyLink);
1115 			if (fLink.Text() != fLink.Address()) {
1116 				menuBuilder.AddItem(B_TRANSLATE("Copy absolute path"),
1117 					kMessageCopyAbsolutePath);
1118 			}
1119 			break;
1120 	}
1121 	menu->SetTargetForItems(fView);
1122 	menu->Go(fView->ConvertToScreen(point), true, true, true);
1123 }
1124 
1125 
1126 void
1127 TermView::HyperLinkMenuState::Exited()
1128 {
1129 	fLink = HyperLink();
1130 }
1131 
1132 
1133 bool
1134 TermView::HyperLinkMenuState::MessageReceived(BMessage* message)
1135 {
1136 	switch (message->what) {
1137 		case kMessageOpenLink:
1138 			if (fLink.IsValid())
1139 				fLink.Open();
1140 			return true;
1141 
1142 		case kMessageCopyLink:
1143 		case kMessageCopyAbsolutePath:
1144 		{
1145 			if (fLink.IsValid()) {
1146 				BString toCopy = message->what == kMessageCopyLink
1147 					? fLink.Text() : fLink.Address();
1148 
1149 				if (!be_clipboard->Lock())
1150 					return true;
1151 
1152 				be_clipboard->Clear();
1153 
1154 				if (BMessage *data = be_clipboard->Data()) {
1155 					data->AddData("text/plain", B_MIME_TYPE, toCopy.String(),
1156 						toCopy.Length());
1157 					be_clipboard->Commit();
1158 				}
1159 
1160 				be_clipboard->Unlock();
1161 			}
1162 			return true;
1163 		}
1164 
1165 		case kMessageMenuClosed:
1166 			fView->_NextState(fView->fDefaultState);
1167 			return true;
1168 	}
1169 
1170 	return false;
1171 }
1172