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    }