A bare-metal editor written in eLua
(Last modified 28 Jul 2013)

Update 28 Jul 2013
I've updated the editor code to fix some bugs, notably in the cut/paste operation, and to add some features.


I have started using eLua on some of my embedded ARM boards.  For example, you can find some early mbed testing I did described here.

Early on, I realized that developing on the PC but running on a different target was a pain.  I was constantly shuffling files back and forth, and finding the areas of the source file I wanted to edit meant keeping several windows open.  What I really needed was a text editor that ran on my ARM platform.  And since eLua is such a powerful language, why not write the editor in eLua?  Sounded like a terrific way to learn a bit about eLua.

Please note that I am hardly an expert in eLua.  I've only been using the language for a few weeks, and much of that time was spent with learning the build system and adding hardware to my dev boards.  So I'm not holding this editor up as good eLua coding.  Feel free to send in helpful suggestions to improve the editor.


Features of the editor
The editor is less than 600 lines of eLua code, so it is fairly small.  It is a full-screen text editor that allows you to:

Using the editor
You invoke the editor by launching it as a typical eLua script.  From the eLua shell, enter:
    /mmc/edit.lua
(assuming that the editor is on your target board's SD card).  Note that you can also provide the path to a file you want to edit, as in:
    /mmc/edit.lua  /mmc/hello.lua

Here is a screenshot showing the editor running in a Hyperterm window:

The editor showing a Hello program

The editor responds instantly to single-character commands.  For example, if I type '?', I immediately see the editor's help screen:

The editor's help screen

Most of these commands are pretty obvious.  I can open a file with the 'o' command, write a file back out with the 'w' command, and quit the editor with the 'q' command.  Note that if you try to exit the editor after you have changed a file, the editor will warn you about unsaved changes and give you a chance to return to the editor.

The commands t, b, n, N, p, and P allow you to move through the text file.  The 'g' command lets you go to a selected line.

There are two ways to modify a selected line.  The 'r' command completely replaces the line; you type in a whole new line of text.  The 'e' command lets you perform substitutions on the selected line.  Substitutions are always of the form: /<old>/<new>/ or \<old>\<new>\.

The '!' command lets you execute an eLua script without leaving the editor.  This is handy if you want to check your current work.  Note that the '!' command executes a file, it cannot execute your text buffer.  If you want to execute the buffer you've been working on but don't want to overwrite the original file, save your work to a temporary file before using the '!' command.  Note that you do not get to supply arguments to the '!' command; you can only enter the name of a file to execute.

Here is a screenshot where I executed the hello.lua script I had been editing:

Running the hello.lua script


Restrictions and concerns
The editor requires a fair amount of RAM.  Even though it is less than 600 lines long, it will not load on an mbed; not enough memory.  I developed this editor on an eLua binary image for the STM32F4 Discovery board, which has a total of 192 KB of RAM.  This is enough RAM to load the editor and load the editor's source file, so I can edit the editor with the editor.  (Whew!)  I can even execute the editor from within the editor, which is pretty trick, but when I exit the second copy of the editor, I also exit the first copy and return to the eLua shell.

eLua usually uses a serial port as the console, but this editor is very snappy due to eLua's speed.  Screens repaint quickly and navigation through the file is brisk.

Here is the full source for the editor.  Just copy this file to your desktop, start an eLua session on your hardware, then use Xmodem to send this file to your target and save it as edit.lua.

Last updated 28 Jul 2013



lines = {}
pastebuff = {}
showlinenums = true
filepath = ""

CMD_QUIT = string.byte('q')
CMD_OPEN = string.byte('o')
CMD_GOTO = string.byte('g')
CMD_INSERT = string.byte('i')
CHAR_CTRL_Z = 268             -- ctrl-Z
CMD_WRITE = string.byte('w')
CMD_DELETE = string.byte('d')
CMD_TOP = string.byte('t')
CMD_BOTTOM = string.byte('b')
CMD_NEXT_BLK = string.byte('N')
CMD_PREV_BLK = string.byte('P')
CMD_APPEND = string.byte('a')
CMD_HELP = string.byte('?')
CMD_REPLACE = string.byte('r')
CMD_COPY = string.byte('c')
CMD_PASTE = string.byte('v')
CMD_CUT = string.byte('x')
CMD_TOGGLE_NUMS = string.byte('#')
CMD_EDIT = string.byte('e')
CMD_NEXT_LINE = string.byte('n')
CMD_PREV_LINE = string.byte('p')
CMD_EXECUTE = string.byte('!')

CMD_YES = string.byte('y')
CMD_NO = string.byte('n')


local  prevstatusmsg = ""

--LINES_IN_DISPLAY = term.getlines()
LINES_IN_DISPLAY = 22
LINE_ERROR_DISPLAY = LINES_IN_DISPLAY+2
LINE_STATUS = LINES_IN_DISPLAY+1
LINE_CMD = LINES_IN_DISPLAY+2


LINE_ENDING = "\n"


function ShowHelp()
  term.clrscr()
  print("   " .. string.char(CMD_HELP)     .. "   show this help screen")
  print()
  print("   " .. string.char(CMD_OPEN)     .. "   open a file for editing")
  print("   " .. string.char(CMD_WRITE)    .. "   write text to a file")
  print("   " .. string.char(CMD_QUIT)     .. "   exit editor, DOES NOT SAVE!")
  print("  " .. string.char(CMD_TOP) .. "/" .. string.char(CMD_BOTTOM)
                                          .. "  display text at top/bottom of file")
  print("  " .. string.char(CMD_NEXT_LINE) .. "/" .. string.char(CMD_NEXT_BLK)
                                          .. "  display next line/block of text")
  print("  " .. string.char(CMD_PREV_LINE) .. "/" .. string.char(CMD_PREV_BLK)
                                           .. "  display previous line/block of text")
  print("   " .. string.char(CMD_TOGGLE_NUMS) .. "   show/hide line numbers")
  print()
  print("   " .. string.char(CMD_REPLACE)  .. "   replace selected line")
  print("   " .. string.char(CMD_EDIT)     .. "   edit selected line")
  print("   " .. string.char(CMD_INSERT)   .. "   add new lines of text at selected line")
  print("   " .. string.char(CMD_APPEND)   .. "   add new lines of text at end of file")
  print("   " .. string.char(CMD_DELETE)   .. "   delete one or more lines of text")
  print()
  print("   " .. string.char(CMD_COPY)     .. "   copy one or more lines of text to paste buffer")
  print("   " .. string.char(CMD_CUT)      .. "   cut (copy and delete) one or more lines of text")
  print("   " .. string.char(CMD_PASTE)    .. "   insert text from paste buffer")
  print()
  print("   " .. string.char(CMD_GOTO)     .. "   go to selected line")
  print("   " .. string.char(CMD_EXECUTE)  .. "   execute a file as an eLua script")
  print()
  term.print(1, LINE_CMD, "Press Enter to continue: ")
  repeat
    c = term.getchar(term.WAIT)
  until (c == term.KC_ENTER)
end



function ModifyLine(linenum, cmd)
  if (cmd:sub(1,1) == string.sub('?',1,1)) then
    term.clrscr()
    print("Enter a substitution string to edit this line.")
    print("A substitution string has a pattern and a replacement")
    print("string, separated by delimiters.")
    print()
    print("For example:")
    print("  /foo/bar/")
    print("will change the first occurence of 'foo' to 'bar'.")
    print()
    print("You can use either '/' or '\\' as delimiters, but you must")
    print("use the same delimiter throughout the string.")
    print()
    term.print(1, LINE_CMD, "Press Enter to continue: ")
    repeat
      c = term.getchar(term.WAIT)
    until (c == term.KC_ENTER)
  else
    delim = cmd:sub(1,1)
    if ((delim == string.sub('/', 1, 1)) or (delim == string.sub('\\', 1, 1))) then
      patend = cmd:find(delim, 2)
      if (patend ~= nil) then
        pattern = cmd:sub(2, patend-1)
        replend = cmd:find(delim, patend+1)
        if (replend ~= nil) then
          repl = cmd:sub(patend+1, replend-1)
          s = lines[linenum]
          s = string.gsub(s, pattern, repl, 1)
          lines[linenum] = s
          filechanged = true
        end
      end
    end
  end
end



function ShowErrorAndWait(errmsg)
  MoveToLineAndPrint(LINE_ERROR_DISPLAY, errmsg)
  repeat
    c = term.getchar(term.WAIT)
  until (c == term.KC_ENTER)
end



function ShowStatus(statusmsg)
  if (statusmsg == nil) then
    MoveToLineAndPrint(LINE_STATUS, prevstatusmsg)
  else
    MoveToLineAndPrint(LINE_STATUS, statusmsg)
    prevstatusmsg = statusmsg
  end
end



function LoadFile(filepath)
  file = io.open(filepath)
  if (file == nil) then
    ShowErrorAndWait("Cannot open file: " .. filepath)
    return
  end

  repeat
    line = file:read("*l")
    if (line ~= nil) then
      table.insert(lines, line)
    end
  until (line == nil)
  file:close()

  ShowStatus("File: " .. filepath)
  filechanged = false
  firstline = 1
end


function SaveFile(savepath)
  file = io.open(savepath, "w")
  if (file == nil) then
    ShowErrorAndWait("Cannot write to file " .. savepath)
    return
  end

  for i, l in ipairs(lines) do
    file:write(l, LINE_ENDING)
  end
  file:flush()
  file:close()
end


function DisplayFile(firstline)
  term.clrscr()
  for i, str in ipairs(lines) do
    if (i >= firstline) then
      if (showlinenums == true) then
        term.print(1, i - firstline + 1, i .. ":" .. str)
      else
        term.print(1, i - firstline + 1, str)
      end
      if ((i - firstline + 1) == LINES_IN_DISPLAY) then
        break
      end
    end
  end
  ShowStatus()
end




function MoveToLineAndPrint(linenum, str)
  term.moveto(1, linenum)
  term.clreol()
  term.print(str)
end











function GetCmd()
  repeat
    MoveToLineAndPrint(LINE_CMD, "Cmd: ")
    c = term.getchar(term.WAIT)

    if (c == CMD_QUIT) then
      if (filechanged == true) then
        MoveToLineAndPrint(LINE_CMD, "File has changed!  Quit without saving? ")
        repeat
          c = term.getchar(term.WAIT)
        until ((c == CMD_YES) or (c == CMD_NO))
      end
      if ((c == CMD_YES) or (c == CMD_QUIT)) then
        done = 1
        return
      end

    elseif (c == CMD_OPEN) then
        if (filechanged == true) then
          MoveToLineAndPrint(LINE_CMD, "File has changed!  Open without saving? ")
            repeat
              c = term.getchar(term.WAIT)
            until ((c == CMD_YES) or (c == CMD_NO))
        end
        if ((c == CMD_YES) or (c == CMD_OPEN)) then
          MoveToLineAndPrint(LINE_CMD, "File: ")
            tfile = io.read("*l")
            if ((tfile ~= nil) and (tfile ~= "")) then
              lines = {}
          filepath = tfile
              LoadFile(filepath)
              firstline = 1
            end
        end
      DisplayFile(firstline)

      elseif (c == CMD_GOTO) then
      MoveToLineAndPrint(LINE_CMD, "Line: ")
      firstline = io.read("*n")
      if (firstline > #lines - (LINES_IN_DISPLAY+1)) then
        firstline = #lines - LINES_IN_DISPLAY + 1
      end
      if (firstline < 1) then firstline = 1 end
      DisplayFile(firstline)
     
    elseif (c == CMD_INSERT) then
      if (#lines == 0) then
        insertline = 1
      else
        MoveToLineAndPrint(LINE_CMD, "Insert above which line: ")
        insertline = tonumber(io.read("*l"))
      end
      if (insertline ~= nil) then
        firstline = insertline - LINES_IN_DISPLAY/2
        if (firstline < 1) then firstline = 1 end
        repeat
          MoveToLineAndPrint(LINE_CMD, "Text (or ctrl-z): ")
          text = io.read("*l")
          if (text ~= nil) then
            table.insert(lines, insertline, text)
            insertline = insertline + 1
            if ((insertline - firstline) > LINES_IN_DISPLAY) then
              firstline = firstline + 1
            end
            filechanged = true
          end
          DisplayFile(firstline)
        until (text == nil)
      end
      DisplayFile(firstline)
     
    elseif (c == CMD_WRITE) then
      MoveToLineAndPrint(LINE_CMD, "File to write: ")
      savefile = io.read("*l")
      if (savefile ~= nil) then
        SaveFile(savefile)
        filechanged = false
        filepath = savefile
      end
      DisplayFile(firstline)
     
    elseif (c == CMD_DELETE) then
      if (#lines == 0) then
        ShowErrorAndWait("File is empty!")
      else
        MoveToLineAndPrint(LINE_CMD, "Lines (start stop): ")
        start, stop = io.read("*number", "*number")
        if (start ~= nil) then
          if (stop == nil) then
            stop = start
          end
          max = #lines
          if (start > max) then
            ShowErrorAndWait("File only has " .. max .. " lines!")
          else
            for n = start, stop do
              table.remove(lines, start)
              filechanged = true
            end
          end
          firstline = start
        end
        DisplayFile(firstline)
      end
     
    elseif (c == CMD_TOP) then
      t = firstline
      firstline = 1
      if (t ~= firstline) then DisplayFile(firstline) end
     
    elseif (c == CMD_BOTTOM) then
      t = firstline
      firstline = #lines - LINES_IN_DISPLAY + 1
      if (firstline < 1) then firstline = 1 end
      if (t ~= firstline) then DisplayFile(firstline) end
     
    elseif (c == CMD_NEXT_BLK) then
      t = firstline
      firstline = firstline + LINES_IN_DISPLAY
      if (firstline > #lines) then
        firstline = #lines - LINES_IN_DISPLAY + 1
        if (firstline < 1) then firstline = 1 end
      end
      if (t ~= firstline) then DisplayFile(firstline) end

    elseif (c == CMD_PREV_BLK) then
      t = firstline
      firstline = firstline - LINES_IN_DISPLAY
      if (firstline < 1) then firstline = 1 end
      if (t ~= firstline) then DisplayFile(firstline) end
     
    elseif (c == CMD_NEXT_LINE) then
      t = firstline
      if (firstline < (#lines - LINES_IN_DISPLAY)) then
        firstline = firstline + 1
      end
      if (t ~= firstline) then DisplayFile(firstline) end
     
    elseif (c == CMD_PREV_LINE) then
      t = firstline
      if (firstline > 1) then firstline = firstline - 1 end
      if (t ~= firstline) then DisplayFile(firstline) end

    elseif (c == CMD_APPEND) then
      repeat
        MoveToLineAndPrint(LINE_CMD, "Text (or ctrl-z): ")
        text = io.read("*l")
        if (text ~= nil) then
          table.insert(lines, text)
          filechanged = true
        end
        firstline = #lines - LINES_IN_DISPLAY + 1
        if (firstline < 1) then firstline = 1 end
        DisplayFile(firstline)
      until (text == nil)

    elseif (c == CMD_REPLACE) then
      MoveToLineAndPrint(LINE_CMD, "Line: ")
      text = io.read("*l")
      currline = tonumber(text)
      if ((currline == nil) or (currline < 1)) then
      elseif (currline > #lines) then
        ShowErrorAndWait("No such line; file only has " .. #lines .. " lines.")
      else
        firstline = currline - (LINES_IN_DISPLAY /2)
        if (firstline < 1) then firstline = 1 end
        DisplayFile(firstline)
        tmsg = prevstatusmsg
        ShowStatus(currline .. ":" .. lines[currline])
        MoveToLineAndPrint(LINE_CMD, "Text (or ctrl-z): ")
        text = io.read("*l")
        if (text ~= nil) then
          lines[currline] = text
          filechanged = true
        end
      end
      DisplayFile(firstline)
      ShowStatus(tmsg)

      elseif ((c == CMD_COPY) or
              (c == CMD_CUT)) then
      if (#lines == 0) then
        ShowErrorAndWait("File is empty!")
      else
        MoveToLineAndPrint(LINE_CMD, "Lines (start stop): ")
        start, stop = io.read("*number", "*number")
        if (start ~= nil) then
          if (stop == nil) then
            stop = start
          end
          max = #lines
          if (start > max) then
            ShowErrorAndWait("File only has " .. max .. " lines!")
          else
            pastebuff = {}
            for n = start, stop do
              table.insert(pastebuff, lines[n])
              if (c == CMD_CUT) then table.remove(lines, n) end
            end
          end
          firstline = start
        end
        DisplayFile(firstline)
      end

    elseif (c == CMD_INSERT) then
      if (#lines == 0) then
        insertline = 1
      else
        MoveToLineAndPrint(LINE_CMD, "Insert above which line: ")
        insertline = tonumber(io.read("*l"))
      end
      if (insertline ~= nil) then
        repeat
          MoveToLineAndPrint(LINE_CMD, "Text (or ctrl-z): ")
          text = io.read("*l")
          if (text ~= nil) then
            table.insert(lines, insertline, text)
            insertline = insertline + 1
        if (insertline-firstline > 10) then firstline = firstline + 1 end
            filechanged = true
          end
          DisplayFile(firstline)
        until (text == nil)
      end
      DisplayFile(firstline)

    elseif (c == CMD_PASTE) then
        if (#pastebuff == 0) then
          ShowErrorAndWait("No text in paste buffer!")
        else
        if (#lines == 0) then
          insertline = 1
        else
          MoveToLineAndPrint(LINE_CMD, "Paste above which line: ")
          insertline = tonumber(io.read("*l"))
        end
        if (insertline ~= nil) then
            if (insertline > #lines) then
                insertline = #lines + 1
              end
          firstline = insertline - LINES_IN_DISPLAY/2
              if firstline < 1 then firstline = 1 end
              for _, text in ipairs(pastebuff) do
                table.insert(lines, insertline, text)
                insertline = insertline + 1
                filechanged = true
              end
            end
        end
      DisplayFile(firstline)
     
    elseif (c == CMD_TOGGLE_NUMS) then
      showlinenums = not showlinenums
      DisplayFile(firstline)
     
    elseif (c == CMD_EDIT) then
      if (#lines == 0) then
        ShowErrorAndWait("No lines in file!")
        DisplayFile(firstline)
      else
        MoveToLineAndPrint(LINE_CMD, "Edit line: ")
        editline = tonumber(io.read("*l"))
        if (editline ~= nil) then
          if ((editline < 1) or (editline > #lines)) then
            ShowErrorAndPrint(LINE_CMD, "File only has " .. #lines .. " lines!")
            DisplayFile(firstline)
          else
            repeat
              DisplayFile(firstline)
              MoveToLineAndPrint(LINE_CMD, "Subst (" .. editline .. "): ")
              cmdline = io.read("*l")
              if (cmdline ~= nil) then
                ModifyLine(editline, cmdline)
              end
            until (cmdline == nil)
          end
        end
      end

    elseif (c == CMD_HELP) then
      ShowHelp()
      DisplayFile(firstline)

    elseif (c == CMD_EXECUTE) then
      if (filepath ~= "") then
        MoveToLineAndPrint(LINE_CMD, "Execute file (" .. filepath .. "): ")
      else
        MoveToLineAndPrint(LINE_CMD, "Execute file: ")
      end
          tfile = io.read("*l")
          if (tfile ~= nil) then
        if ((tfile == "") and (filepath ~= "")) then
          dofile(filepath)
        elseif (tfile ~= "") then
          dofile(tfile)
        end
      end
      ShowErrorAndWait("Press Enter to continue: ")
        DisplayFile(firstline)

    else
      DisplayFile(1)
    end
  until (done == 1)
end

done = 0
if (#arg >= 1) then
  LoadFile(arg[1])
  filepath = arg[1]
end
DisplayFile(1)
GetCmd()
print()





Summary
This is a fun project and a great way to learn about eLua.  I'm sure there are bugs in the above code, so check back now and then for updates, and send me email if you change something; I might add your changes in, as well.


Home