001 /*
002 * Copyright (C) 2012 eXo Platform SAS.
003 *
004 * This is free software; you can redistribute it and/or modify it
005 * under the terms of the GNU Lesser General Public License as
006 * published by the Free Software Foundation; either version 2.1 of
007 * the License, or (at your option) any later version.
008 *
009 * This software is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * You should have received a copy of the GNU Lesser General Public
015 * License along with this software; if not, write to the Free
016 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
017 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
018 */
019
020 package org.crsh.term.console;
021
022 import org.crsh.term.spi.TermIO;
023
024 import java.io.IOException;
025 import java.util.Iterator;
026 import java.util.LinkedList;
027 import java.util.NoSuchElementException;
028
029 final class TermIOBuffer implements Appendable, Iterator<CharSequence> {
030
031 /** . */
032 private char[] buffer;
033
034 /** . */
035 private int size;
036
037 /** Cursor Position, always equal to {@link #size} unless the underlying *.IO class supports editing. */
038 private int curAt;
039
040 /** . */
041 private LinkedList<CharSequence> lines;
042
043 /** Do we have a issued a CR previously? */
044 private boolean previousCR;
045
046 /** Whether or not we do echoing. */
047 private boolean echoing;
048
049 /** . */
050 private final TermIO io;
051
052 TermIOBuffer(TermIO io) {
053 this.buffer = new char[128];
054 this.size = 0;
055 this.curAt = 0;
056 this.lines = new LinkedList<CharSequence>();
057 this.previousCR = false;
058 this.echoing = true;
059 this.io = io;
060 }
061
062 /**
063 * Clears the buffer without doing any echoing.
064 */
065 void clear() {
066 this.previousCR = false;
067 this.curAt = 0;
068 this.size = 0;
069 }
070
071 /**
072 * Returns the total number of chars in the buffer, independently of the cursor position.
073 *
074 * @return the number of chars
075 */
076 int getSize() {
077 return size;
078 }
079
080 /**
081 * Returns the current cursor position.
082 *
083 * @return the cursor position
084 */
085 int getCursor() {
086 return curAt;
087 }
088
089 /**
090 * Returns a character at a specified index in the buffer.
091 *
092 * @param index the index
093 * @return the char
094 * @throws IndexOutOfBoundsException if the index is negative or larget than the size
095 */
096 char charAt(int index) throws IndexOutOfBoundsException {
097 if (index < 0) {
098 throw new IndexOutOfBoundsException("No negative position accepted");
099 }
100 if (index >= size) {
101 throw new IndexOutOfBoundsException("Cannot accept position greater than size:" + index + " >= " + size);
102 }
103 return buffer[index];
104 }
105
106 CharSequence getBufferToCursor() {
107 return new String(buffer, 0, curAt);
108 }
109
110 boolean isEchoing() {
111 return echoing;
112 }
113
114 void setEchoing(boolean echoing) {
115 this.echoing = echoing;
116 }
117
118 // Iterator<CharSequence> implementation *****************************************************************************
119
120 public boolean hasNext() {
121 return lines.size() > 0;
122 }
123
124 public CharSequence next() {
125 if (lines.size() > 0) {
126 return lines.removeFirst();
127 } else {
128 throw new NoSuchElementException();
129 }
130 }
131
132 public void remove() {
133 throw new UnsupportedOperationException();
134 }
135
136 // Appendable implementation *****************************************************************************************
137
138 public TermIOBuffer append(char c) throws IOException {
139 if (appendData(c)) {
140 io.flush();
141 }
142 return this;
143 }
144
145 public TermIOBuffer append(CharSequence s) throws IOException {
146 return append(s, 0, s.length());
147 }
148
149 public TermIOBuffer append(CharSequence csq, int start, int end) throws IOException {
150 if (appendData(csq, start, end)) {
151 io.flush();
152 }
153 return this;
154 }
155
156 // Protected methods *************************************************************************************************
157
158 /**
159 * Replace all the characters before the cursor by the provided char sequence.
160 *
161 * @param s the new char sequence
162 * @return the l
163 * @throws IOException any IOException
164 */
165 CharSequence replace(CharSequence s) throws IOException {
166 StringBuilder builder = new StringBuilder();
167 boolean flush = false;
168 for (int i = appendDel();i != -1;i = appendDel()) {
169 builder.append((char)i);
170 flush = true;
171 }
172 flush |= appendData(s, 0, s.length());
173 if (flush) {
174 io.flush();
175 }
176 return builder.reverse().toString();
177 }
178
179 boolean moveRight() throws IOException {
180 return moveRight(1) == 1;
181 }
182
183 boolean moveLeft() throws IOException {
184 return moveLeft(1) == 1;
185 }
186
187 int moveRight(int count) throws IOException, IllegalArgumentException {
188 if (count < 0) {
189 throw new IllegalArgumentException("Cannot move with negative count " + count);
190 }
191 int delta = 0;
192 while (delta < count) {
193 if (curAt + delta < size && io.moveRight(buffer[curAt + delta])) {
194 delta++;
195 } else {
196 break;
197 }
198 }
199 if (delta > 0) {
200 io.flush();
201 curAt += delta;
202 }
203 return delta;
204 }
205
206 int moveLeft(int count) throws IOException, IllegalArgumentException {
207 if (count < 0) {
208 throw new IllegalArgumentException("Cannot move with negative count " + count);
209 }
210 int delta = 0;
211 while (delta < count) {
212 if (delta < curAt && io.moveLeft()) {
213 delta++;
214 } else {
215 break;
216 }
217 }
218 if (delta > 0) {
219 io.flush();
220 curAt -= delta;
221 }
222 return delta;
223 }
224
225 /**
226 * Delete the char under the cursor or return -1 if no char was deleted.
227 *
228 * @return the deleted char
229 * @throws IOException any IOException
230 */
231 int del() throws IOException {
232 int ret = appendDel();
233 if (ret != -1) {
234 io.flush();
235 }
236 return ret;
237 }
238
239 private boolean appendData(CharSequence s, int start, int end) throws IOException {
240 if (start < 0) {
241 throw new IndexOutOfBoundsException("No negative start");
242 }
243 if (end < 0) {
244 throw new IndexOutOfBoundsException("No negative end");
245 }
246 if (end > s.length()) {
247 throw new IndexOutOfBoundsException("End cannot be greater than sequence length");
248 }
249 if (end < start) {
250 throw new IndexOutOfBoundsException("Start cannot be greater than end");
251 }
252 boolean flush = false;
253 for (int i = start;i < end;i++) {
254 flush |= appendData(s.charAt(i));
255 }
256 return flush;
257 }
258
259 /**
260 * Append a char at the current cursor position and increment the cursor position.
261 *
262 * @param c the char to append
263 * @return true if flush is required
264 * @throws IOException any IOException
265 */
266 private boolean appendData(char c) throws IOException {
267 if (previousCR && c == '\n') {
268 previousCR = false;
269 return false;
270 } else if (c == '\r' || c == '\n') {
271 previousCR = c == '\r';
272 String line = new String(buffer, 0, size);
273 lines.add(line);
274 size = 0;
275 curAt = size;
276 return echoCRLF();
277 } else {
278 if (push(c)) {
279 return echo(c);
280 } else {
281 String disp = new String(buffer, curAt, size - curAt);
282 io.write(disp);
283 int amount = size - curAt - 1;
284 curAt++;
285 while (amount > 0) {
286 io.moveLeft();
287 amount--;
288 }
289 return true;
290 }
291 }
292 }
293
294 /**
295 * Delete the char before the cursor.
296 *
297 * @return the removed char value or -1 if no char was removed
298 * @throws IOException any IOException
299 */
300 private int appendDel() throws IOException {
301
302 // If the cursor is at the most right position (i.e no more chars after)
303 if (curAt == size){
304 int popped = pop();
305
306 //
307 if (popped != -1) {
308 echoDel();
309 // We do not care about the return value of echoDel, but we will return a value that indcates
310 // that a flush is required although it may not
311 // to properly carry out the status we should have two things to return
312 // 1/ the popped char
313 // 2/ the boolean indicating if flush is required
314 }
315
316 //
317 return popped;
318 } else {
319 // We are editing the line
320
321 // Shift all the chars after the cursor
322 int popped = pop();
323
324 //
325 if (popped != -1) {
326
327 // We move the cursor to left
328 if (io.moveLeft()) {
329 StringBuilder disp = new StringBuilder();
330 disp.append(buffer, curAt, size - curAt);
331 disp.append(' ');
332 io.write(disp);
333 int amount = size - curAt + 1;
334 while (amount > 0) {
335 io.moveLeft();
336 amount--;
337 }
338 } else {
339 throw new UnsupportedOperationException("not implemented");
340 }
341 }
342
343 //
344 return popped;
345 }
346 }
347
348 private boolean echo(char c) throws IOException {
349 if (echoing) {
350 io.write(c);
351 return true;
352 } else {
353 return false;
354 }
355 }
356
357 private void echo(String s) throws IOException {
358 if (echoing) {
359 io.write(s);
360 io.flush();
361 }
362 }
363
364 private boolean echoDel() throws IOException {
365 if (echoing) {
366 io.writeDel();
367 return true;
368 } else {
369 return false;
370 }
371 }
372
373 private boolean echoCRLF() throws IOException {
374 if (echoing) {
375 io.writeCRLF();
376 return true;
377 } else {
378 return false;
379 }
380 }
381
382 /**
383 * Popup one char from buffer at the current cursor position.
384 *
385 * @return the popped char or -1 if none was removed
386 */
387 private int pop() {
388 if (curAt > 0) {
389 char popped = buffer[curAt - 1];
390 if (curAt == size) {
391 buffer[curAt] = 0;
392 size = --curAt;
393 return popped;
394 } else {
395 for (int i = curAt;i < size;i++) {
396 buffer[i - 1] = buffer[i];
397 }
398 buffer[--size] = 0;
399 curAt--;
400 }
401 return popped;
402 } else {
403 return -1;
404 }
405 }
406
407 /**
408 * Push one char in the buffer at the current cursor position. This operation ensures that the buffer
409 * is large enough and it may increase the buffer capacity when required. The cursor position is incremented
410 * when a char is appended at the last position, otherwise the cursor position remains unchanged.
411 *
412 * @param c the char to push
413 * @return true if the cursor position was incremented
414 */
415 private boolean push(char c) {
416 if (size >= buffer.length) {
417 char[] tmp = new char[buffer.length * 2 + 1];
418 System.arraycopy(buffer, 0, tmp, 0, buffer.length);
419 TermIOBuffer.this.buffer = tmp;
420 }
421 if (curAt == size) {
422 buffer[size++] = c;
423 curAt++;
424 return true;
425 } else {
426 for (int i = size - 1;i > curAt - 1;i--) {
427 buffer[i + 1] = buffer[i];
428 }
429 buffer[curAt] = c;
430 ++size;
431 return false;
432 }
433 }
434 }