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 /** 081 * Returns the current cursor position. 082 * 083 * @return the cursor position 084 */ 085 int getCursor() { 086 return curAt; 087 } 088 089 /** 090 * Returns a character at a specified index in the buffer. 091 * 092 * @param index the index 093 * @return the char 094 * @throws IndexOutOfBoundsException if the index is negative or larget than the size 095 */ 096 char charAt(int index) throws IndexOutOfBoundsException { 097 if (index < 0) { 098 throw new IndexOutOfBoundsException("No negative position accepted"); 099 } 100 if (index >= size) { 101 throw new IndexOutOfBoundsException("Cannot accept position greater than size:" + index + " >= " + size); 102 } 103 return buffer[index]; 104 } 105 106 CharSequence getBufferToCursor() { 107 return new String(buffer, 0, curAt); 108 } 109 110 boolean isEchoing() { 111 return echoing; 112 } 113 114 void setEchoing(boolean echoing) { 115 this.echoing = echoing; 116 } 117 118 // Iterator<CharSequence> implementation ***************************************************************************** 119 120 public boolean hasNext() { 121 return lines.size() > 0; 122 } 123 124 public CharSequence next() { 125 if (lines.size() > 0) { 126 return lines.removeFirst(); 127 } else { 128 throw new NoSuchElementException(); 129 } 130 } 131 132 public void remove() { 133 throw new UnsupportedOperationException(); 134 } 135 136 // Appendable implementation ***************************************************************************************** 137 138 public TermIOBuffer append(char c) throws IOException { 139 if (appendData(c)) { 140 io.flush(); 141 } 142 return this; 143 } 144 145 public TermIOBuffer append(CharSequence s) throws IOException { 146 return append(s, 0, s.length()); 147 } 148 149 public TermIOBuffer append(CharSequence csq, int start, int end) throws IOException { 150 if (appendData(csq, start, end)) { 151 io.flush(); 152 } 153 return this; 154 } 155 156 // Protected methods ************************************************************************************************* 157 158 /** 159 * Replace all the characters before the cursor by the provided char sequence. 160 * 161 * @param s the new char sequence 162 * @return the l 163 * @throws IOException any IOException 164 */ 165 CharSequence replace(CharSequence s) throws IOException { 166 StringBuilder builder = new StringBuilder(); 167 boolean flush = false; 168 for (int i = appendDel();i != -1;i = appendDel()) { 169 builder.append((char)i); 170 flush = true; 171 } 172 flush |= appendData(s, 0, s.length()); 173 if (flush) { 174 io.flush(); 175 } 176 return builder.reverse().toString(); 177 } 178 179 boolean moveRight() throws IOException { 180 return moveRight(1) == 1; 181 } 182 183 boolean moveLeft() throws IOException { 184 return moveLeft(1) == 1; 185 } 186 187 int moveRight(int count) throws IOException, IllegalArgumentException { 188 if (count < 0) { 189 throw new IllegalArgumentException("Cannot move with negative count " + count); 190 } 191 int delta = 0; 192 while (delta < count) { 193 if (curAt + delta < size && io.moveRight(buffer[curAt + delta])) { 194 delta++; 195 } else { 196 break; 197 } 198 } 199 if (delta > 0) { 200 io.flush(); 201 curAt += delta; 202 } 203 return delta; 204 } 205 206 int moveLeft(int count) throws IOException, IllegalArgumentException { 207 if (count < 0) { 208 throw new IllegalArgumentException("Cannot move with negative count " + count); 209 } 210 int delta = 0; 211 while (delta < count) { 212 if (delta < curAt && io.moveLeft()) { 213 delta++; 214 } else { 215 break; 216 } 217 } 218 if (delta > 0) { 219 io.flush(); 220 curAt -= delta; 221 } 222 return delta; 223 } 224 225 /** 226 * Delete the char under the cursor or return -1 if no char was deleted. 227 * 228 * @return the deleted char 229 * @throws IOException any IOException 230 */ 231 int del() throws IOException { 232 int ret = appendDel(); 233 if (ret != -1) { 234 io.flush(); 235 } 236 return ret; 237 } 238 239 private boolean appendData(CharSequence s, int start, int end) throws IOException { 240 if (start < 0) { 241 throw new IndexOutOfBoundsException("No negative start"); 242 } 243 if (end < 0) { 244 throw new IndexOutOfBoundsException("No negative end"); 245 } 246 if (end > s.length()) { 247 throw new IndexOutOfBoundsException("End cannot be greater than sequence length"); 248 } 249 if (end < start) { 250 throw new IndexOutOfBoundsException("Start cannot be greater than end"); 251 } 252 boolean flush = false; 253 for (int i = start;i < end;i++) { 254 flush |= appendData(s.charAt(i)); 255 } 256 return flush; 257 } 258 259 /** 260 * Append a char at the current cursor position and increment the cursor position. 261 * 262 * @param c the char to append 263 * @return true if flush is required 264 * @throws IOException any IOException 265 */ 266 private boolean appendData(char c) throws IOException { 267 if (previousCR && c == '\n') { 268 previousCR = false; 269 return false; 270 } else if (c == '\r' || c == '\n') { 271 previousCR = c == '\r'; 272 String line = new String(buffer, 0, size); 273 lines.add(line); 274 size = 0; 275 curAt = size; 276 return echoCRLF(); 277 } else { 278 if (push(c)) { 279 return echo(c); 280 } else { 281 String disp = new String(buffer, curAt, size - curAt); 282 io.write(disp); 283 int amount = size - curAt - 1; 284 curAt++; 285 while (amount > 0) { 286 io.moveLeft(); 287 amount--; 288 } 289 return true; 290 } 291 } 292 } 293 294 /** 295 * Delete the char before the cursor. 296 * 297 * @return the removed char value or -1 if no char was removed 298 * @throws IOException any IOException 299 */ 300 private int appendDel() throws IOException { 301 302 // If the cursor is at the most right position (i.e no more chars after) 303 if (curAt == size){ 304 int popped = pop(); 305 306 // 307 if (popped != -1) { 308 echoDel(); 309 // We do not care about the return value of echoDel, but we will return a value that indcates 310 // that a flush is required although it may not 311 // to properly carry out the status we should have two things to return 312 // 1/ the popped char 313 // 2/ the boolean indicating if flush is required 314 } 315 316 // 317 return popped; 318 } else { 319 // We are editing the line 320 321 // Shift all the chars after the cursor 322 int popped = pop(); 323 324 // 325 if (popped != -1) { 326 327 // We move the cursor to left 328 if (io.moveLeft()) { 329 StringBuilder disp = new StringBuilder(); 330 disp.append(buffer, curAt, size - curAt); 331 disp.append(' '); 332 io.write(disp); 333 int amount = size - curAt + 1; 334 while (amount > 0) { 335 io.moveLeft(); 336 amount--; 337 } 338 } else { 339 throw new UnsupportedOperationException("not implemented"); 340 } 341 } 342 343 // 344 return popped; 345 } 346 } 347 348 private boolean echo(char c) throws IOException { 349 if (echoing) { 350 io.write(c); 351 return true; 352 } else { 353 return false; 354 } 355 } 356 357 private void echo(String s) throws IOException { 358 if (echoing) { 359 io.write(s); 360 io.flush(); 361 } 362 } 363 364 private boolean echoDel() throws IOException { 365 if (echoing) { 366 io.writeDel(); 367 return true; 368 } else { 369 return false; 370 } 371 } 372 373 private boolean echoCRLF() throws IOException { 374 if (echoing) { 375 io.writeCRLF(); 376 return true; 377 } else { 378 return false; 379 } 380 } 381 382 /** 383 * Popup one char from buffer at the current cursor position. 384 * 385 * @return the popped char or -1 if none was removed 386 */ 387 private int pop() { 388 if (curAt > 0) { 389 char popped = buffer[curAt - 1]; 390 if (curAt == size) { 391 buffer[curAt] = 0; 392 size = --curAt; 393 return popped; 394 } else { 395 for (int i = curAt;i < size;i++) { 396 buffer[i - 1] = buffer[i]; 397 } 398 buffer[--size] = 0; 399 curAt--; 400 } 401 return popped; 402 } else { 403 return -1; 404 } 405 } 406 407 /** 408 * Push one char in the buffer at the current cursor position. This operation ensures that the buffer 409 * is large enough and it may increase the buffer capacity when required. The cursor position is incremented 410 * when a char is appended at the last position, otherwise the cursor position remains unchanged. 411 * 412 * @param c the char to push 413 * @return true if the cursor position was incremented 414 */ 415 private boolean push(char c) { 416 if (size >= buffer.length) { 417 char[] tmp = new char[buffer.length * 2 + 1]; 418 System.arraycopy(buffer, 0, tmp, 0, buffer.length); 419 TermIOBuffer.this.buffer = tmp; 420 } 421 if (curAt == size) { 422 buffer[size++] = c; 423 curAt++; 424 return true; 425 } else { 426 for (int i = size - 1;i > curAt - 1;i--) { 427 buffer[i + 1] = buffer[i]; 428 } 429 buffer[curAt] = c; 430 ++size; 431 return false; 432 } 433 } 434 }