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, version 3 of the License ONLY.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
18 module iv
.editline
/*is aliced*/;
25 // ////////////////////////////////////////////////////////////////////////// //
34 int lpos
; // cursor position: [0..len]
35 int llen
; // line length (line can be longer)
36 //uint ofs; // used in text draw
42 @property int pos () const pure nothrow @safe @nogc { pragma(inline
, true); return lpos
; }
43 @property void pos (int npos
) pure nothrow @safe @nogc { if (npos
< 0) npos
= 0; if (npos
> llen
) npos
= llen
; lpos
= npos
; }
44 void movePos (int delta
) {
45 if (delta
< -MaxLen
) delta
= -MaxLen
;
46 if (delta
> MaxLen
) delta
= MaxLen
;
51 @property int length () const pure nothrow @safe @nogc { pragma(inline
, true); return llen
; }
53 char opIndex (long pos
) const pure nothrow @trusted @nogc { pragma(inline
, true); return (pos
>= 0 && pos
< llen ? cline
.ptr
[cast(uint)pos
] : 0); }
54 const(char)[] opIndex() (in auto ref WordPos wp
) const pure nothrow @safe @nogc { pragma(inline
, true); return this[wp
.s
..wp
.e
]; }
56 // do not slice it, buffer WILL change
57 const(char)[] opSlice () const pure nothrow @safe @nogc { pragma(inline
, true); return cline
[0..llen
]; }
58 const(char)[] opSlice (long lo
, long hi
) const pure nothrow @safe @nogc {
59 //pragma(inline, true);
60 if (lo
>= hi
) return null;
61 if (hi
<= 0 || lo
>= llen
) return null;
63 if (hi
> llen
) hi
= llen
;
64 return cline
[cast(uint)lo
..cast(uint)hi
];
68 void clear () pure nothrow @trusted @nogc { lpos
= llen
= 0; }
70 // crop line at current position
71 void crop () pure nothrow @trusted @nogc { llen
= lpos
; }
73 // set current line, move cursor to end; crop if length is too big
74 // return `false` if new line was cropped
75 bool set (const(char)[] s
...) pure nothrow @trusted @nogc {
77 if (s
.length
> MaxLen
) { s
= s
[0..MaxLen
]; res
= false; }
78 if (s
.length
) cline
[0..s
.length
] = s
[];
79 lpos
= llen
= cast(int)s
.length
;
83 // insert chars at cursor position, move cursor
84 // return `false` if there is no room, line is not modified in this case
85 bool insert (const(char)[] s
...) pure nothrow @trusted @nogc {
86 if (s
.length
== 0) return true;
87 if (s
.length
> MaxLen || llen
+s
.length
> MaxLen
) return false;
90 import core
.stdc
.string
: memmove
;
91 memmove(cline
.ptr
+lpos
+cast(int)s
.length
, cline
.ptr
+lpos
, llen
-lpos
);
93 llen
+= cast(int)s
.length
;
95 cline
[lpos
..lpos
+cast(int)s
.length
] = s
[];
96 lpos
+= cast(int)s
.length
;
100 // replace chars at cursor position, move cursor; does appending
101 // return `false` if there is no room, line is not modified in this case
102 bool replace (const(char)[] s
...) pure nothrow @trusted @nogc {
103 if (s
.length
== 0) return true;
104 if (s
.length
> MaxLen || lpos
+s
.length
> MaxLen
) return false;
106 cline
[lpos
..lpos
+cast(int)s
.length
] = s
[];
107 lpos
+= cast(int)s
.length
;
108 if (llen
< lpos
) llen
= lpos
;
112 // remove chars at cursor position (delete), don't move cursor
113 void remove (int len
) pure nothrow @trusted @nogc {
114 if (len
< 1 || lpos
>= llen
) return;
115 if (len
> MaxLen
) len
= MaxLen
;
116 if (lpos
+len
>= llen
) {
120 import core
.stdc
.string
: memmove
;
121 memmove(cline
.ptr
+lpos
, cline
.ptr
+lpos
+len
, llen
-(lpos
+len
));
127 // delete chars at cursor position (backspace), move cursor
128 void backspace (int len
) pure nothrow @trusted @nogc {
129 if (len
< 1 || lpos
< 1) return;
130 if (len
> lpos
) len
= lpos
;
135 // //// something to make programmer's life easier //// //
137 static struct WordPos
{
138 int s
, e
; // start and end position, suitable for slicing
139 @property bool valid () const pure nothrow @safe @nogc { pragma(inline
, true); return (s
>= 0 && s
< e
); }
140 @property int length () const pure nothrow @safe @nogc { pragma(inline
, true); return e
-s
; }
143 // word positions range
144 auto words () inout pure nothrow @trusted @nogc {
145 static struct Result
{
149 this (in Line aln
) pure nothrow @safe @nogc { ln
= aln
; popFront(); }
150 this() (in Line aln
, in auto ref WordPos awp
) pure nothrow @safe @nogc { ln
= aln
; wp
.s
= awp
.s
; wp
.e
= awp
.e
; }
152 @property const(char)[] word () const pure nothrow @safe @nogc { pragma(inline
, true); return ln
[wp
.s
..wp
.e
]; }
153 @property WordPos
front () const pure nothrow @safe @nogc { pragma(inline
, true); return WordPos(wp
.s
, wp
.e
); }
154 @property bool empty () const pure nothrow @safe @nogc { pragma(inline
, true); return (wp
.s
>= ln
.llen
); }
155 @property auto save () const pure nothrow @safe @nogc { pragma(inline
, true); return Result(ln
, wp
); }
156 @property void popFront () pure nothrow @trusted @nogc {
157 if (wp
.s
>= ln
.llen
) return;
159 while (wp
.s
< ln
.llen
&& ln
.cline
.ptr
[wp
.s
] <= ' ') ++wp
.s
;
161 if (wp
.e
>= ln
.llen
) return;
162 char qch
= ln
.cline
.ptr
[wp
.e
];
163 if (qch
== '"' || qch
== '\'' || qch
== '`') {
166 while (wp
.e
< ln
.llen
) {
167 char ch
= ln
.cline
.ptr
[wp
.e
++];
169 if (wp
.e
< ln
.llen
) ++wp
.e
;
170 } else if (ch
== qch
) {
175 while (wp
.e
< ln
.llen
&& ln
.cline
.ptr
[wp
.e
] > ' ') ++wp
.e
;
182 int wordCount () const pure nothrow @trusted @nogc {
185 while (!ww
.empty
) { ++res
; ww
.popFront(); }
189 const(char)[] word (int idx
) const pure nothrow @trusted @nogc {
190 if (idx
< 0) return null;
192 while (idx
> 0 && !ww
.empty
) { --idx
; ww
.popFront(); }
193 if (!ww
.empty
) return this[ww
.front
.s
..ww
.front
.e
];
197 // assume that `pos` is inside word, return `true` if word starts with quote
198 bool wordQuoted (int pos
) const pure nothrow @trusted @nogc {
199 foreach (const ref wp
; words
) {
200 if (pos
>= wp
.s
&& pos
< wp
.e
) {
202 char ch
= this[wp
.s
];
203 return (ch
== '"' || ch
== '\'' || ch
== '`');
209 // we can autocomplete empty line or any word if we're at it's end
210 bool canAutocomplete () const pure nothrow @trusted @nogc {
211 if (llen
== 0) return true;
212 if (llen
== MaxLen
) return false;
213 if (lpos
== 0) return false; // we have some text, so can't
214 if (lpos
== llen
) return (cline
.ptr
[lpos
-1] <= ' ' ||
!wordQuoted(lpos
-1));
216 return (cline
.ptr
[lpos
] <= ' ' && cline
.ptr
[lpos
-1] > ' ' && !wordQuoted(lpos
-1));
219 // get word number for autocompletion
220 int acWordNum () const pure nothrow @trusted @nogc {
221 if (!canAutocomplete
) return -1;
222 if (lpos
== llen
&& cline
.ptr
[lpos
-1] <= ' ') return wordCount
;
224 foreach (ref wp
; words
) {
225 if (lpos
>= wp
.s
&& lpos
<= wp
.e
) return idx
;
231 // get word for autocompletion
232 WordPos
acWordPos () const pure nothrow @trusted @nogc {
233 if (!canAutocomplete
) return WordPos();
234 if (lpos
== llen
&& cline
.ptr
[lpos
-1] <= ' ') return WordPos(llen
, llen
);
235 foreach (ref wp
; words
) if (lpos
>= wp
.s
&& lpos
<= wp
.e
) return wp
;
236 return WordPos(llen
, llen
);
239 // replace current with new
240 bool acWordReplace (const(char)[] w
, bool addSpace
=false) pure nothrow @trusted @nogc {
241 if (w
.length
== 0 || w
.length
> MaxLen ||
!canAutocomplete
) return false;
242 foreach (const ref wp
; words
) {
243 if (lpos
>= wp
.s
&& lpos
<= wp
.e
) {
244 // check if we should add space
245 if (addSpace
) addSpace
= (wp
.e
>= llen || cline
.ptr
[wp
.e
] > ' ');
246 int addlen
= (addSpace ?
1 : 0);
247 if (llen
-wp
.length
+w
.length
+addlen
> MaxLen
) return false;
248 backspace(wp
.length
);
250 if (addSpace
) insert(' ');
257 // replace current word with new, add space
258 bool acWordReplaceSpaced (const(char)[] w
) pure nothrow @trusted @nogc { return acWordReplace(w
, true); }
268 uint historyLimit
= 512;
276 this () { promptbuf
= ">".dup
; curline
= new Line(); }
278 final @property inout(Line
) line () inout pure nothrow @safe @nogc { pragma(inline
, true); return curline
; }
280 // const(char)[] returns slice of internal buffer, don't store it!
281 final @property T
get(T
=string
) () if (is(T
: const(char)[])) {
282 static if (is(T
== string
)) return curline
[].idup
;
283 else static if (is(T
== const(char)[])) return curline
[];
284 else return curline
[].dup
;
287 final @property string
prompt () { return promptbuf
.idup
; }
288 final @property prompt (const(char)[] pt
) {
289 promptbuf
.length
= 0;
290 promptbuf
.assumeSafeAppend
;
291 if (pt
.length
> 61) { promptbuf
~= "..."; pt
= pt
[$-61..$]; }
295 final void pushCurrentToHistory () { pushToHistory(get
!string
); }
297 final void pushToHistory(T
: const(char)[]) (T s
) {
298 static if (is(T
== typeof(null))) string hs
= null;
299 else static if (is(T
== string
)) string hs
= s
.xstrip
;
300 else string hs
= s
.xstrip
.idup
;
301 if (hs
.length
== 0) return;
302 // find duplicate and remove it
303 foreach (immutable idx
, string st
; history
) {
304 if (st
.xstrip
== hs
) {
307 foreach (immutable c
; idx
+1..history
.length
) history
[c
-1] = history
[c
];
309 foreach_reverse (immutable c
; 1..history
.length
) history
[c
] = history
[c
-1];
316 if (history
.length
> historyLimit
) history
.length
= historyLimit
;
317 else if (history
.length
< historyLimit
) history
.length
+= 1;
318 foreach_reverse (immutable c
; 1..history
.length
) history
[c
] = history
[c
-1];
322 final Result
readline (const(char)[] initval
=null) {
323 char[Line
.MaxLen
] lastInput
; // stored for history walks
324 uint lastLen
; // stored for history walks
325 int hpos
= -1; // -1: current
326 int curofs
; // output offset
334 auto wdt
= ttyWidth
-1;
335 if (wdt
< 16) wdt
= 16;
336 const(char)[] cline
= (hpos
< 0 ? curline
[] : history
[hpos
]);
337 if (wdt
<= promptbuf
.length
+3) wdt
= 3; else wdt
-= cast(int)promptbuf
.length
;
338 int wvis
= (wdt
< 10 ? wdt
-1 : 8);
339 int cpos
= curline
.pos
;
340 // special handling for negative offset: it means "make left part visible"
342 // is cursor at eol? special handling
343 if (cpos
== curline
.length
) {
345 if (curofs
< 0) curofs
= 0;
347 // make wvis char after cursor visible
348 curofs
= cpos
-wdt
+wvis
;
349 if (curofs
< 0) curofs
= 0;
350 // i did something wrong here...
351 if (curofs
>= curline
.length
) {
352 curofs
= curline
.length
-wdt
;
353 if (curofs
< 0) curofs
= 0;
357 // is cursor too far at left?
359 // make wvis left chars visible
361 if (curofs
< 0) curofs
= 0;
363 // is cursor too far at right?
364 if (curofs
+wdt
<= cpos
) {
365 // make wvis right chars visible
366 curofs
= cpos
-wdt
+wvis
;
367 if (curofs
< 0) curofs
= 0;
368 if (curofs
>= curline
.length
) {
369 curofs
= curline
.length
-wdt
;
370 if (curofs
< 0) curofs
= 0;
374 assert(curofs
>= 0 && curofs
<= curline
.length
);
375 int end
= curofs
+wdt
;
376 if (end
> curline
.length
) end
= curline
.length
;
379 wrt(curline
[curofs
..end
]);
383 wrt("\x1b[K\r\x1b[");
384 wrtuint(promptbuf
.length
+(cpos
-curofs
));
391 if (initval
.length
) curline
.set(initval
);
393 auto ttymode
= ttyGetMode();
394 scope(exit
) ttySetMode(ttymode
);
397 void delPrevWord () {
398 if (curline
.pos
> 0) {
400 while (curline
.pos
> 0 && curline
[curline
.pos
-1] <= ' ') curline
.backspace(1);
401 while (curline
.pos
> 0 && curline
[curline
.pos
-1] > ' ') curline
.backspace(1);
407 auto key
= ttyReadKey();
408 if (key
.key
== TtyEvent
.Key
.Error
) { curline
.clear(); return Result
.CtrlD
; }
409 if (key
.key
== TtyEvent
.Key
.Unknown
) continue;
410 if (key
.key
== TtyEvent
.Key
.ModChar
) {
411 if (!key
.alt
&& key
.ctrl
&& !key
.shift
&& key
.ch
== 'W') { delPrevWord(); continue; }
412 if (!key
.alt
&& key
.ctrl
&& !key
.shift
&& key
.ch
== 'C') { curline
.clear(); return Result
.CtrlC
; }
413 if (!key
.alt
&& key
.ctrl
&& !key
.shift
&& key
.ch
== 'D') { curline
.clear(); return Result
.CtrlD
; }
414 if (!key
.alt
&& key
.ctrl
&& !key
.shift
&& key
.ch
== 'K') { fixCurLine(); curline
.crop(); continue; }
415 if (!key
.alt
&& key
.ctrl
&& !key
.shift
&& key
.ch
== 'Y') { fixCurLine(); curline
.clear(); continue; }
416 if (!key
.alt
&& key
.ctrl
&& !key
.shift
&& key
.ch
== 'A') { curline
.movePos(-curline
.MaxLen
); continue; }
417 if (!key
.alt
&& key
.ctrl
&& !key
.shift
&& key
.ch
== 'E') { curline
.movePos(curline
.MaxLen
); continue; }
421 case TtyEvent
.Key
.Left
:
422 if (!key
.alt
&& !key
.shift
) {
424 if (curline
.pos
> 0) {
425 if (curline
[curline
.pos
-1] <= ' ') {
427 while (curline
.pos
> 0 && curline
[curline
.pos
-1] <= ' ') curline
.movePos(-1);
429 // move to word start
430 while (curline
.pos
> 0 && curline
[curline
.pos
-1] > ' ') curline
.movePos(-1);
438 case TtyEvent
.Key
.Right
:
439 if (!key
.alt
&& !key
.shift
) {
441 if (curline
.pos
< curline
.length
) {
442 if (curline
[curline
.pos
] <= ' ') {
443 // move to word start
444 while (curline
.pos
< curline
.length
&& curline
[curline
.pos
] <= ' ') curline
.movePos(1);
447 while (curline
.pos
< curline
.length
&& curline
[curline
.pos
] > ' ') curline
.movePos(1);
455 case TtyEvent
.Key
.Home
:
456 if (!key
.ctrl
&& !key
.alt
&& !key
.shift
) curline
.movePos(-curline
.MaxLen
);
458 case TtyEvent
.Key
.End
:
459 if (!key
.ctrl
&& !key
.alt
&& !key
.shift
) curline
.movePos(curline
.MaxLen
);
461 case TtyEvent
.Key
.Enter
:
464 return Result
.Normal
;
465 case TtyEvent
.Key
.Tab
:
466 if (!key
.ctrl
&& !key
.alt
&& !key
.shift
) { fixCurLine(); autocomplete(); continue; }
468 case TtyEvent
.Key
.Up
:
469 if (!key
.ctrl
&& !key
.alt
&& !key
.shift
) {
470 if (history
.length
== 0) continue;
472 // store current line so we can return to it
473 lastInput
[0..curline
.length
] = curline
[];
474 lastLen
= curline
.length
;
476 } else if (hpos
< history
.length
-1) {
481 curline
.set(history
[hpos
]);
485 case TtyEvent
.Key
.Down
:
486 if (!key
.ctrl
&& !key
.alt
&& !key
.shift
) {
487 if (history
.length
== 0) continue;
489 // restore previous user line
491 curline
.set(lastInput
[0..lastLen
]);
493 } else if (hpos
> 0) {
495 curline
.set(history
[hpos
]);
500 case TtyEvent
.Key
.Backspace
:
501 if (!key
.ctrl
&& !key
.alt
&& !key
.shift
) {
502 if (curline
.length
> 0 && curline
.pos
> 0) {
504 curline
.backspace(1);
506 } else if (!key
.ctrl
&& key
.alt
&& !key
.shift
) {
510 case TtyEvent
.Key
.Delete
:
511 if (!key
.ctrl
&& !key
.alt
&& !key
.shift
) {
512 if (curline
.pos
< curline
.length
) {
518 case TtyEvent
.Key
.Char
:
519 if (key
.ch
>= ' ' && key
.ch
< 127) {
520 if (curline
.length
< Line
.MaxLen
) {
522 curline
.insert(cast(char)key
.ch
);
531 void autocomplete () {}
535 void clearOutput () { wrt("\r\x1b[K"); }
537 void wrt (const(char)[] str...) nothrow @nogc {
538 import core
.sys
.posix
.unistd
: write
;
539 if (str.length
) write(1, str.ptr
, str.length
);
542 void wrtuint (int n
) nothrow @nogc {
544 uint pos
= buf
.length
;
547 buf
.ptr
[--pos
] = cast(char)(n
%10+'0');
553 void beep () { wrt('\x07'); }
557 // ////////////////////////////////////////////////////////////////////////// //
558 version(editline_test
) {
561 auto el
= new EditLine();
564 auto res
= el
.readline();
566 if (res
!= EditLine
.Result
.Normal
) break;
567 writeln("[", el
.get
.quote
, "]");
568 writeln(el
.line
.wordCount
, " words in line");
569 foreach (const ref wp
; el
.line
.words
) writeln(" ", el
.line
[wp
].quote
);
570 el
.pushCurrentToHistory();