iv.vfs: don't turn "w+" mode to "r+" mode, lol
[iv.d.git] / editline.d
blob2e5fe9af1eede1b76de53dae4ebc2a056e29668a
1 /* Simple readline/editline replacement. Deliberately non-configurable.
3 * Written by Ketmar // Invisible Vector <ketmar@ketmar.no-ip.org>
4 * Understanding is not required. Only obedience.
6 * This program is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <http://www.gnu.org/licenses/>.
19 module iv.editline /*is aliced*/;
21 import iv.alice;
22 import iv.rawtty;
23 import iv.strex;
26 // ////////////////////////////////////////////////////////////////////////// //
27 class EditLine {
28 public:
29 final class Line {
30 public:
31 enum MaxLen = 4096;
33 private:
34 char[MaxLen] cline;
35 int lpos; // cursor position: [0..len]
36 int llen; // line length (line can be longer)
37 //uint ofs; // used in text draw
39 public:
40 this () {}
42 // cursor position
43 @property int pos () const pure nothrow @safe @nogc { pragma(inline, true); return lpos; }
44 @property void pos (int npos) pure nothrow @safe @nogc { if (npos < 0) npos = 0; if (npos > llen) npos = llen; lpos = npos; }
45 void movePos (int delta) {
46 if (delta < -MaxLen) delta = -MaxLen;
47 if (delta > MaxLen) delta = MaxLen;
48 pos = lpos+delta;
51 // line length
52 @property int length () const pure nothrow @safe @nogc { pragma(inline, true); return llen; }
54 char opIndex (long pos) const pure nothrow @trusted @nogc { pragma(inline, true); return (pos >= 0 && pos < llen ? cline.ptr[cast(uint)pos] : 0); }
55 const(char)[] opIndex() (in auto ref WordPos wp) const pure nothrow @safe @nogc { pragma(inline, true); return this[wp.s..wp.e]; }
57 // do not slice it, buffer WILL change
58 const(char)[] opSlice () const pure nothrow @safe @nogc { pragma(inline, true); return cline[0..llen]; }
59 const(char)[] opSlice (long lo, long hi) const pure nothrow @safe @nogc {
60 //pragma(inline, true);
61 if (lo >= hi) return null;
62 if (hi <= 0 || lo >= llen) return null;
63 if (lo < 0) lo = 0;
64 if (hi > llen) hi = llen;
65 return cline[cast(uint)lo..cast(uint)hi];
68 // clear line
69 void clear () pure nothrow @trusted @nogc { lpos = llen = 0; }
71 // crop line at current position
72 void crop () pure nothrow @trusted @nogc { llen = lpos; }
74 // set current line, move cursor to end; crop if length is too big
75 // return `false` if new line was cropped
76 bool set (const(char)[] s...) pure nothrow @trusted @nogc {
77 bool res = true;
78 if (s.length > MaxLen) { s = s[0..MaxLen]; res = false; }
79 if (s.length) cline[0..s.length] = s[];
80 lpos = llen = cast(int)s.length;
81 return res;
84 // insert chars at cursor position, move cursor
85 // return `false` if there is no room, line is not modified in this case
86 bool insert (const(char)[] s...) pure nothrow @trusted @nogc {
87 if (s.length == 0) return true;
88 if (s.length > MaxLen || llen+s.length > MaxLen) return false;
89 // make room
90 if (lpos < llen) {
91 import core.stdc.string : memmove;
92 memmove(cline.ptr+lpos+cast(int)s.length, cline.ptr+lpos, llen-lpos);
94 llen += cast(int)s.length;
95 // copy
96 cline[lpos..lpos+cast(int)s.length] = s[];
97 lpos += cast(int)s.length;
98 return true;
101 // replace chars at cursor position, move cursor; does appending
102 // return `false` if there is no room, line is not modified in this case
103 bool replace (const(char)[] s...) pure nothrow @trusted @nogc {
104 if (s.length == 0) return true;
105 if (s.length > MaxLen || lpos+s.length > MaxLen) return false;
106 // replace
107 cline[lpos..lpos+cast(int)s.length] = s[];
108 lpos += cast(int)s.length;
109 if (llen < lpos) llen = lpos;
110 return true;
113 // remove chars at cursor position (delete), don't move cursor
114 void remove (int len) pure nothrow @trusted @nogc {
115 if (len < 1 || lpos >= llen) return;
116 if (len > MaxLen) len = MaxLen;
117 if (lpos+len >= llen) {
118 // strip
119 llen = lpos;
120 } else {
121 import core.stdc.string : memmove;
122 memmove(cline.ptr+lpos, cline.ptr+lpos+len, llen-(lpos+len));
123 llen -= len;
124 assert(lpos < llen);
128 // delete chars at cursor position (backspace), move cursor
129 void backspace (int len) pure nothrow @trusted @nogc {
130 if (len < 1 || lpos < 1) return;
131 if (len > lpos) len = lpos;
132 lpos -= len;
133 remove(len);
136 // //// something to make programmer's life easier //// //
138 static struct WordPos {
139 int s, e; // start and end position, suitable for slicing
140 @property bool valid () const pure nothrow @safe @nogc { pragma(inline, true); return (s >= 0 && s < e); }
141 @property int length () const pure nothrow @safe @nogc { pragma(inline, true); return e-s; }
144 // word positions range
145 auto words () inout pure nothrow @trusted @nogc {
146 static struct Result {
147 private:
148 const Line ln;
149 WordPos wp;
150 this (in Line aln) pure nothrow @safe @nogc { ln = aln; popFront(); }
151 this() (in Line aln, in auto ref WordPos awp) pure nothrow @safe @nogc { ln = aln; wp.s = awp.s; wp.e = awp.e; }
152 public:
153 @property const(char)[] word () const pure nothrow @safe @nogc { pragma(inline, true); return ln[wp.s..wp.e]; }
154 @property WordPos front () const pure nothrow @safe @nogc { pragma(inline, true); return WordPos(wp.s, wp.e); }
155 @property bool empty () const pure nothrow @safe @nogc { pragma(inline, true); return (wp.s >= ln.llen); }
156 @property auto save () const pure nothrow @safe @nogc { pragma(inline, true); return Result(ln, wp); }
157 @property void popFront () pure nothrow @trusted @nogc {
158 if (wp.s >= ln.llen) return;
159 wp.s = wp.e;
160 while (wp.s < ln.llen && ln.cline.ptr[wp.s] <= ' ') ++wp.s;
161 wp.e = wp.s;
162 if (wp.e >= ln.llen) return;
163 char qch = ln.cline.ptr[wp.e];
164 if (qch == '"' || qch == '\'' || qch == '`') {
165 // quoted
166 ++wp.e;
167 while (wp.e < ln.llen) {
168 char ch = ln.cline.ptr[wp.e++];
169 if (ch == '\\') {
170 if (wp.e < ln.llen) ++wp.e;
171 } else if (ch == qch) {
172 break;
175 } else {
176 while (wp.e < ln.llen && ln.cline.ptr[wp.e] > ' ') ++wp.e;
180 return Result(this);
183 int wordCount () const pure nothrow @trusted @nogc {
184 int res = 0;
185 auto ww = words();
186 while (!ww.empty) { ++res; ww.popFront(); }
187 return res;
190 const(char)[] word (int idx) const pure nothrow @trusted @nogc {
191 if (idx < 0) return null;
192 auto ww = words();
193 while (idx > 0 && !ww.empty) { --idx; ww.popFront(); }
194 if (!ww.empty) return this[ww.front.s..ww.front.e];
195 return null;
198 // assume that `pos` is inside word, return `true` if word starts with quote
199 bool wordQuoted (int pos) const pure nothrow @trusted @nogc {
200 foreach (const ref wp; words) {
201 if (pos >= wp.s && pos < wp.e) {
202 // our word
203 char ch = this[wp.s];
204 return (ch == '"' || ch == '\'' || ch == '`');
207 return false;
210 // we can autocomplete empty line or any word if we're at it's end
211 bool canAutocomplete () const pure nothrow @trusted @nogc {
212 if (llen == 0) return true;
213 if (llen == MaxLen) return false;
214 if (lpos == 0) return false; // we have some text, so can't
215 if (lpos == llen) return (cline.ptr[lpos-1] <= ' ' || !wordQuoted(lpos-1));
216 // end of word?
217 return (cline.ptr[lpos] <= ' ' && cline.ptr[lpos-1] > ' ' && !wordQuoted(lpos-1));
220 // get word number for autocompletion
221 int acWordNum () const pure nothrow @trusted @nogc {
222 if (!canAutocomplete) return -1;
223 if (lpos == llen && cline.ptr[lpos-1] <= ' ') return wordCount;
224 int idx;
225 foreach (ref wp; words) {
226 if (lpos >= wp.s && lpos <= wp.e) return idx;
227 ++idx;
229 return idx;
232 // get word for autocompletion
233 WordPos acWordPos () const pure nothrow @trusted @nogc {
234 if (!canAutocomplete) return WordPos();
235 if (lpos == llen && cline.ptr[lpos-1] <= ' ') return WordPos(llen, llen);
236 foreach (ref wp; words) if (lpos >= wp.s && lpos <= wp.e) return wp;
237 return WordPos(llen, llen);
240 // replace current with new
241 bool acWordReplace (const(char)[] w, bool addSpace=false) pure nothrow @trusted @nogc {
242 if (w.length == 0 || w.length > MaxLen || !canAutocomplete) return false;
243 foreach (const ref wp; words) {
244 if (lpos >= wp.s && lpos <= wp.e) {
245 // check if we should add space
246 if (addSpace) addSpace = (wp.e >= llen || cline.ptr[wp.e] > ' ');
247 int addlen = (addSpace ? 1 : 0);
248 if (llen-wp.length+w.length+addlen > MaxLen) return false;
249 backspace(wp.length);
250 insert(w);
251 if (addSpace) insert(' ');
252 return true;
255 return false;
258 // replace current word with new, add space
259 bool acWordReplaceSpaced (const(char)[] w) pure nothrow @trusted @nogc { return acWordReplace(w, true); }
262 public:
263 enum Result {
264 Normal,
265 CtrlC,
266 CtrlD,
269 uint historyLimit = 512;
270 string[] history;
272 protected:
273 Line curline;
274 char[] promptbuf;
276 public:
277 this () { promptbuf = ">".dup; curline = new Line(); }
279 final @property inout(Line) line () inout pure nothrow @safe @nogc { pragma(inline, true); return curline; }
281 // const(char)[] returns slice of internal buffer, don't store it!
282 final @property T get(T=string) () if (is(T : const(char)[])) {
283 static if (is(T == string)) return curline[].idup;
284 else static if (is(T == const(char)[])) return curline[];
285 else return curline[].dup;
288 final @property string prompt () { return promptbuf.idup; }
289 final @property prompt (const(char)[] pt) {
290 promptbuf.length = 0;
291 promptbuf.assumeSafeAppend;
292 if (pt.length > 61) { promptbuf ~= "..."; pt = pt[$-61..$]; }
293 promptbuf ~= pt;
296 final void pushCurrentToHistory () { pushToHistory(get!string); }
298 final void pushToHistory(T : const(char)[]) (T s) {
299 static if (is(T == typeof(null))) string hs = null;
300 else static if (is(T == string)) string hs = s.xstrip;
301 else string hs = s.xstrip.idup;
302 if (hs.length == 0) return;
303 // find duplicate and remove it
304 foreach (immutable idx, string st; history) {
305 if (st.xstrip == hs) {
306 // i found her!
307 // remove
308 foreach (immutable c; idx+1..history.length) history[c-1] = history[c];
309 // move
310 foreach_reverse (immutable c; 1..history.length) history[c] = history[c-1];
311 // set
312 history[0] = hs;
313 // done
314 return;
317 if (history.length > historyLimit) history.length = historyLimit;
318 else if (history.length < historyLimit) history.length += 1;
319 foreach_reverse (immutable c; 1..history.length) history[c] = history[c-1];
320 history[0] = hs;
323 final Result readline (const(char)[] initval=null) {
324 char[Line.MaxLen] lastInput; // stored for history walks
325 uint lastLen; // stored for history walks
326 int hpos = -1; // -1: current
327 int curofs; // output offset
329 void fixCurLine () {
330 hpos = -1;
331 lastLen = 0;
334 void drawLine () {
335 auto wdt = ttyWidth-1;
336 if (wdt < 16) wdt = 16;
337 const(char)[] cline = (hpos < 0 ? curline[] : history[hpos]);
338 if (wdt <= promptbuf.length+3) wdt = 3; else wdt -= cast(int)promptbuf.length;
339 int wvis = (wdt < 10 ? wdt-1 : 8);
340 int cpos = curline.pos;
341 // special handling for negative offset: it means "make left part visible"
342 if (curofs < 0) {
343 // is cursor at eol? special handling
344 if (cpos == curline.length) {
345 curofs = cpos-wdt;
346 if (curofs < 0) curofs = 0;
347 } else {
348 // make wvis char after cursor visible
349 curofs = cpos-wdt+wvis;
350 if (curofs < 0) curofs = 0;
351 // i did something wrong here...
352 if (curofs >= curline.length) {
353 curofs = curline.length-wdt;
354 if (curofs < 0) curofs = 0;
357 } else {
358 // is cursor too far at left?
359 if (cpos < curofs) {
360 // make wvis left chars visible
361 curofs = cpos-wvis;
362 if (curofs < 0) curofs = 0;
364 // is cursor too far at right?
365 if (curofs+wdt <= cpos) {
366 // make wvis right chars visible
367 curofs = cpos-wdt+wvis;
368 if (curofs < 0) curofs = 0;
369 if (curofs >= curline.length) {
370 curofs = curline.length-wdt;
371 if (curofs < 0) curofs = 0;
375 assert(curofs >= 0 && curofs <= curline.length);
376 int end = curofs+wdt;
377 if (end > curline.length) end = curline.length;
378 wrt("\r");
379 wrt(promptbuf);
380 wrt(curline[curofs..end]);
381 if (cpos == end) {
382 wrt("\x1b[K");
383 } else {
384 wrt("\x1b[K\r\x1b[");
385 wrtuint(promptbuf.length+(cpos-curofs));
386 wrt("C");
390 curofs = 0;
391 curline.clear();
392 if (initval.length) curline.set(initval);
394 auto ttymode = ttyGetMode();
395 scope(exit) ttySetMode(ttymode);
396 ttySetRaw();
398 void delPrevWord () {
399 if (curline.pos > 0) {
400 fixCurLine();
401 while (curline.pos > 0 && curline[curline.pos-1] <= ' ') curline.backspace(1);
402 while (curline.pos > 0 && curline[curline.pos-1] > ' ') curline.backspace(1);
406 for (;;) {
407 drawLine();
408 auto key = ttyReadKey();
409 if (key.key == TtyEvent.Key.Error) { curline.clear(); return Result.CtrlD; }
410 if (key.key == TtyEvent.Key.Unknown) continue;
411 if (key.key == TtyEvent.Key.ModChar) {
412 if (!key.alt && key.ctrl && !key.shift && key.ch == 'W') { delPrevWord(); continue; }
413 if (!key.alt && key.ctrl && !key.shift && key.ch == 'C') { curline.clear(); return Result.CtrlC; }
414 if (!key.alt && key.ctrl && !key.shift && key.ch == 'D') { curline.clear(); return Result.CtrlD; }
415 if (!key.alt && key.ctrl && !key.shift && key.ch == 'K') { fixCurLine(); curline.crop(); continue; }
416 if (!key.alt && key.ctrl && !key.shift && key.ch == 'Y') { fixCurLine(); curline.clear(); continue; }
417 if (!key.alt && key.ctrl && !key.shift && key.ch == 'A') { curline.movePos(-curline.MaxLen); continue; }
418 if (!key.alt && key.ctrl && !key.shift && key.ch == 'E') { curline.movePos(curline.MaxLen); continue; }
419 continue;
421 switch (key.key) {
422 case TtyEvent.Key.Left:
423 if (!key.alt && !key.shift) {
424 if (key.ctrl) {
425 if (curline.pos > 0) {
426 if (curline[curline.pos-1] <= ' ') {
427 // move to word end
428 while (curline.pos > 0 && curline[curline.pos-1] <= ' ') curline.movePos(-1);
429 } else {
430 // move to word start
431 while (curline.pos > 0 && curline[curline.pos-1] > ' ') curline.movePos(-1);
434 } else {
435 curline.movePos(-1);
438 break;
439 case TtyEvent.Key.Right:
440 if (!key.alt && !key.shift) {
441 if (key.ctrl) {
442 if (curline.pos < curline.length) {
443 if (curline[curline.pos] <= ' ') {
444 // move to word start
445 while (curline.pos < curline.length && curline[curline.pos] <= ' ') curline.movePos(1);
446 } else {
447 // move to word end
448 while (curline.pos < curline.length && curline[curline.pos] > ' ') curline.movePos(1);
451 } else {
452 curline.movePos(1);
455 break;
456 case TtyEvent.Key.Home:
457 if (!key.ctrl && !key.alt && !key.shift) curline.movePos(-curline.MaxLen);
458 break;
459 case TtyEvent.Key.End:
460 if (!key.ctrl && !key.alt && !key.shift) curline.movePos(curline.MaxLen);
461 break;
462 case TtyEvent.Key.Enter:
463 fixCurLine();
464 wrt("\r\n");
465 return Result.Normal;
466 case TtyEvent.Key.Tab:
467 if (!key.ctrl && !key.alt && !key.shift) { fixCurLine(); autocomplete(); continue; }
468 break;
469 case TtyEvent.Key.Up:
470 if (!key.ctrl && !key.alt && !key.shift) {
471 if (history.length == 0) continue;
472 if (hpos == -1) {
473 // store current line so we can return to it
474 lastInput[0..curline.length] = curline[];
475 lastLen = curline.length;
476 hpos = 0;
477 } else if (hpos < history.length-1) {
478 ++hpos;
479 } else {
480 continue;
482 curline.set(history[hpos]);
483 curofs = 0;
485 break;
486 case TtyEvent.Key.Down:
487 if (!key.ctrl && !key.alt && !key.shift) {
488 if (history.length == 0) continue;
489 if (hpos == 0) {
490 // restore previous user line
491 hpos = -1;
492 curline.set(lastInput[0..lastLen]);
493 curofs = 0;
494 } else if (hpos > 0) {
495 --hpos;
496 curline.set(history[hpos]);
497 curofs = 0;
500 break;
501 case TtyEvent.Key.Backspace:
502 if (!key.ctrl && !key.alt && !key.shift) {
503 if (curline.length > 0 && curline.pos > 0) {
504 fixCurLine();
505 curline.backspace(1);
507 } else if (!key.ctrl && key.alt && !key.shift) {
508 delPrevWord();
510 break;
511 case TtyEvent.Key.Delete:
512 if (!key.ctrl && !key.alt && !key.shift) {
513 if (curline.pos < curline.length) {
514 fixCurLine();
515 curline.remove(1);
518 break;
519 case TtyEvent.Key.Char:
520 if (key.ch >= ' ' && key.ch < 127) {
521 if (curline.length < Line.MaxLen) {
522 fixCurLine();
523 curline.insert(cast(char)key.ch);
526 break;
527 default:
532 void autocomplete () {}
534 public:
535 static:
536 void clearOutput () { wrt("\r\x1b[K"); }
538 void wrt (const(char)[] str...) nothrow @nogc {
539 import core.sys.posix.unistd : write;
540 if (str.length) write(1, str.ptr, str.length);
543 void wrtuint (int n) nothrow @nogc {
544 char[32] buf = void;
545 uint pos = buf.length;
546 if (n < 0) n = 0;
547 do {
548 buf.ptr[--pos] = cast(char)(n%10+'0');
549 n /= 10;
550 } while (n != 0);
551 wrt(buf[pos..$]);
554 void beep () { wrt('\x07'); }
558 // ////////////////////////////////////////////////////////////////////////// //
559 version(editline_test) {
560 import iv.strex;
561 void main () {
562 auto el = new EditLine();
563 for (;;) {
564 import std.stdio;
565 auto res = el.readline();
566 writeln;
567 if (res != EditLine.Result.Normal) break;
568 writeln("[", el.get.quote, "]");
569 writeln(el.line.wordCount, " words in line");
570 foreach (const ref wp; el.line.words) writeln(" ", el.line[wp].quote);
571 el.pushCurrentToHistory();