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
22struct ScrollBufferItem {
23	int cols;
24	VTermScreenCell cells[];
25};
26
27
28TermView::TermView()
29	:
30	BView("TermView", B_WILL_DRAW | B_FRAME_EVENTS)
31{
32	_Init();
33}
34
35
36TermView::TermView(BRect r)
37	:
38	BView(r, "TermView", B_FOLLOW_ALL_SIDES, B_WILL_DRAW | B_FRAME_EVENTS)
39{
40	_Init();
41}
42
43
44TermView::~TermView()
45{
46	vterm_free(fTerm);
47}
48
49
50void
51TermView::AttachedToWindow()
52{
53	BView::AttachedToWindow();
54	MakeFocus();
55}
56
57
58void
59TermView::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
182void
183TermView::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
191void
192TermView::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
201void
202TermView::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
239void
240TermView::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
254void
255TermView::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
275void
276TermView::SetLineTerminator(BString terminator)
277{
278	fLineTerminator = terminator;
279}
280
281
282void
283TermView::PushBytes(const char* bytes, size_t length)
284{
285	vterm_push_bytes(fTerm, bytes, length);
286}
287
288
289void
290TermView::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
303void
304TermView::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
326void
327TermView::_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
361VTermRect
362TermView::_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
387BRect 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
412BRect
413TermView::_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
424void
425TermView::_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
454void
455TermView::_Damage(VTermRect rect)
456{
457	Invalidate(_GlyphsToPixels(rect));
458}
459
460
461void
462TermView::_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
482void
483TermView::_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
499void
500TermView::_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
527int
528TermView::_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
549TermView::_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
559TermView::_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
569TermView::_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
579TermView::_PopLine(int cols, VTermScreenCell* cells, void* user)
580{
581	TermView* view = (TermView*)user;
582	return view->_PopLine(cols, cells);
583}
584
585
586const
587VTermScreenCallbacks 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