]>
Commit | Line | Data |
---|---|---|
ca4e99e8 DI |
1 | /** |
2 | * @license MIT | |
3 | */ | |
4 | ||
92068f36 DI |
5 | import { ITerminal } from './Interfaces'; |
6 | ||
7 | /** | |
8 | * The maximum number of refresh frames to skip when the write buffer is non- | |
9 | * empty. Note that these frames may be intermingled with frames that are | |
10 | * skipped via requestAnimationFrame's mechanism. | |
11 | */ | |
12 | const MAX_REFRESH_FRAME_SKIP = 5; | |
13 | ||
92068f36 | 14 | /** |
20f22fb6 | 15 | * Flags used to render terminal text properly. |
92068f36 | 16 | */ |
20f22fb6 DI |
17 | enum FLAGS { |
18 | BOLD = 1, | |
19 | UNDERLINE = 2, | |
20 | BLINK = 4, | |
21 | INVERSE = 8, | |
22 | INVISIBLE = 16 | |
92068f36 DI |
23 | }; |
24 | ||
25 | let brokenBold: boolean = null; | |
26 | ||
27 | export class Renderer { | |
28 | /** A queue of the rows to be refreshed */ | |
29 | private _refreshRowsQueue: {start: number, end: number}[] = []; | |
30 | private _refreshFramesSkipped = 0; | |
31 | private _refreshAnimationFrame = null; | |
32 | ||
33 | constructor(private _terminal: ITerminal) { | |
34 | // Figure out whether boldness affects | |
35 | // the character width of monospace fonts. | |
36 | if (brokenBold === null) { | |
37 | brokenBold = checkBoldBroken((<any>this._terminal).document); | |
92068f36 DI |
38 | } |
39 | ||
9d8f6c9a DI |
40 | // TODO: Pull more DOM interactions into Renderer.constructor, element for |
41 | // example should be owned by Renderer (and also exposed by Terminal due to | |
42 | // to established public API). | |
92068f36 DI |
43 | } |
44 | ||
45 | /** | |
46 | * Queues a refresh between two rows (inclusive), to be done on next animation | |
47 | * frame. | |
48 | * @param {number} start The start row. | |
49 | * @param {number} end The end row. | |
50 | */ | |
51 | public queueRefresh(start: number, end: number): void { | |
52 | this._refreshRowsQueue.push({ start: start, end: end }); | |
53 | if (!this._refreshAnimationFrame) { | |
54 | this._refreshAnimationFrame = window.requestAnimationFrame(this._refreshLoop.bind(this)); | |
55 | } | |
56 | } | |
57 | ||
58 | /** | |
59 | * Performs the refresh loop callback, calling refresh only if a refresh is | |
60 | * necessary before queueing up the next one. | |
61 | */ | |
62 | private _refreshLoop(): void { | |
63 | // Skip MAX_REFRESH_FRAME_SKIP frames if the writeBuffer is non-empty as it | |
64 | // will need to be immediately refreshed anyway. This saves a lot of | |
65 | // rendering time as the viewport DOM does not need to be refreshed, no | |
66 | // scroll events, no layouts, etc. | |
67 | const skipFrame = this._terminal.writeBuffer.length > 0 && this._refreshFramesSkipped++ <= MAX_REFRESH_FRAME_SKIP; | |
68 | if (skipFrame) { | |
69 | this._refreshAnimationFrame = window.requestAnimationFrame(this._refreshLoop.bind(this)); | |
70 | return; | |
71 | } | |
72 | ||
73 | this._refreshFramesSkipped = 0; | |
74 | let start; | |
75 | let end; | |
76 | if (this._refreshRowsQueue.length > 4) { | |
77 | // Just do a full refresh when 5+ refreshes are queued | |
78 | start = 0; | |
79 | end = this._terminal.rows - 1; | |
80 | } else { | |
81 | // Get start and end rows that need refreshing | |
82 | start = this._refreshRowsQueue[0].start; | |
83 | end = this._refreshRowsQueue[0].end; | |
84 | for (let i = 1; i < this._refreshRowsQueue.length; i++) { | |
85 | if (this._refreshRowsQueue[i].start < start) { | |
86 | start = this._refreshRowsQueue[i].start; | |
87 | } | |
88 | if (this._refreshRowsQueue[i].end > end) { | |
89 | end = this._refreshRowsQueue[i].end; | |
90 | } | |
91 | } | |
92 | } | |
93 | this._refreshRowsQueue = []; | |
94 | this._refreshAnimationFrame = null; | |
95 | this._refresh(start, end); | |
96 | } | |
97 | ||
98 | /** | |
99 | * Refreshes (re-renders) terminal content within two rows (inclusive) | |
100 | * | |
101 | * Rendering Engine: | |
102 | * | |
103 | * In the screen buffer, each character is stored as a an array with a character | |
104 | * and a 32-bit integer: | |
105 | * - First value: a utf-16 character. | |
106 | * - Second value: | |
107 | * - Next 9 bits: background color (0-511). | |
108 | * - Next 9 bits: foreground color (0-511). | |
109 | * - Next 14 bits: a mask for misc. flags: | |
110 | * - 1=bold | |
111 | * - 2=underline | |
112 | * - 4=blink | |
113 | * - 8=inverse | |
114 | * - 16=invisible | |
115 | * | |
116 | * @param {number} start The row to start from (between 0 and terminal's height terminal - 1) | |
117 | * @param {number} end The row to end at (between fromRow and terminal's height terminal - 1) | |
118 | */ | |
119 | private _refresh(start: number, end: number): void { | |
92068f36 DI |
120 | let x, y, i, line, out, ch, ch_width, width, data, attr, bg, fg, flags, row, parent, focused = document.activeElement; |
121 | ||
122 | // If this is a big refresh, remove the terminal rows from the DOM for faster calculations | |
123 | if (end - start >= this._terminal.rows / 2) { | |
124 | parent = this._terminal.element.parentNode; | |
125 | if (parent) { | |
126 | this._terminal.element.removeChild(this._terminal.rowContainer); | |
127 | } | |
128 | } | |
129 | ||
130 | width = this._terminal.cols; | |
131 | y = start; | |
132 | ||
133 | if (end >= this._terminal.rows) { | |
134 | this._terminal.log('`end` is too large. Most likely a bad CSR.'); | |
135 | end = this._terminal.rows - 1; | |
136 | } | |
137 | ||
138 | for (; y <= end; y++) { | |
139 | row = y + this._terminal.ydisp; | |
140 | ||
141 | line = this._terminal.lines.get(row); | |
142 | if (!line || !this._terminal.children[y]) { | |
143 | // Continue if the line is not available, this means a resize is currently in progress | |
144 | continue; | |
145 | } | |
146 | out = ''; | |
147 | ||
148 | if (this._terminal.y === y - (this._terminal.ybase - this._terminal.ydisp) | |
149 | && this._terminal.cursorState | |
150 | && !this._terminal.cursorHidden) { | |
151 | x = this._terminal.x; | |
152 | } else { | |
153 | x = -1; | |
154 | } | |
155 | ||
156 | attr = this._terminal.defAttr; | |
157 | i = 0; | |
158 | ||
159 | for (; i < width; i++) { | |
160 | if (!line[i]) { | |
161 | // Continue if the character is not available, this means a resize is currently in progress | |
162 | continue; | |
163 | } | |
164 | data = line[i][0]; | |
165 | ch = line[i][1]; | |
166 | ch_width = line[i][2]; | |
167 | if (!ch_width) | |
168 | continue; | |
169 | ||
170 | if (i === x) data = -1; | |
171 | ||
172 | if (data !== attr) { | |
173 | if (attr !== this._terminal.defAttr) { | |
174 | out += '</span>'; | |
175 | } | |
176 | if (data !== this._terminal.defAttr) { | |
177 | if (data === -1) { | |
178 | out += '<span class="reverse-video terminal-cursor">'; | |
179 | } else { | |
180 | let classNames = []; | |
181 | ||
182 | bg = data & 0x1ff; | |
183 | fg = (data >> 9) & 0x1ff; | |
184 | flags = data >> 18; | |
185 | ||
186 | if (flags & FLAGS.BOLD) { | |
187 | if (!brokenBold) { | |
188 | classNames.push('xterm-bold'); | |
189 | } | |
190 | // See: XTerm*boldColors | |
191 | if (fg < 8) fg += 8; | |
192 | } | |
193 | ||
194 | if (flags & FLAGS.UNDERLINE) { | |
195 | classNames.push('xterm-underline'); | |
196 | } | |
197 | ||
198 | if (flags & FLAGS.BLINK) { | |
199 | classNames.push('xterm-blink'); | |
200 | } | |
201 | ||
202 | // If inverse flag is on, then swap the foreground and background variables. | |
203 | if (flags & FLAGS.INVERSE) { | |
204 | /* One-line variable swap in JavaScript: http://stackoverflow.com/a/16201730 */ | |
205 | bg = [fg, fg = bg][0]; | |
206 | // Should inverse just be before the | |
207 | // above boldColors effect instead? | |
208 | if ((flags & 1) && fg < 8) fg += 8; | |
209 | } | |
210 | ||
211 | if (flags & FLAGS.INVISIBLE) { | |
212 | classNames.push('xterm-hidden'); | |
213 | } | |
214 | ||
215 | /** | |
216 | * Weird situation: Invert flag used black foreground and white background results | |
217 | * in invalid background color, positioned at the 256 index of the 256 terminal | |
218 | * color map. Pin the colors manually in such a case. | |
219 | * | |
220 | * Source: https://github.com/sourcelair/xterm.js/issues/57 | |
221 | */ | |
222 | if (flags & FLAGS.INVERSE) { | |
223 | if (bg === 257) { | |
224 | bg = 15; | |
225 | } | |
226 | if (fg === 256) { | |
227 | fg = 0; | |
228 | } | |
229 | } | |
230 | ||
231 | if (bg < 256) { | |
232 | classNames.push('xterm-bg-color-' + bg); | |
233 | } | |
234 | ||
235 | if (fg < 256) { | |
236 | classNames.push('xterm-color-' + fg); | |
237 | } | |
238 | ||
239 | out += '<span'; | |
240 | if (classNames.length) { | |
241 | out += ' class="' + classNames.join(' ') + '"'; | |
242 | } | |
243 | out += '>'; | |
244 | } | |
245 | } | |
246 | } | |
247 | ||
248 | if (ch_width === 2) { | |
249 | out += '<span class="xterm-wide-char">'; | |
250 | } | |
251 | switch (ch) { | |
252 | case '&': | |
253 | out += '&'; | |
254 | break; | |
255 | case '<': | |
256 | out += '<'; | |
257 | break; | |
258 | case '>': | |
259 | out += '>'; | |
260 | break; | |
261 | default: | |
262 | if (ch <= ' ') { | |
263 | out += ' '; | |
264 | } else { | |
265 | out += ch; | |
266 | } | |
267 | break; | |
268 | } | |
269 | if (ch_width === 2) { | |
270 | out += '</span>'; | |
271 | } | |
272 | ||
273 | attr = data; | |
274 | } | |
275 | ||
276 | if (attr !== this._terminal.defAttr) { | |
277 | out += '</span>'; | |
278 | } | |
279 | ||
280 | this._terminal.children[y].innerHTML = out; | |
281 | } | |
282 | ||
283 | if (parent) { | |
284 | this._terminal.element.appendChild(this._terminal.rowContainer); | |
285 | } | |
286 | ||
287 | this._terminal.emit('refresh', {element: this._terminal.element, start: start, end: end}); | |
288 | }; | |
289 | } | |
290 | ||
291 | ||
292 | // if bold is broken, we can't | |
293 | // use it in the terminal. | |
294 | function checkBoldBroken(document) { | |
295 | const body = document.getElementsByTagName('body')[0]; | |
296 | const el = document.createElement('span'); | |
297 | el.innerHTML = 'hello world'; | |
298 | body.appendChild(el); | |
299 | const w1 = el.scrollWidth; | |
300 | el.style.fontWeight = 'bold'; | |
301 | const w2 = el.scrollWidth; | |
302 | body.removeChild(el); | |
303 | return w1 !== w2; | |
304 | } |