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    }