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    }