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