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 }