001 /*
002 * Copyright (C) 2003-2009 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.text.Style;
023
024 import java.io.IOException;
025 import java.util.LinkedList;
026 import java.util.NoSuchElementException;
027
028 /**
029 * <p>This class provides an abstraction for a console. This implementation wraps the input and output of a terminal
030 * based on a bidirectional io.</p>
031 *
032 * <p>Interactions between terminal and console are done though the {@link ViewReader} and {@link ViewWriter}
033 * classes.</p>
034 *
035 * @author <a href="mailto:julien.viet@exoplatform.com">Julien Viet</a>
036 * @version $Revision$
037 */
038 public final class Console {
039
040 /** . */
041 private char[] buffer;
042
043 /** . */
044 private int size;
045
046 /** Cursor Position, always equal to {@link #size} unless the underlying *.IO class supports editing. */
047 private int curAt;
048
049 /** . */
050 private LinkedList<CharSequence> lines;
051
052 /** Do we have a issued a CR previously? */
053 private boolean previousCR;
054
055 /** Whether or not we do echoing. */
056 private boolean echoing;
057
058 /** . */
059 private final ViewWriter viewWriter;
060
061 /** . */
062 private final ViewReader viewReader = new ViewReader() {
063
064 @Override
065 public CharSequence replace(CharSequence s) throws IOException {
066 StringBuilder builder = new StringBuilder();
067 boolean flush = false;
068 for (int i = appendDel();i != -1;i = appendDel()) {
069 builder.append((char)i);
070 flush = true;
071 }
072 flush |= appendData(s, 0, s.length());
073 if (flush) {
074 viewWriter.flush();
075 }
076 return builder.reverse().toString();
077 }
078
079 @Override
080 public ViewReader append(char c) throws IOException {
081 if (appendData(c)) {
082 viewWriter.flush();
083 }
084 return this;
085 }
086
087 @Override
088 public ViewReader append(CharSequence s) throws IOException {
089 return append(s, 0, s.length());
090 }
091
092 @Override
093 public ViewReader append(CharSequence csq, int start, int end) throws IOException {
094 if (appendData(csq, start, end)) {
095 viewWriter.flush();
096 }
097 return this;
098 }
099
100 @Override
101 public int del() throws IOException {
102 int ret = appendDel();
103 if (ret != -1) {
104 viewWriter.flush();
105 }
106 return ret;
107 }
108
109 @Override
110 public boolean moveRight() throws IOException {
111 return Console.this.moveRight();
112 }
113
114 @Override
115 public boolean moveLeft() throws IOException {
116 return Console.this.moveLeft();
117 }
118 };
119
120 /** . */
121 private final ConsoleReader reader = new ConsoleReader() {
122 @Override
123 public int getSize() {
124 return size;
125 }
126
127 @Override
128 public boolean hasNext() {
129 return lines.size() > 0;
130 }
131
132 @Override
133 public CharSequence next() {
134 if (lines.size() > 0) {
135 return lines.removeFirst();
136 } else {
137 throw new NoSuchElementException();
138 }
139 }
140 };
141
142 /** . */
143 private final ConsoleWriter writer = new ConsoleWriter() {
144
145 //
146 private boolean previousCR;
147
148 @Override
149 public void write(CharSequence s) throws IOException {
150 for (int i = 0;i < s.length();i++) {
151 char c = s.charAt(i);
152 writeNoFlush(c);
153 }
154 viewWriter.flush();
155 }
156
157 public void write(char c) throws IOException {
158 writeNoFlush(c);
159 viewWriter.flush();
160 }
161
162 @Override
163 public void write(Style style) throws IOException {
164 viewWriter.write(style);
165 }
166
167 private void writeNoFlush(char c) throws IOException {
168 if (previousCR && c == '\n') {
169 previousCR = false;
170 } else if (c == '\r' || c == '\n') {
171 previousCR = c == '\r';
172 viewWriter.writeCRLF();
173 } else {
174 viewWriter.write(c);
175 }
176 }
177 };
178
179 public Console(ViewWriter viewWriter) {
180 this.buffer = new char[128];
181 this.size = 0;
182 this.curAt = 0;
183 this.lines = new LinkedList<CharSequence>();
184 this.previousCR = false;
185 this.echoing = true;
186 this.viewWriter = viewWriter;
187 }
188
189 /**
190 * Clears the buffer without doing any echoing.
191 */
192 public void clearBuffer() {
193 this.previousCR = false;
194 this.curAt = 0;
195 this.size = 0;
196 }
197
198 public CharSequence getBuffer() {
199 return new String(buffer, 0, size);
200 }
201
202 public CharSequence getBufferToCursor() {
203 return new String(buffer, 0, curAt);
204 }
205
206 public boolean isEchoing() {
207 return echoing;
208 }
209
210 public void setEchoing(boolean echoing) {
211 Console.this.echoing = echoing;
212 }
213
214 /**
215 * Returns the console reader.
216 *
217 * @return the console reader
218 */
219 public ConsoleReader getReader() {
220 return reader;
221 }
222
223 public ViewReader getViewReader() {
224 return viewReader;
225 }
226
227 public ConsoleWriter getWriter() {
228 return writer;
229 }
230
231 private boolean appendData(CharSequence s, int start, int end) throws IOException {
232 if (start < 0) {
233 throw new IndexOutOfBoundsException("No negative start");
234 }
235 if (end < 0) {
236 throw new IndexOutOfBoundsException("No negative end");
237 }
238 if (end > s.length()) {
239 throw new IndexOutOfBoundsException("End cannot be greater than sequence length");
240 }
241 if (end < start) {
242 throw new IndexOutOfBoundsException("Start cannot be greater than end");
243 }
244 boolean flush = false;
245 for (int i = start;i < end;i++) {
246 flush |= appendData(s.charAt(i));
247 }
248 return flush;
249 }
250
251 /**
252 * Append a char at the current cursor position and increment the cursor position.
253 *
254 * @param c the char to append
255 * @return true if flush is required
256 * @throws IOException any IOException
257 */
258 private boolean appendData(char c) throws IOException {
259 if (previousCR && c == '\n') {
260 previousCR = false;
261 return false;
262 } else if (c == '\r' || c == '\n') {
263 previousCR = c == '\r';
264 String line = new String(buffer, 0, size);
265 lines.add(line);
266 size = 0;
267 curAt = size;
268 return echoCRLF();
269 } else {
270 if (push(c)) {
271 return echo(c);
272 } else {
273 String disp = new String(buffer, curAt, size - curAt);
274 viewWriter.write(disp);
275 int amount = size - curAt - 1;
276 curAt++;
277 while (amount > 0) {
278 viewWriter.writeMoveLeft();
279 amount--;
280 }
281 return true;
282 }
283 }
284 }
285
286 /**
287 * Delete the char before the cursor.
288 *
289 * @return the removed char value or -1 if no char was removed
290 * @throws IOException any IOException
291 */
292 private int appendDel() throws IOException {
293
294 // If the cursor is at the most right position (i.e no more chars after)
295 if (curAt == size){
296 int popped = pop();
297
298 //
299 if (popped != -1) {
300 echoDel();
301 // We do not care about the return value of echoDel, but we will return a value that indcates
302 // that a flush is required although it may not
303 // to properly carry out the status we should have two things to return
304 // 1/ the popped char
305 // 2/ the boolean indicating if flush is required
306 }
307
308 //
309 return popped;
310 } else {
311 // We are editing the line
312
313 // Shift all the chars after the cursor
314 int popped = pop();
315
316 //
317 if (popped != -1) {
318
319 // We move the cursor to left
320 if (viewWriter.writeMoveLeft()) {
321 StringBuilder disp = new StringBuilder();
322 disp.append(buffer, curAt, size - curAt);
323 disp.append(' ');
324 viewWriter.write(disp);
325 int amount = size - curAt + 1;
326 while (amount > 0) {
327 viewWriter.writeMoveLeft();
328 amount--;
329 }
330 } else {
331 throw new UnsupportedOperationException("not implemented");
332 }
333 }
334
335 //
336 return popped;
337 }
338 }
339
340 private boolean moveRight() throws IOException {
341 if (curAt < size && viewWriter.writeMoveRight(buffer[curAt])) {
342 viewWriter.flush();
343 curAt++;
344 return true;
345 } else {
346 return false;
347 }
348 }
349
350 private boolean moveLeft() throws IOException {
351 boolean moved = curAt > 0 && viewWriter.writeMoveLeft();
352 if (moved) {
353 viewWriter.flush();
354 curAt--;
355 }
356 return moved;
357 }
358
359 private boolean echo(char c) throws IOException {
360 if (echoing) {
361 viewWriter.write(c);
362 return true;
363 } else {
364 return false;
365 }
366 }
367
368 private void echo(String s) throws IOException {
369 if (echoing) {
370 viewWriter.write(s);
371 viewWriter.flush();
372 }
373 }
374
375 private boolean echoDel() throws IOException {
376 if (echoing) {
377 viewWriter.writeDel();
378 return true;
379 } else {
380 return false;
381 }
382 }
383
384 private boolean echoCRLF() throws IOException {
385 if (echoing) {
386 viewWriter.writeCRLF();
387 return true;
388 } else {
389 return false;
390 }
391 }
392
393 /**
394 * Popup one char from buffer at the current cursor position.
395 *
396 * @return the popped char or -1 if none was removed
397 */
398 private int pop() {
399 if (curAt > 0) {
400 char popped = buffer[curAt - 1];
401 if (curAt == size) {
402 buffer[curAt] = 0;
403 size = --curAt;
404 return popped;
405 } else {
406 for (int i = curAt;i < size;i++) {
407 buffer[i - 1] = buffer[i];
408 }
409 buffer[--size] = 0;
410 curAt--;
411 }
412 return popped;
413 } else {
414 return -1;
415 }
416 }
417
418 /**
419 * Push one char in the buffer at the current cursor position. This operation ensures that the buffer
420 * is large enough and it may increase the buffer capacity when required. The cursor position is incremented
421 * when a char is appended at the last position, otherwise the cursor position remains unchanged.
422 *
423 * @param c the char to push
424 * @return true if the cursor position was incremented
425 */
426 private boolean push(char c) {
427 if (size >= buffer.length) {
428 char[] tmp = new char[buffer.length * 2 + 1];
429 System.arraycopy(buffer, 0, tmp, 0, buffer.length);
430 Console.this.buffer = tmp;
431 }
432 if (curAt == size) {
433 buffer[size++] = c;
434 curAt++;
435 return true;
436 } else {
437 for (int i = size - 1;i > curAt - 1;i--) {
438 buffer[i + 1] = buffer[i];
439 }
440 buffer[curAt] = c;
441 ++size;
442 return false;
443 }
444 }
445 }