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