xref: /haiku/src/apps/serialconnect/TermView.cpp (revision 3a6bc1cf655f552194bae298fadfdde896dad690)
1 /*
2  * Copyright 2012-2019, Adrien Destugues, pulkomandy@pulkomandy.tk
3  * Distributed under the terms of the MIT licence.
4  */
5 
6 
7 #include "TermView.h"
8 
9 #include <stdio.h>
10 
11 #include <Clipboard.h>
12 #include <Entry.h>
13 #include <File.h>
14 #include <Font.h>
15 #include <Layout.h>
16 #include <ScrollBar.h>
17 
18 #include "SerialApp.h"
19 #include "libvterm/src/vterm_internal.h"
20 
21 
22 struct ScrollBufferItem {
23 	int cols;
24 	VTermScreenCell cells[];
25 };
26 
27 
TermView()28 TermView::TermView()
29 	:
30 	BView("TermView", B_WILL_DRAW | B_FRAME_EVENTS)
31 {
32 	_Init();
33 }
34 
35 
TermView(BRect r)36 TermView::TermView(BRect r)
37 	:
38 	BView(r, "TermView", B_FOLLOW_ALL_SIDES, B_WILL_DRAW | B_FRAME_EVENTS)
39 {
40 	_Init();
41 }
42 
43 
~TermView()44 TermView::~TermView()
45 {
46 	vterm_free(fTerm);
47 }
48 
49 
50 void
AttachedToWindow()51 TermView::AttachedToWindow()
52 {
53 	BView::AttachedToWindow();
54 	MakeFocus();
55 }
56 
57 
58 void
Draw(BRect updateRect)59 TermView::Draw(BRect updateRect)
60 {
61 	VTermRect updatedChars = _PixelsToGlyphs(updateRect);
62 
63 	VTermPos pos;
64 	font_height height;
65 	GetFontHeight(&height);
66 
67 	VTermPos cursorPos;
68 	vterm_state_get_cursorpos(vterm_obtain_state(fTerm), &cursorPos);
69 
70 	for (pos.row = updatedChars.start_row; pos.row <= updatedChars.end_row;
71 			pos.row++) {
72 		int x = updatedChars.start_col * fFontWidth + kBorderSpacing;
73 		int y = pos.row * fFontHeight + (int)ceil(height.ascent)
74 			+ kBorderSpacing;
75 		MovePenTo(x, y);
76 
77 		BString string;
78 		VTermScreenCell cell;
79 		int width = 0;
80 		bool isCursor = false;
81 
82 		pos.col = updatedChars.start_col;
83 		_GetCell(pos, cell);
84 
85 		for (pos.col = updatedChars.start_col;
86 				pos.col <= updatedChars.end_col;) {
87 
88 			VTermScreenCell newCell;
89 			_GetCell(pos, newCell);
90 
91 			// We need to start a new extent if:
92 			// - The attributes change
93 			// - The colors change
94 			// - The end of line is reached
95 			// - The current cell is under the cursor
96 			// - The current cell is right of the cursor
97 			if (*(uint32_t*)&cell.attrs != *(uint32_t*)&newCell.attrs
98 				|| !vterm_color_equal(cell.fg, newCell.fg)
99 				|| !vterm_color_equal(cell.bg, newCell.bg)
100 				|| pos.col >= updatedChars.end_col
101 				|| (pos.col == cursorPos.col && pos.row == cursorPos.row)
102 				|| (pos.col == cursorPos.col + 1 && pos.row == cursorPos.row)) {
103 
104 				rgb_color foreground, background;
105 				foreground.red = cell.fg.red;
106 				foreground.green = cell.fg.green;
107 				foreground.blue = cell.fg.blue;
108 				foreground.alpha = 255;
109 				background.red = cell.bg.red;
110 				background.green = cell.bg.green;
111 				background.blue = cell.bg.blue;
112 				background.alpha = 255;
113 
114 				// Draw the cursor by swapping foreground and background colors
115 				if (isCursor ^ cell.attrs.reverse) {
116 					SetLowColor(foreground);
117 					SetViewColor(foreground);
118 					SetHighColor(background);
119 				} else {
120 					SetLowColor(background);
121 					SetViewColor(background);
122 					SetHighColor(foreground);
123 				}
124 
125 				FillRect(BRect(x, y - ceil(height.ascent) + 1,
126 					x + width * fFontWidth - 1,
127 					y + ceil(height.descent) + ceil(height.leading)),
128 					B_SOLID_LOW);
129 
130 				BFont font = be_fixed_font;
131 				if (cell.attrs.bold)
132 					font.SetFace(B_BOLD_FACE);
133 				if (cell.attrs.underline)
134 					font.SetFace(B_UNDERSCORE_FACE);
135 				if (cell.attrs.italic)
136 					font.SetFace(B_ITALIC_FACE);
137 				if (cell.attrs.blink) // FIXME make it actually blink
138 					font.SetFace(B_OUTLINED_FACE);
139 #if 0
140 				// FIXME B_NEGATIVE_FACE isn't actually implemented so we
141 				// instead swap the colors above
142 				if (cell.attrs.reverse)
143 					font.SetFace(B_NEGATIVE_FACE);
144 #endif
145 				if (cell.attrs.strike)
146 					font.SetFace(B_STRIKEOUT_FACE);
147 
148 				// TODO handle "font" (alternate fonts), dwl and dhl (double size)
149 
150 				SetFont(&font);
151 				DrawString(string);
152 				x += width * fFontWidth;
153 
154 				// Prepare for next cell
155 				cell = newCell;
156 				string = "";
157 				width = 0;
158 			}
159 
160 			if (pos.col == cursorPos.col && pos.row == cursorPos.row)
161 				isCursor = true;
162 			else
163 				isCursor = false;
164 
165 			if (newCell.chars[0] == 0) {
166 				string += " ";
167 				pos.col ++;
168 				width += 1;
169 			} else {
170 				char buffer[VTERM_MAX_CHARS_PER_CELL];
171 				wcstombs(buffer, (wchar_t*)newCell.chars,
172 					VTERM_MAX_CHARS_PER_CELL);
173 				string += buffer;
174 				width += newCell.width;
175 				pos.col += newCell.width;
176 			}
177 		}
178 	}
179 }
180 
181 
182 void
FrameResized(float width,float height)183 TermView::FrameResized(float width, float height)
184 {
185 	VTermRect newSize = _PixelsToGlyphs(BRect(0, 0, width - 2 * kBorderSpacing,
186 				height - 2 * kBorderSpacing));
187 	vterm_set_size(fTerm, newSize.end_row, newSize.end_col);
188 }
189 
190 
191 void
GetPreferredSize(float * width,float * height)192 TermView::GetPreferredSize(float* width, float* height)
193 {
194 	if (width != NULL)
195 		*width = kDefaultWidth * fFontWidth + 2 * kBorderSpacing - 1;
196 	if (height != NULL)
197 		*height = kDefaultHeight * fFontHeight + 2 * kBorderSpacing - 1;
198 }
199 
200 
201 void
KeyDown(const char * bytes,int32 numBytes)202 TermView::KeyDown(const char* bytes, int32 numBytes)
203 {
204 	// Translate some keys to more usual VT100 escape codes
205 	switch (bytes[0]) {
206 		case B_UP_ARROW:
207 			numBytes = 3;
208 			bytes = "\x1B[A";
209 			break;
210 		case B_DOWN_ARROW:
211 			numBytes = 3;
212 			bytes = "\x1B[B";
213 			break;
214 		case B_RIGHT_ARROW:
215 			numBytes = 3;
216 			bytes = "\x1B[C";
217 			break;
218 		case B_LEFT_ARROW:
219 			numBytes = 3;
220 			bytes = "\x1B[D";
221 			break;
222 		case B_BACKSPACE:
223 			numBytes = 1;
224 			bytes = "\x7F";
225 			break;
226 		case '\n':
227 			numBytes = fLineTerminator.Length();
228 			bytes = fLineTerminator.String();
229 			break;
230 	}
231 
232 	// Send the bytes to the serial port
233 	BMessage* keyEvent = new BMessage(kMsgDataWrite);
234 	keyEvent->AddData("data", B_RAW_TYPE, bytes, numBytes);
235 	be_app_messenger.SendMessage(keyEvent);
236 }
237 
238 
239 void
MouseDown(BPoint where)240 TermView::MouseDown(BPoint where)
241 {
242 	int32 buttons = B_PRIMARY_MOUSE_BUTTON;
243 	int32 clicks = 1;
244 	if (Looper() != NULL && Looper()->CurrentMessage() != NULL) {
245 		Looper()->CurrentMessage()->FindInt32("buttons", &buttons);
246 		Looper()->CurrentMessage()->FindInt32("clicks", &clicks);
247 	}
248 
249 	if (buttons == B_TERTIARY_MOUSE_BUTTON && clicks == 1)
250 		PasteFromClipboard();
251 }
252 
253 
254 void
MessageReceived(BMessage * message)255 TermView::MessageReceived(BMessage* message)
256 {
257 	switch (message->what)
258 	{
259 		case 'DATA':
260 		{
261 			entry_ref ref;
262 			if (message->FindRef("refs", &ref) == B_OK)
263 			{
264 				// The user just dropped a file on us
265 				// TODO send it by XMODEM or so
266 			}
267 			break;
268 		}
269 		default:
270 			BView::MessageReceived(message);
271 	}
272 }
273 
274 
275 void
SetLineTerminator(BString terminator)276 TermView::SetLineTerminator(BString terminator)
277 {
278 	fLineTerminator = terminator;
279 }
280 
281 
282 void
PushBytes(const char * bytes,size_t length)283 TermView::PushBytes(const char* bytes, size_t length)
284 {
285 	vterm_push_bytes(fTerm, bytes, length);
286 }
287 
288 
289 void
Clear()290 TermView::Clear()
291 {
292 	while (fScrollBuffer.ItemAt(0)) {
293 		free(fScrollBuffer.RemoveItem((int32)0));
294 	}
295 
296 	vterm_state_reset(vterm_obtain_state(fTerm), 1);
297 	vterm_screen_reset(fTermScreen, 1);
298 
299 	_UpdateScrollbar();
300 }
301 
302 
303 void
PasteFromClipboard()304 TermView::PasteFromClipboard()
305 {
306 	if (!be_clipboard->Lock())
307 		return;
308 
309 	BMessage* message = be_clipboard->Data();
310 
311 	const void *data;
312 	ssize_t size;
313 	if (message->FindData("text/plain", B_MIME_TYPE, &data, &size) == B_OK) {
314 		BMessage* keyEvent = new BMessage(kMsgDataWrite);
315 		keyEvent->AddData("data", B_RAW_TYPE, data, size);
316 		be_app_messenger.SendMessage(keyEvent);
317 	}
318 
319 	be_clipboard->Unlock();
320 }
321 
322 
323 // #pragma mark -
324 
325 
326 void
_Init()327 TermView::_Init()
328 {
329 	SetFont(be_fixed_font);
330 
331 	font_height height;
332 	GetFontHeight(&height);
333 	fFontHeight = (int)(ceilf(height.ascent) + ceilf(height.descent)
334 		+ ceilf(height.leading));
335 	fFontWidth = (int)be_fixed_font->StringWidth("X");
336 	fTerm = vterm_new(kDefaultHeight, kDefaultWidth);
337 
338 	fTermScreen = vterm_obtain_screen(fTerm);
339 	vterm_screen_set_callbacks(fTermScreen, &sScreenCallbacks, this);
340 	vterm_screen_reset(fTermScreen, 1);
341 
342 	vterm_parser_set_utf8(fTerm, 1);
343 
344 	VTermScreenCell cell;
345 	VTermPos firstPos;
346 	firstPos.row = 0;
347 	firstPos.col = 0;
348 	_GetCell(firstPos, cell);
349 
350 	rgb_color background;
351 	background.red = cell.bg.red;
352 	background.green = cell.bg.green;
353 	background.blue = cell.bg.blue;
354 	background.alpha = 255;
355 
356 	SetViewColor(background);
357 	SetLineTerminator("\n");
358 }
359 
360 
361 VTermRect
_PixelsToGlyphs(BRect pixels) const362 TermView::_PixelsToGlyphs(BRect pixels) const
363 {
364 	pixels.OffsetBy(-kBorderSpacing, -kBorderSpacing);
365 
366 	VTermRect rect;
367 	rect.start_col = (int)floor(pixels.left / fFontWidth);
368 	rect.end_col = (int)ceil(pixels.right / fFontWidth);
369 	rect.start_row = (int)floor(pixels.top / fFontHeight);
370 	rect.end_row = (int)ceil(pixels.bottom / fFontHeight);
371 #if 0
372 	printf(
373 		"TOP %d ch < %f px\n"
374 		"BTM %d ch < %f px\n"
375 		"LFT %d ch < %f px\n"
376 		"RGH %d ch < %f px\n",
377 		rect.start_row, pixels.top,
378 		rect.end_row, pixels.bottom,
379 		rect.start_col, pixels.left,
380 		rect.end_col, pixels.right
381 	);
382 #endif
383 	return rect;
384 }
385 
386 
_GlyphsToPixels(const VTermRect & glyphs) const387 BRect TermView::_GlyphsToPixels(const VTermRect& glyphs) const
388 {
389 	BRect rect;
390 	rect.top = glyphs.start_row * fFontHeight;
391 	rect.bottom = glyphs.end_row * fFontHeight;
392 	rect.left = glyphs.start_col * fFontWidth;
393 	rect.right = glyphs.end_col * fFontWidth;
394 
395 	rect.OffsetBy(kBorderSpacing, kBorderSpacing);
396 #if 0
397 	printf(
398 		"TOP %d ch > %f px (%f)\n"
399 		"BTM %d ch > %f px\n"
400 		"LFT %d ch > %f px (%f)\n"
401 		"RGH %d ch > %f px\n",
402 		glyphs.start_row, rect.top, fFontHeight,
403 		glyphs.end_row, rect.bottom,
404 		glyphs.start_col, rect.left, fFontWidth,
405 		glyphs.end_col, rect.right
406 	);
407 #endif
408 	return rect;
409 }
410 
411 
412 BRect
_GlyphsToPixels(const int width,const int height) const413 TermView::_GlyphsToPixels(const int width, const int height) const
414 {
415 	VTermRect rect;
416 	rect.start_row = 0;
417 	rect.start_col = 0;
418 	rect.end_row = height;
419 	rect.end_col = width;
420 	return _GlyphsToPixels(rect);
421 }
422 
423 
424 void
_GetCell(VTermPos pos,VTermScreenCell & cell)425 TermView::_GetCell(VTermPos pos, VTermScreenCell& cell)
426 {
427 	// First handle cells from the normal screen
428 	if (vterm_screen_get_cell(fTermScreen, pos, &cell) != 0)
429 		return;
430 
431 	// Try the scroll-back buffer
432 	if (pos.row < 0 && pos.col >= 0) {
433 		int offset = - pos.row - 1;
434 		ScrollBufferItem* line
435 			= (ScrollBufferItem*)fScrollBuffer.ItemAt(offset);
436 		if (line != NULL && pos.col < line->cols) {
437 			cell = line->cells[pos.col];
438 			return;
439 		}
440 	}
441 
442 	// All cells outside the used terminal area are drawn with the same
443 	// background color as the top-left one.
444 	// TODO should they use the attributes of the closest neighbor instead?
445 	VTermPos firstPos;
446 	firstPos.row = 0;
447 	firstPos.col = 0;
448 	vterm_screen_get_cell(fTermScreen, firstPos, &cell);
449 	cell.chars[0] = 0;
450 	cell.width = 1;
451 }
452 
453 
454 void
_Damage(VTermRect rect)455 TermView::_Damage(VTermRect rect)
456 {
457 	Invalidate(_GlyphsToPixels(rect));
458 }
459 
460 
461 void
_MoveCursor(VTermPos pos,VTermPos oldPos,int visible)462 TermView::_MoveCursor(VTermPos pos, VTermPos oldPos, int visible)
463 {
464 	VTermRect r;
465 
466 	// We need to erase the cursor from its old position
467 	r.start_row = oldPos.row;
468 	r.start_col = oldPos.col;
469 	r.end_col = oldPos.col + 1;
470 	r.end_row = oldPos.row + 1;
471 	Invalidate(_GlyphsToPixels(r));
472 
473 	// And we need to draw it at the new one
474 	r.start_row = pos.row;
475 	r.start_col = pos.col;
476 	r.end_col = pos.col + 1;
477 	r.end_row = pos.row + 1;
478 	Invalidate(_GlyphsToPixels(r));
479 }
480 
481 
482 void
_PushLine(int cols,const VTermScreenCell * cells)483 TermView::_PushLine(int cols, const VTermScreenCell* cells)
484 {
485 	ScrollBufferItem* item = (ScrollBufferItem*)malloc(sizeof(int)
486 		+ cols * sizeof(VTermScreenCell));
487 	item->cols = cols;
488 	memcpy(item->cells, cells, cols * sizeof(VTermScreenCell));
489 
490 	fScrollBuffer.AddItem(item, 0);
491 
492 	// Remove extra items if the scrollback gets too long
493 	free(fScrollBuffer.RemoveItem(kScrollBackSize));
494 
495 	_UpdateScrollbar();
496 }
497 
498 
499 void
_UpdateScrollbar()500 TermView::_UpdateScrollbar()
501 {
502 	int availableRows, availableCols;
503 	vterm_get_size(fTerm, &availableRows, &availableCols);
504 
505 	VTermRect dirty;
506 	dirty.start_col = 0;
507 	dirty.end_col = availableCols;
508 	dirty.end_row = 0;
509 	dirty.start_row = -fScrollBuffer.CountItems();
510 	// FIXME we should rather use CopyRect if possible, and only invalidate the
511 	// newly exposed area here.
512 	Invalidate(_GlyphsToPixels(dirty));
513 
514 	BScrollBar* scrollBar = ScrollBar(B_VERTICAL);
515 	if (scrollBar != NULL) {
516 		float range = (fScrollBuffer.CountItems() + availableRows)
517 			* fFontHeight;
518 		scrollBar->SetRange(availableRows * fFontHeight - range, 0.0f);
519 		// TODO we need to adjust this in FrameResized, as availableRows can
520 		// change
521 		scrollBar->SetProportion(availableRows * fFontHeight / range);
522 		scrollBar->SetSteps(fFontHeight, fFontHeight * 3);
523 	}
524 }
525 
526 
527 int
_PopLine(int cols,VTermScreenCell * cells)528 TermView::_PopLine(int cols, VTermScreenCell* cells)
529 {
530 	ScrollBufferItem* item =
531 		(ScrollBufferItem*)fScrollBuffer.RemoveItem((int32)0);
532 	if (item == NULL)
533 		return 0;
534 
535 	_UpdateScrollbar();
536 	if (item->cols >= cols) {
537 		memcpy(cells, item->cells, cols * sizeof(VTermScreenCell));
538 	} else {
539 		memcpy(cells, item->cells, item->cols * sizeof(VTermScreenCell));
540 		for (int i = item->cols; i < cols; i++)
541 			cells[i] = cells[i - 1];
542 	}
543 	free(item);
544 	return 1;
545 }
546 
547 
548 /* static */ int
_Damage(VTermRect rect,void * user)549 TermView::_Damage(VTermRect rect, void* user)
550 {
551 	TermView* view = (TermView*)user;
552 	view->_Damage(rect);
553 
554 	return 0;
555 }
556 
557 
558 /* static */ int
_MoveCursor(VTermPos pos,VTermPos oldPos,int visible,void * user)559 TermView::_MoveCursor(VTermPos pos, VTermPos oldPos, int visible, void* user)
560 {
561 	TermView* view = (TermView*)user;
562 	view->_MoveCursor(pos, oldPos, visible);
563 
564 	return 0;
565 }
566 
567 
568 /* static */ int
_PushLine(int cols,const VTermScreenCell * cells,void * user)569 TermView::_PushLine(int cols, const VTermScreenCell* cells, void* user)
570 {
571 	TermView* view = (TermView*)user;
572 	view->_PushLine(cols, cells);
573 
574 	return 0;
575 }
576 
577 
578 /* static */ int
_PopLine(int cols,VTermScreenCell * cells,void * user)579 TermView::_PopLine(int cols, VTermScreenCell* cells, void* user)
580 {
581 	TermView* view = (TermView*)user;
582 	return view->_PopLine(cols, cells);
583 }
584 
585 
586 const
587 VTermScreenCallbacks TermView::sScreenCallbacks = {
588 	&TermView::_Damage,
589 	/*.moverect =*/ NULL,
590 	&TermView::_MoveCursor,
591 	/*.settermprop =*/ NULL,
592 	/*.setmousefunc =*/ NULL,
593 	/*.bell =*/ NULL,
594 	/*.resize =*/ NULL,
595 	&TermView::_PushLine,
596 	&TermView::_PopLine,
597 };
598