RichTextConsole

For a C# project I was lacking a console like interface, only in a Windows forms application. So I rolled my own component based on a RichTextBox. It features a history, autocompletion and command sensitive autocompletion. The goal was to make it really similar to how bash handles these things. Since I didn't want to get into threading the inputs communicated through events, and not a synchronous Console.ReadLine() method as in regular consoles.

code

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Linq;
using System.Text;
using System.Windows.Forms;

namespace HotKey.Graphics
{
    public delegate void InputMadeEventHandler (string input);
    public delegate bool SingleCommandCompletedEventHandler (string command);
    public delegate void RestoreAfterSingleCommandEventHandler ();

    public partial class RichTextConsole : RichTextBox
    {
        /// 
        /// When the return key is pressed the current input is taken and issued to any
        /// registrants of the event through the event argument.
        /// 
        public event InputMadeEventHandler OnInputMade;

        /// 
        /// When a command is fully typed out or autocompleted this event is triggered
        /// so that the registrant can set a new list of commands for command sensitive 
        /// auto completion.
        /// 
        public event SingleCommandCompletedEventHandler OnSingleCommandCompleted;

        /// 
        /// Triggers when a command was fully typed out or autocompleted and then
        /// was deleted or changed.
        /// 
        public event RestoreAfterSingleCommandEventHandler OnRestoreAfterSingleCommand;

        #region Public variables
        public List Commands { get; set; }
        public string Prompt { get; set; }
        #endregion

        #region Private variables
        private class internalInput
        {
            public string[] words;
            public int index;
        }
        private int offset;
        private List history = new List();
        private int historyPos = 0;
        private string curLine;
        private bool isSingleCommand = false;
        #endregion

        public RichTextConsole()
        {
            InitializeComponent();
        }

        protected override void OnGotFocus(EventArgs e)
        {
            base.OnGotFocus(e);
            SelectionStart = TextLength;
        }

        protected override void OnKeyDown(KeyEventArgs e)
        {
            internalInput tmp = GetInput();

            if (SelectionStart < offset) 
            {
                if (!(e.Control & e.KeyCode == Keys.C)) // Enable ctrl+c'ing of marked text
                {
                    e.Handled = true;
                    e.SuppressKeyPress = true;
                    return;
                }
            }

            if (e.KeyCode == Keys.Up | e.KeyCode == Keys.Down)
            {
                e.Handled = true;
                if (historyPos == history.Count && e.KeyCode == Keys.Up)
                    curLine = Text.Substring(offset);

                if ((historyPos == history.Count && e.KeyCode == Keys.Down) 
                  | (historyPos == 0 && e.KeyCode == Keys.Up))
                    return;

                historyPos += e.KeyCode == Keys.Up ? -1 : 1;

                if (historyPos == history.Count)
                    SetInput(curLine);
                else
                    SetInput(history.ElementAt(historyPos));   
            }

            if (e.KeyCode == Keys.Left | e.KeyCode == Keys.Back)
            {
                if (SelectionStart == offset) e.Handled = true;
            }

            if (e.KeyCode == Keys.Enter)
            {
                e.Handled = true;
                string str = Text.Substring(offset).Trim();
                if (str.Length == 0)
                    return;
                OnInputMade(str);
                history.Add(str);
                historyPos = history.Count();
                NewPrompt();
            }

            if (e.KeyCode == Keys.Tab)
            {
                e.SuppressKeyPress = true;
                e.Handled = true;

                // Do completion!
                var matches = Commands.Where(c => c.StartsWith(tmp.words[tmp.index]));
                switch (matches.Count())
                {
                    case 0:
                        // Simple
                        break;

                    case 1:
                        // Insert completed command
                        Write(matches.First().Substring(tmp.words[tmp.index].Length));
                        if (SelectionStart == TextLength) Write(" ");
                        break;

                    default:
                        // Show matches
                        var orderedMatches = matches.OrderBy(m => m);
                        int max = orderedMatches.Max(m => m.Length) + 2;
                        int colcount = 80 / max; // Character width for autocomplete suggestions is hardcoded :(
                        string tmpstr = "";
                        for (int i = 0; i < orderedMatches.Count(); i++)
                            tmpstr += (i % colcount == 0 ? "\n" : "") + orderedMatches.ElementAt(i).PadRight(max);
                        Write(tmpstr);
   
                        // Detect how far all matches are equal
                        int x = tmp.words[tmp.index].Length;
                        var charArrays = matches.Select(m => m.ToCharArray());
                        while (
                            x < charArrays.Min(ca => ca.Count()) &&
                            charArrays.All(ca => ca[x] == charArrays.First()[x])
                        ) x++;
                        
                        // Reconstruct GetInput line
                        NewPrompt();
                        for (int i = 0; i < tmp.words.Count(); i++)
                        {
                            if (tmp.index == i)
                                Write(matches.First().Substring(0, x));
                            else
                                Write(tmp.words[i]);
                            if (i != tmp.words.Count() - 1) Write(" ");
                        }
                        SelectionStart = offset + tmp.words.Take(tmp.index).Sum(w => w.Length + 1) + x;
                        break;
                }
            }

            base.OnKeyDown(e);

            // See if there is an exact match for command sensitive auto completion.
            tmp = GetInput();
            if (tmp.words.Count() == 2 && Commands.Contains(tmp.words[0]))
                isSingleCommand = OnSingleCommandCompleted(tmp.words[0]);
            else
                if (isSingleCommand) OnRestoreAfterSingleCommand();
        }

        #region Public functions
        public void NewPrompt()
        {
            if (TextLength == 0)
                Write(Prompt);
            else
                WriteLine(Prompt);
            offset = TextLength;
        }

        public void Write(string text)
        {
            Enabled = false; // Disabling makes the caret disappear while 
                             // updating and prevents flickering.
            int tmp = SelectionStart;
            Text = Text.Insert(SelectionStart, text);
            SelectionStart = tmp + text.Length;
            ScrollToCaret();
            Enabled = true;
            Focus();
        }

        public void WriteLine(string line)
        {
            Enabled = false; // Disabling makes the caret disappear while 
                             // updating and prevents flickering.
            Text += "\n" + line;
            SelectionStart = TextLength;
            ScrollToCaret();
            Enabled = true;
            Focus();
        }

        public void Empty()
        {
            Text = "";
            NewPrompt();
        }
        #endregion

        #region Private methods
        private internalInput GetInput()
        {
            internalInput i = new internalInput();
            i.words = Text.Substring(offset).Split(" ".ToCharArray());
            i.index = 0;
            int sum = 0;
            for (int k = 0; k < i.words.Count(); k++)
            {
                sum += i.words[k].Length + 1;
                if (sum > SelectionStart - offset)
                {
                    i.index = k;
                    break;
                }
            }
            return i;
        }

        private void SetInput(string s)
        {
            if (TextLength > offset)
                Text = Text.Remove(offset);
            Text += s;
            SelectionStart = TextLength;
        }
        #endregion
    }
}