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