2 // getline.cs: A command line editor
5 // Miguel de Icaza (miguel@novell.com)
7 // Copyright 2008 Novell, Inc.
9 // Dual-licensed under the terms of the MIT X11 license or the
12 // USE -define:DEMO to build this as a standalone file and test it
15 // Enter an error (a = 1); Notice how the prompt is in the wrong line
16 // This is caused by Stderr not being tracked by System.Console.
18 // Why is Thread.Interrupt not working? Currently I resort to Abort which is too much.
20 // Limitations in System.Console:
21 // Console needs SIGWINCH support of some sort
22 // Console needs a way of updating its position after things have been written
23 // behind its back (P/Invoke puts for example).
24 // System.Console needs to get the DELETE character, and report accordingly.
30 using System.Threading;
31 using System.Reflection;
33 namespace Mono.Terminal {
35 public class LineEditor {
37 public class Completion {
38 public string [] Result;
41 public Completion (string prefix, string [] result)
48 public delegate Completion AutoCompleteHandler (string text, int pos);
50 //static StreamWriter log;
52 // The text being edited.
55 // The text as it is rendered (replaces (char)1 with ^A on display for example).
56 StringBuilder rendered_text;
58 // The prompt specified, and the prompt shown to the user.
62 // The current cursor position, indexes into "text", for an index
63 // into rendered_text, use TextToRenderPos
66 // The row where we started displaying data.
69 // The maximum length that has been displayed on the screen
72 // If we are done editing, this breaks the interactive loop
75 // The thread where the Editing started taking place
78 // Our object that tracks history
81 // The contents of the kill buffer (cut/paste in Emacs parlance)
82 string kill_buffer = "";
84 // The string being searched for
88 // whether we are searching (-1= reverse; 0 = no; 1 = forward)
91 // The position where we found the match.
94 // Used to implement the Kill semantics (multiple Alt-Ds accumulate)
95 KeyHandler last_handler;
97 delegate void KeyHandler ();
100 public ConsoleKeyInfo CKI;
101 public KeyHandler KeyHandler;
103 public Handler (ConsoleKey key, KeyHandler h)
105 CKI = new ConsoleKeyInfo ((char) 0, key, false, false, false);
109 public Handler (char c, KeyHandler h)
112 // Use the "Zoom" as a flag that we only have a character.
113 CKI = new ConsoleKeyInfo (c, ConsoleKey.Zoom, false, false, false);
116 public Handler (ConsoleKeyInfo cki, KeyHandler h)
122 public static Handler Control (char c, KeyHandler h)
124 return new Handler ((char) (c - 'A' + 1), h);
127 public static Handler Alt (char c, ConsoleKey k, KeyHandler h)
129 ConsoleKeyInfo cki = new ConsoleKeyInfo ((char) c, k, false, true, false);
130 return new Handler (cki, h);
135 /// Invoked when the user requests auto-completion using the tab character
138 /// The result is null for no values found, an array with a single
139 /// string, in that case the string should be the text to be inserted
140 /// for example if the word at pos is "T", the result for a completion
141 /// of "ToString" should be "oString", not "ToString".
143 /// When there are multiple results, the result should be the full
146 public AutoCompleteHandler AutoCompleteEvent;
148 static Handler [] handlers;
150 public LineEditor (string name) : this (name, 10) { }
152 public LineEditor (string name, int histsize)
154 handlers = new Handler [] {
155 new Handler (ConsoleKey.Home, CmdHome),
156 new Handler (ConsoleKey.End, CmdEnd),
157 new Handler (ConsoleKey.LeftArrow, CmdLeft),
158 new Handler (ConsoleKey.RightArrow, CmdRight),
159 new Handler (ConsoleKey.UpArrow, CmdHistoryPrev),
160 new Handler (ConsoleKey.DownArrow, CmdHistoryNext),
161 new Handler (ConsoleKey.Enter, CmdDone),
162 new Handler (ConsoleKey.Backspace, CmdBackspace),
163 new Handler (ConsoleKey.Delete, CmdDeleteChar),
164 new Handler (ConsoleKey.Tab, CmdTabOrComplete),
167 Handler.Control ('A', CmdHome),
168 Handler.Control ('E', CmdEnd),
169 Handler.Control ('B', CmdLeft),
170 Handler.Control ('F', CmdRight),
171 Handler.Control ('P', CmdHistoryPrev),
172 Handler.Control ('N', CmdHistoryNext),
173 Handler.Control ('K', CmdKillToEOF),
174 Handler.Control ('Y', CmdYank),
175 Handler.Control ('D', CmdDeleteChar),
176 Handler.Control ('L', CmdRefresh),
177 Handler.Control ('R', CmdReverseSearch),
178 Handler.Control ('G', delegate {} ),
179 Handler.Alt ('B', ConsoleKey.B, CmdBackwardWord),
180 Handler.Alt ('F', ConsoleKey.F, CmdForwardWord),
182 Handler.Alt ('D', ConsoleKey.D, CmdDeleteWord),
183 Handler.Alt ((char) 8, ConsoleKey.Backspace, CmdDeleteBackword),
186 //Handler.Control ('T', CmdDebug),
189 Handler.Control ('Q', delegate { HandleChar (Console.ReadKey (true).KeyChar); })
192 rendered_text = new StringBuilder ();
193 text = new StringBuilder ();
195 history = new History (name, histsize);
197 //if (File.Exists ("log"))File.Delete ("log");
198 //log = File.CreateText ("log");
204 Console.WriteLine ();
210 Console.Write (shown_prompt);
211 Console.Write (rendered_text);
213 int max = System.Math.Max (rendered_text.Length + shown_prompt.Length, max_rendered);
215 for (int i = rendered_text.Length + shown_prompt.Length; i < max_rendered; i++)
217 max_rendered = shown_prompt.Length + rendered_text.Length;
219 // Write one more to ensure that we always wrap around properly if we are at the
226 void UpdateHomeRow (int screenpos)
228 int lines = 1 + (screenpos / Console.WindowWidth);
230 home_row = Console.CursorTop - (lines - 1);
236 void RenderFrom (int pos)
238 int rpos = TextToRenderPos (pos);
241 for (i = rpos; i < rendered_text.Length; i++)
242 Console.Write (rendered_text [i]);
244 if ((shown_prompt.Length + rendered_text.Length) > max_rendered)
245 max_rendered = shown_prompt.Length + rendered_text.Length;
247 int max_extra = max_rendered - shown_prompt.Length;
248 for (; i < max_extra; i++)
253 void ComputeRendered ()
255 rendered_text.Length = 0;
257 for (int i = 0; i < text.Length; i++){
258 int c = (int) text [i];
261 rendered_text.Append (" ");
263 rendered_text.Append ('^');
264 rendered_text.Append ((char) (c + (int) 'A' - 1));
267 rendered_text.Append ((char)c);
271 int TextToRenderPos (int pos)
275 for (int i = 0; i < pos; i++){
292 int TextToScreenPos (int pos)
294 return shown_prompt.Length + TextToRenderPos (pos);
298 get { return prompt; }
299 set { prompt = value; }
304 return (shown_prompt.Length + rendered_text.Length)/Console.WindowWidth;
308 void ForceCursor (int newpos)
312 int actual_pos = shown_prompt.Length + TextToRenderPos (cursor);
313 int row = home_row + (actual_pos/Console.WindowWidth);
314 int col = actual_pos % Console.WindowWidth;
316 if (row >= Console.BufferHeight)
317 row = Console.BufferHeight-1;
318 Console.SetCursorPosition (col, row);
320 //log.WriteLine ("Going to cursor={0} row={1} col={2} actual={3} prompt={4} ttr={5} old={6}", newpos, row, col, actual_pos, prompt.Length, TextToRenderPos (cursor), cursor);
324 void UpdateCursor (int newpos)
326 if (cursor == newpos)
329 ForceCursor (newpos);
332 void InsertChar (char c)
334 int prev_lines = LineCount;
335 text = text.Insert (cursor, c);
337 if (prev_lines != LineCount){
339 Console.SetCursorPosition (0, home_row);
341 ForceCursor (++cursor);
344 ForceCursor (++cursor);
345 UpdateHomeRow (TextToScreenPos (cursor));
357 void CmdTabOrComplete ()
359 bool complete = false;
361 if (AutoCompleteEvent != null){
362 if (TabAtStartCompletes)
365 for (int i = 0; i < cursor; i++){
366 if (!Char.IsWhiteSpace (text [i])){
374 Completion completion = AutoCompleteEvent (text.ToString (), cursor);
375 string [] completions = completion.Result;
376 if (completions == null)
379 int ncompletions = completions.Length;
380 if (ncompletions == 0)
383 if (completions.Length == 1){
384 InsertTextAtCursor (completions [0]);
388 for (int p = 0; p < completions [0].Length; p++){
389 char c = completions [0][p];
392 for (int i = 1; i < ncompletions; i++){
393 if (completions [i].Length < p)
396 if (completions [i][p] != c){
404 InsertTextAtCursor (completions [0].Substring (0, last+1));
406 Console.WriteLine ();
407 foreach (string s in completions){
408 Console.Write (completion.Prefix);
412 Console.WriteLine ();
414 ForceCursor (cursor);
429 UpdateCursor (text.Length);
437 UpdateCursor (cursor-1);
440 void CmdBackwardWord ()
442 int p = WordBackward (cursor);
448 void CmdForwardWord ()
450 int p = WordForward (cursor);
458 if (cursor == text.Length)
461 UpdateCursor (cursor+1);
464 void RenderAfter (int p)
468 ForceCursor (cursor);
476 text.Remove (--cursor, 1);
478 RenderAfter (cursor);
481 void CmdDeleteChar ()
483 // If there is no input, this behaves like EOF
484 if (text.Length == 0){
487 Console.WriteLine ();
491 if (cursor == text.Length)
493 text.Remove (cursor, 1);
495 RenderAfter (cursor);
498 int WordForward (int p)
500 if (p >= text.Length)
504 if (Char.IsPunctuation (text [p]) || Char.IsSymbol (text [p]) || Char.IsWhiteSpace (text[p])){
505 for (; i < text.Length; i++){
506 if (Char.IsLetterOrDigit (text [i]))
509 for (; i < text.Length; i++){
510 if (!Char.IsLetterOrDigit (text [i]))
514 for (; i < text.Length; i++){
515 if (!Char.IsLetterOrDigit (text [i]))
524 int WordBackward (int p)
533 if (Char.IsPunctuation (text [i]) || Char.IsSymbol (text [i]) || Char.IsWhiteSpace (text[i])){
535 if (Char.IsLetterOrDigit (text [i]))
539 if (!Char.IsLetterOrDigit (text[i]))
544 if (!Char.IsLetterOrDigit (text [i]))
556 void CmdDeleteWord ()
558 int pos = WordForward (cursor);
563 string k = text.ToString (cursor, pos-cursor);
565 if (last_handler == CmdDeleteWord)
566 kill_buffer = kill_buffer + k;
570 text.Remove (cursor, pos-cursor);
572 RenderAfter (cursor);
575 void CmdDeleteBackword ()
577 int pos = WordBackward (cursor);
581 string k = text.ToString (pos, cursor-pos);
583 if (last_handler == CmdDeleteBackword)
584 kill_buffer = k + kill_buffer;
588 text.Remove (pos, cursor-pos);
594 // Adds the current line to the history if needed
596 void HistoryUpdateLine ()
598 history.Update (text.ToString ());
601 void CmdHistoryPrev ()
603 if (!history.PreviousAvailable ())
606 HistoryUpdateLine ();
608 SetText (history.Previous ());
611 void CmdHistoryNext ()
613 if (!history.NextAvailable())
616 history.Update (text.ToString ());
617 SetText (history.Next ());
623 kill_buffer = text.ToString (cursor, text.Length-cursor);
624 text.Length = cursor;
626 RenderAfter (cursor);
631 InsertTextAtCursor (kill_buffer);
634 void InsertTextAtCursor (string str)
636 int prev_lines = LineCount;
637 text.Insert (cursor, str);
639 if (prev_lines != LineCount){
640 Console.SetCursorPosition (0, home_row);
642 cursor += str.Length;
643 ForceCursor (cursor);
646 cursor += str.Length;
647 ForceCursor (cursor);
648 UpdateHomeRow (TextToScreenPos (cursor));
652 void SetSearchPrompt (string s)
654 SetPrompt ("(reverse-i-search)`" + s + "': ");
657 void ReverseSearch ()
661 if (cursor == text.Length){
662 // The cursor is at the end of the string
664 p = text.ToString ().LastIndexOf (search);
668 ForceCursor (cursor);
672 // The cursor is somewhere in the middle of the string
673 int start = (cursor == match_at) ? cursor - 1 : cursor;
675 p = text.ToString ().LastIndexOf (search, start);
679 ForceCursor (cursor);
685 // Need to search backwards in history
686 HistoryUpdateLine ();
687 string s = history.SearchBackward (search);
695 void CmdReverseSearch ()
699 last_search = search;
702 SetSearchPrompt ("");
705 if (last_search != "" && last_search != null){
706 search = last_search;
707 SetSearchPrompt (search);
717 void SearchAppend (char c)
720 SetSearchPrompt (search);
723 // If the new typed data still matches the current text, stay here
725 if (cursor < text.Length){
726 string r = text.ToString (cursor, text.Length - cursor);
727 if (r.StartsWith (search))
739 ForceCursor (cursor);
742 void InterruptEdit (object sender, ConsoleCancelEventArgs a)
744 // Do not abort our program:
747 // Interrupt the editor
751 void HandleChar (char c)
764 ConsoleModifiers mod;
766 cki = Console.ReadKey (true);
767 if (cki.Key == ConsoleKey.Escape){
768 cki = Console.ReadKey (true);
770 mod = ConsoleModifiers.Alt;
774 bool handled = false;
776 foreach (Handler handler in handlers){
777 ConsoleKeyInfo t = handler.CKI;
779 if (t.Key == cki.Key && t.Modifiers == mod){
781 handler.KeyHandler ();
782 last_handler = handler.KeyHandler;
784 } else if (t.KeyChar == cki.KeyChar && t.Key == ConsoleKey.Zoom){
786 handler.KeyHandler ();
787 last_handler = handler.KeyHandler;
793 if (last_handler != CmdReverseSearch){
801 if (cki.KeyChar != (char) 0)
802 HandleChar (cki.KeyChar);
806 void InitText (string initial)
808 text = new StringBuilder (initial);
810 cursor = text.Length;
812 ForceCursor (cursor);
815 void SetText (string newtext)
817 Console.SetCursorPosition (0, home_row);
821 void SetPrompt (string newprompt)
823 shown_prompt = newprompt;
824 Console.SetCursorPosition (0, home_row);
826 ForceCursor (cursor);
829 public string Edit (string prompt, string initial)
831 edit_thread = Thread.CurrentThread;
833 Console.CancelKeyPress += InterruptEdit;
836 history.CursorToEnd ();
840 shown_prompt = prompt;
842 history.Append (initial);
847 } catch (ThreadAbortException){
849 Thread.ResetAbort ();
850 Console.WriteLine ();
855 Console.WriteLine ();
857 Console.CancelKeyPress -= InterruptEdit;
864 string result = text.ToString ();
866 history.Accept (result);
868 history.RemoveLast ();
873 public bool TabAtStartCompletes { get; set; }
876 // Emulates the bash-like behavior, where edits done to the
877 // history are recorded
885 public History (string app, int size)
888 throw new ArgumentException ("size");
891 string dir = Environment.GetFolderPath (Environment.SpecialFolder.ApplicationData);
892 //Console.WriteLine (dir);
893 if (!Directory.Exists (dir)){
895 Directory.CreateDirectory (dir);
901 histfile = Path.Combine (dir, app) + ".history";
904 history = new string [size];
905 head = tail = cursor = 0;
907 if (File.Exists (histfile)){
908 using (StreamReader sr = File.OpenText (histfile)){
911 while ((line = sr.ReadLine ()) != null){
921 if (histfile == null)
925 using (StreamWriter sw = File.CreateText (histfile)){
926 int start = (count == history.Length) ? head : tail;
927 for (int i = start; i < start+count; i++){
928 int p = i % history.Length;
929 sw.WriteLine (history [p]);
938 // Appends a value to the history
940 public void Append (string s)
942 //Console.WriteLine ("APPENDING {0} head={1} tail={2}", s, head, tail);
944 head = (head+1) % history.Length;
946 tail = (tail+1 % history.Length);
947 if (count != history.Length)
949 //Console.WriteLine ("DONE: head={1} tail={2}", s, head, tail);
953 // Updates the current cursor location with the string,
954 // to support editing of history items. For the current
955 // line to participate, an Append must be done before.
957 public void Update (string s)
959 history [cursor] = s;
962 public void RemoveLast ()
966 head = history.Length-1;
969 public void Accept (string s)
973 t = history.Length-1;
978 public bool PreviousAvailable ()
980 //Console.WriteLine ("h={0} t={1} cursor={2}", head, tail, cursor);
993 public bool NextAvailable ()
997 int next = (cursor + 1) % history.Length;
1005 // Returns: a string with the previous line contents, or
1006 // nul if there is no data in the history to move to.
1008 public string Previous ()
1010 if (!PreviousAvailable ())
1015 cursor = history.Length - 1;
1017 return history [cursor];
1020 public string Next ()
1022 if (!NextAvailable ())
1025 cursor = (cursor + 1) % history.Length;
1026 return history [cursor];
1029 public void CursorToEnd ()
1039 Console.WriteLine ("Head={0} Tail={1} Cursor={2} count={3}", head, tail, cursor, count);
1040 for (int i = 0; i < history.Length;i++){
1041 Console.WriteLine (" {0} {1}: {2}", i == cursor ? "==>" : " ", i, history[i]);
1046 public string SearchBackward (string term)
1048 for (int i = 0; i < count; i++){
1049 int slot = cursor-i-1;
1051 slot = history.Length+slot;
1052 if (slot >= history.Length)
1054 if (history [slot] != null && history [slot].IndexOf (term) != -1){
1056 return history [slot];
1070 LineEditor le = new LineEditor ("foo");
1073 while ((s = le.Edit ("shell> ", "")) != null){
1074 Console.WriteLine ("----> [{0}]", s);