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*/;
26 // ////////////////////////////////////////////////////////////////////////// //
35 int lpos
; // cursor position: [0..len]
36 int llen
; // line length (line can be longer)
37 //uint ofs; // used in text draw
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
;
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;
64 if (hi
> llen
) hi
= llen
;
65 return cline
[cast(uint)lo
..cast(uint)hi
];
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 {
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
;
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;
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
;
96 cline
[lpos
..lpos
+cast(int)s
.length
] = s
[];
97 lpos
+= cast(int)s
.length
;
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;
107 cline
[lpos
..lpos
+cast(int)s
.length
] = s
[];
108 lpos
+= cast(int)s
.length
;
109 if (llen
< lpos
) llen
= lpos
;
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
) {
121 import core
.stdc
.string
: memmove
;
122 memmove(cline
.ptr
+lpos
, cline
.ptr
+lpos
+len
, llen
-(lpos
+len
));
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
;
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
{
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
; }
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;
160 while (wp
.s
< ln
.llen
&& ln
.cline
.ptr
[wp
.s
] <= ' ') ++wp
.s
;
162 if (wp
.e
>= ln
.llen
) return;
163 char qch
= ln
.cline
.ptr
[wp
.e
];
164 if (qch
== '"' || qch
== '\'' || qch
== '`') {
167 while (wp
.e
< ln
.llen
) {
168 char ch
= ln
.cline
.ptr
[wp
.e
++];
170 if (wp
.e
< ln
.llen
) ++wp
.e
;
171 } else if (ch
== qch
) {
176 while (wp
.e
< ln
.llen
&& ln
.cline
.ptr
[wp
.e
] > ' ') ++wp
.e
;
183 int wordCount () const pure nothrow @trusted @nogc {
186 while (!ww
.empty
) { ++res
; ww
.popFront(); }
190 const(char)[] word (int idx
) const pure nothrow @trusted @nogc {
191 if (idx
< 0) return null;
193 while (idx
> 0 && !ww
.empty
) { --idx
; ww
.popFront(); }
194 if (!ww
.empty
) return this[ww
.front
.s
..ww
.front
.e
];
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
) {
203 char ch
= this[wp
.s
];
204 return (ch
== '"' || ch
== '\'' || ch
== '`');
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));
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
;
225 foreach (ref wp
; words
) {
226 if (lpos
>= wp
.s
&& lpos
<= wp
.e
) 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
);
251 if (addSpace
) insert(' ');
258 // replace current word with new, add space
259 bool acWordReplaceSpaced (const(char)[] w
) pure nothrow @trusted @nogc { return acWordReplace(w
, true); }
269 uint historyLimit
= 512;
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..$]; }
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
) {
308 foreach (immutable c
; idx
+1..history
.length
) history
[c
-1] = history
[c
];
310 foreach_reverse (immutable c
; 1..history
.length
) history
[c
] = history
[c
-1];
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];
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
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"
343 // is cursor at eol? special handling
344 if (cpos
== curline
.length
) {
346 if (curofs
< 0) curofs
= 0;
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;
358 // is cursor too far at left?
360 // make wvis left chars visible
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
;
380 wrt(curline
[curofs
..end
]);
384 wrt("\x1b[K\r\x1b[");
385 wrtuint(promptbuf
.length
+(cpos
-curofs
));
392 if (initval
.length
) curline
.set(initval
);
394 auto ttymode
= ttyGetMode();
395 scope(exit
) ttySetMode(ttymode
);
398 void delPrevWord () {
399 if (curline
.pos
> 0) {
401 while (curline
.pos
> 0 && curline
[curline
.pos
-1] <= ' ') curline
.backspace(1);
402 while (curline
.pos
> 0 && curline
[curline
.pos
-1] > ' ') curline
.backspace(1);
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; }
422 case TtyEvent
.Key
.Left
:
423 if (!key
.alt
&& !key
.shift
) {
425 if (curline
.pos
> 0) {
426 if (curline
[curline
.pos
-1] <= ' ') {
428 while (curline
.pos
> 0 && curline
[curline
.pos
-1] <= ' ') curline
.movePos(-1);
430 // move to word start
431 while (curline
.pos
> 0 && curline
[curline
.pos
-1] > ' ') curline
.movePos(-1);
439 case TtyEvent
.Key
.Right
:
440 if (!key
.alt
&& !key
.shift
) {
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);
448 while (curline
.pos
< curline
.length
&& curline
[curline
.pos
] > ' ') curline
.movePos(1);
456 case TtyEvent
.Key
.Home
:
457 if (!key
.ctrl
&& !key
.alt
&& !key
.shift
) curline
.movePos(-curline
.MaxLen
);
459 case TtyEvent
.Key
.End
:
460 if (!key
.ctrl
&& !key
.alt
&& !key
.shift
) curline
.movePos(curline
.MaxLen
);
462 case TtyEvent
.Key
.Enter
:
465 return Result
.Normal
;
466 case TtyEvent
.Key
.Tab
:
467 if (!key
.ctrl
&& !key
.alt
&& !key
.shift
) { fixCurLine(); autocomplete(); continue; }
469 case TtyEvent
.Key
.Up
:
470 if (!key
.ctrl
&& !key
.alt
&& !key
.shift
) {
471 if (history
.length
== 0) continue;
473 // store current line so we can return to it
474 lastInput
[0..curline
.length
] = curline
[];
475 lastLen
= curline
.length
;
477 } else if (hpos
< history
.length
-1) {
482 curline
.set(history
[hpos
]);
486 case TtyEvent
.Key
.Down
:
487 if (!key
.ctrl
&& !key
.alt
&& !key
.shift
) {
488 if (history
.length
== 0) continue;
490 // restore previous user line
492 curline
.set(lastInput
[0..lastLen
]);
494 } else if (hpos
> 0) {
496 curline
.set(history
[hpos
]);
501 case TtyEvent
.Key
.Backspace
:
502 if (!key
.ctrl
&& !key
.alt
&& !key
.shift
) {
503 if (curline
.length
> 0 && curline
.pos
> 0) {
505 curline
.backspace(1);
507 } else if (!key
.ctrl
&& key
.alt
&& !key
.shift
) {
511 case TtyEvent
.Key
.Delete
:
512 if (!key
.ctrl
&& !key
.alt
&& !key
.shift
) {
513 if (curline
.pos
< curline
.length
) {
519 case TtyEvent
.Key
.Char
:
520 if (key
.ch
>= ' ' && key
.ch
< 127) {
521 if (curline
.length
< Line
.MaxLen
) {
523 curline
.insert(cast(char)key
.ch
);
532 void autocomplete () {}
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 {
545 uint pos
= buf
.length
;
548 buf
.ptr
[--pos
] = cast(char)(n
%10+'0');
554 void beep () { wrt('\x07'); }
558 // ////////////////////////////////////////////////////////////////////////// //
559 version(editline_test
) {
562 auto el
= new EditLine();
565 auto res
= el
.readline();
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();