diff --git a/src/Acorn.jl b/src/Acorn.jl
index e25e2c3..dfc4906 100644
--- a/src/Acorn.jl
+++ b/src/Acorn.jl
@@ -1,5 +1,61 @@
module Acorn
-# package code goes here
+include("terminal.jl")
+
+# Configuration submodule
+include("EditorConfig.jl")
+using .EditorConfig
+
+include("row.jl")
+include("cmds/Command.jl")
+include("editor.jl")
+
+
+# Load commands
+include("cmds/open.jl")
+include("cmds/save.jl")
+include("cmds/quit.jl")
+include("cmds/find.jl")
+include("cmds/help.jl")
+include("cmds/bind.jl")
+include("cmds/set.jl")
+include("cmds/echo.jl")
+
+function acorn(filename::String, rel=true)
+ ed = Editor()
+
+ rel && (filename = abspath(filename))
+
+ editorOpen(ed, filename)
+
+ setStatusMessage(ed, "HELP: Ctrl-Q: quit | Ctrl-S: save | Ctrl-F: find")
+
+ Base.Terminals.raw!(ed.term, true)
+
+ try
+ while !ed.quit
+ refreshScreen(ed)
+ processKeypress(ed)
+ end
+ catch ex
+ editorQuit(ed, force=true)
+ throw(ex)
+ end
+
+ Base.Terminals.raw!(ed.term, false)
+
+ return nothing
+end
+
+function acorn()
+ # If a file is given, open it
+ if length(ARGS) > 0
+ filename = ARGS[1]
+ acorn(filename, rel=false)
+ end
+end
+
+export
+ acorn
end # module
diff --git a/src/EditorConfig.jl b/src/EditorConfig.jl
new file mode 100644
index 0000000..13d847f
--- /dev/null
+++ b/src/EditorConfig.jl
@@ -0,0 +1,133 @@
+module EditorConfig
+
+
+###############
+## Parameter ##
+###############
+
+type Parameter{T}
+ value::T
+ validate::Union{Function, Void}
+ desc::String # Used when calling help
+end
+
+validate(p::Parameter) = p.validate == nothing ? true : p.validate(p.value)
+
+function set(p::Parameter, x)
+ old_val = p.value
+
+ # Correct type?
+ try
+ p.value = x
+ catch Exception
+ p.value = old_val
+ throw(ArgumentError("Invalid parameter assignment: $sym, $x"))
+ end
+
+ # Valid?
+ if !validate(p)
+ p.value = old_val
+ throw(ArgumentError("Invalid parameter assignmnt: $sym, $x"))
+ end
+end
+
+
+
+
+############
+## CONFIG ##
+############
+
+const CONFIG = Dict{Symbol, Parameter}()
+
+
+
+function configSet(sym::Symbol, x)
+ # Check if parameter exists
+ if !(sym in keys(CONFIG))
+ throw(ArgumentError("No parameter named $sym"))
+ end
+
+ p = CONFIG[sym]
+ set(p, x)
+
+ CONFIG[sym] = p
+end
+
+configGet(sym::Symbol) = CONFIG[sym].value
+configDesc(sym::Symbol) = CONFIG[sym].desc
+configIsParam(sym::Symbol) = sym in keys(CONFIG)
+
+
+
+
+
+##################
+## KEY BINDINGS ##
+##################
+
+
+const KEY_BINDINGS = Dict{UInt32, String}()
+
+function setKeyBinding(c::Char, s::String)
+ KEY_BINDINGS[UInt32(c) & 0x1f] = s
+end
+
+function getKeyBinding(c::Char)
+ get(KEY_BINDINGS, UInt32(c) & 0x1f, "")
+end
+
+isKeyBound(c::Char) = (UInt32(c) & 0x1f) in keys(KEY_BINDINGS)
+
+
+
+
+########################
+## DEFAULT PARAMETERS ##
+########################
+
+CONFIG[:tab_stop] = Parameter{Int}(4, n-> n > 0 && n <= 16, "visual size of a tab in number of spaces")
+CONFIG[:expandtab] = Parameter{Bool}(false, nothing, "if true, use spaces instead of tabs when pressing ")
+
+
+##########################
+## DEFAULT KEY BINDINGS ##
+##########################
+
+setKeyBinding('s', "save")
+setKeyBinding('o', "open")
+setKeyBinding('f', "find")
+setKeyBinding('q', "quit")
+
+
+# # In juliarc:
+# # using Acorn
+#
+#
+# ## KEY_BINDINGS
+# #Acorn.addKeyBinding('f', "find")
+# Acorn.addKeyBinding('o', "open")
+# Acorn.addKeyBinding('s', "save")
+#
+# """
+# bind f find
+# bind o open
+# bind s save
+# set tab_stop 4
+# set expandtab true
+# """
+
+# TODO:
+# hard/soft tabs
+# show hidden characters
+
+export
+ configGet,
+ configSet,
+ configIsParam,
+ configDesc,
+ setKeyBinding,
+ getKeyBinding,
+ isKeyBound
+
+end #module
diff --git a/src/cmds/.quit.jl.swp b/src/cmds/.quit.jl.swp
new file mode 100644
index 0000000..add5738
Binary files /dev/null and b/src/cmds/.quit.jl.swp differ
diff --git a/src/cmds/Command.jl b/src/cmds/Command.jl
new file mode 100644
index 0000000..652f029
--- /dev/null
+++ b/src/cmds/Command.jl
@@ -0,0 +1,11 @@
+type Command
+ name::Symbol
+ help::String
+ cmd::Function
+end
+
+const COMMANDS = Dict{Symbol, Command}()
+
+function addCommand(name::Symbol, cmd::Function; help::String="")
+ COMMANDS[name] = Command(name, help, cmd)
+end
diff --git a/src/cmds/bind.jl b/src/cmds/bind.jl
new file mode 100644
index 0000000..69bf321
--- /dev/null
+++ b/src/cmds/bind.jl
@@ -0,0 +1,21 @@
+function commandBind(ed::Editor, args::String)
+ arg_arr = strip.(split(args, ' ', limit=2))
+
+ if length(arg_arr) != 2
+ setStatusMessage(ed, "bind: command requires two arguments")
+ return
+ elseif !( length(arg_arr[1]) == 1 && isalpha(arg_arr[1][1]) )
+ setStatusMessage(ed, "bind: first arg must be a letter")
+ return
+ # elseif !Base.isidentifier(arg_arr[2])
+ # setStatusMessage(ed, "bind: command $(arg_arr[2]) is invalid")
+ # return
+ end
+
+ key = lowercase(arg_arr[1][1])
+
+ setKeyBinding(key, join(arg_arr[2]))
+end
+
+addCommand(:bind, commandBind,
+ help="bind : bind ctrl- to command ")
diff --git a/src/cmds/echo.jl b/src/cmds/echo.jl
new file mode 100644
index 0000000..ec5efe8
--- /dev/null
+++ b/src/cmds/echo.jl
@@ -0,0 +1,6 @@
+function commandEcho(ed::Editor, args::String)
+ setStatusMessage(ed, args)
+end
+
+addCommand(:echo, commandEcho,
+ help="echo : set the status message to ")
diff --git a/src/cmds/find.jl b/src/cmds/find.jl
new file mode 100644
index 0000000..a076a2e
--- /dev/null
+++ b/src/cmds/find.jl
@@ -0,0 +1,86 @@
+
+function initFind(ed::Editor)
+ ed.params[:find] = Dict{Symbol, Any}()
+
+ # 1 indexed, 0 none
+ ed.params[:find][:last_match] = 0
+ ed.params[:find][:direction] = 1
+end
+
+
+function findCallback(ed::Editor, query::String, key::Char)
+ # If the params have not been created, init them
+ if !(:find in keys(ed.params))
+ initFind(ed)
+ end
+
+ last_match = ed.params[:find][:last_match]
+ direction = ed.params[:find][:direction]
+
+ if key == '\r' || key == '\x1b'
+ last_match = 0
+ direction = 1
+ return
+ elseif key == ARROW_RIGHT || key == ARROW_DOWN
+ direction = 1
+ elseif key == ARROW_LEFT || key == ARROW_UP
+ direction = -1
+ else
+ last_match = 0
+ direction = 1
+ end
+
+ last_match == 0 && (direction = 1)
+ current = last_match
+ for i = 1:length(ed.rows)
+ current += direction
+
+ # Bounds check
+ if current == 0
+ # At begenning of document? Go to end
+ current = length(ed.rows)
+ elseif current == length(ed.rows)+1
+ # At end of doc? Got to start
+ current = 1
+ end
+
+ row = ed.rows[current]
+ loc = search(row.chars, query)
+ if loc != 0:-1
+ last_match = current
+ ed.csr.y = current
+ ed.csr.x = first(loc)#charX(row, first(loc))
+ ed.rowoff = length(ed.rows)
+ break
+ end
+ end
+
+ # Update params
+ ed.params[:find][:last_match] = last_match
+ ed.params[:find][:direction] = direction
+
+end
+
+function editorFind(ed::Editor, str::String)
+ saved_cx, saved_cy = ed.csr.x, ed.csr.y
+ saved_coloff = ed.coloff
+ saved_rowoff = ed.rowoff
+
+ findCallback(ed, str, ' ')
+ query = editorPrompt(ed, "Search (arrow keys for next/prev): ",
+ callback=findCallback,
+ buf=str,
+ showcursor=false)
+
+ # If the user cancels the search, restore view
+ if query == ""
+ ed.csr.x = saved_cx
+ ed.csr.y = saved_cy
+ ed.coloff = saved_coloff
+ ed.rowoff = saved_rowoff
+ end
+end
+
+
+addCommand(:find, editorFind,
+ help="type to find, to get cursor, arrow keys to next/prev")
diff --git a/src/cmds/help.jl b/src/cmds/help.jl
new file mode 100644
index 0000000..94245eb
--- /dev/null
+++ b/src/cmds/help.jl
@@ -0,0 +1,31 @@
+function commandHelp(ed::Editor, args::String)
+ if args == ""
+ setStatusMessage(ed, "Type help for command specific help")
+ return
+ end
+
+ if !Base.isidentifier(args)
+ setStatusMessage(ed, "help: '$args' is not a valid command name")
+ return
+ end
+
+ sym = Symbol(args)
+
+ # Try getting help information from commands and params
+ helptext = ""
+
+ if configIsParam(sym)
+ helptext = configDesc(sym)
+ elseif sym in keys(COMMANDS)
+ helptext = COMMANDS[sym].help
+ end
+
+ if helptext == ""
+ setStatusMessage(ed, "No help documents for '$args'")
+ else
+ setStatusMessage(ed, helptext)
+ end
+end
+
+addCommand(:help, commandHelp,
+ help="Type help for command specific help")
diff --git a/src/cmds/open.jl b/src/cmds/open.jl
new file mode 100644
index 0000000..69295c4
--- /dev/null
+++ b/src/cmds/open.jl
@@ -0,0 +1,10 @@
+function commandOpen(ed::Editor, args::String)
+ if args == ""
+ editorOpen(ed)
+ else
+ editorOpen(ed, args)
+ end
+end
+
+addCommand(:open, commandOpen,
+ help="open: open a file")
diff --git a/src/cmds/quit.jl b/src/cmds/quit.jl
new file mode 100644
index 0000000..ed6f638
--- /dev/null
+++ b/src/cmds/quit.jl
@@ -0,0 +1,6 @@
+function commandQuit(ed::Editor, args::String)
+ editorQuit(ed, force=strip(args) == "!")
+end
+
+addCommand(:quit, commandQuit,
+ help="quit [!]: quit acorn. run 'quit !' to force quit")
diff --git a/src/cmds/save.jl b/src/cmds/save.jl
new file mode 100644
index 0000000..e6059ef
--- /dev/null
+++ b/src/cmds/save.jl
@@ -0,0 +1,6 @@
+function commandSave(ed::Editor, args::String)
+ editorSave(ed, args)
+end
+
+addCommand(:save, commandSave,
+ help="save: save the file")
diff --git a/src/cmds/set.jl b/src/cmds/set.jl
new file mode 100644
index 0000000..b9bfa13
--- /dev/null
+++ b/src/cmds/set.jl
@@ -0,0 +1,36 @@
+function commandSet(ed::Editor, args::String)
+ arg_arr = strip.(split(args, ' '))
+
+ # Initial checks
+ if length(arg_arr) != 2
+ setStatusMessage(ed, "set: command requires two arguments")
+ return
+ elseif !Base.isidentifier(arg_arr[1])
+ setStatusMessage(ed, "set: $(arg_arr[1]) is not a valid command name")
+ return
+ end
+
+ sym = Symbol(arg_arr[1])
+
+ # Check if it is a valid parameter
+ if !configIsParam(sym)
+ setStatusMessage(ed, "set: '$sym' is not a valid parameter name")
+ return
+ end
+
+ # Attempt to assign the parameter
+ try
+ val = parse(arg_arr[2])
+ configSet(sym, val)
+ catch Exception
+ setStatusMessage(ed, "set: invalid argument for $sym '$(arg_arr[2])'")
+ end
+
+ for row in ed.rows
+ update!(row)
+ end
+ refreshScreen(ed)
+end
+
+addCommand(:set, commandSet,
+ help="set : set the given parameter to a value")
diff --git a/src/editor.jl b/src/editor.jl
new file mode 100644
index 0000000..3914fac
--- /dev/null
+++ b/src/editor.jl
@@ -0,0 +1,536 @@
+# """ Clear the screen, print an error message, and kill the program """
+# function die(msg)
+# write(STDOUT, "\x1b[2J")
+# write(STDOUT, "\x1b[H")
+# error(msg)
+# end
+
+
+##########
+# CURSOR #
+##########
+
+mutable struct Cursor
+ x::Int
+ y::Int
+ rx::Int
+end
+
+
+##########
+# EDITOR #
+##########
+
+mutable struct Editor
+ rowoff::Int
+ coloff::Int
+ width::Int
+ height::Int
+ filename::String
+ statusmsg::String
+ status_time::Float64
+ dirty::Bool
+ quit::Bool
+
+ csr::Cursor
+ rows::Rows
+ term::Base.Terminals.TTYTerminal
+
+ params::Dict{Symbol, Dict{Symbol, Any}}
+end
+
+function Editor()
+ rowoff = 0
+ coloff = 0
+ width = 0
+ height = 0
+ filename = ""
+ statusmsg = ""
+ status_time = 0
+ dirty = false
+ quit = false
+
+ csr = Cursor(1,1,1)
+ rows = Rows()
+ term = Base.Terminals.TTYTerminal(get(ENV, "TERM", @static is_windows() ? "" : "dumb"), STDIN, STDOUT, STDERR)
+
+ params = Dict{Symbol, Dict{Symbol, Any}}()
+
+ Editor(rowoff, coloff, width, height, filename,
+ statusmsg, status_time, dirty, quit,
+ csr, rows, term, params)
+end
+
+
+
+
+###################
+# FILE OPERATIONS #
+###################
+
+function expand(filename::String)
+ # If the path is something like "~/Desktop/..." expand it
+ if length(filename) > 1 && filename[1] == '~'
+ filename = expanduser(filename)
+ end
+
+ return filename
+end
+
+""" Open a file and read the lines into the ed.rows array """
+function editorOpen(ed::Editor, filename::String)
+ try
+ filename = expand(filename)
+
+ # If no file exists, create it
+ !isfile(filename) && open(filename, "w") do f end
+
+ open(filename, "r") do file
+ clearRows(ed.rows)
+ for line in eachline(file)
+ appendRow(ed.rows, line)
+ end
+ ed.filename = filename
+ ed.csr.x = 1
+ ed.csr.y = 1
+ ed.dirty = false
+ end
+ catch Exception e
+ setStatusMessage(ed, "Cannot open file $filename")
+ end
+end
+
+
+function editorOpen(ed::Editor)
+ if ed.dirty
+ confirm = editorPrompt(ed, "There are unsaved changes. Open another file? [y/n]: ")
+ if confirm != "y"
+ setStatusMessage(ed, "Open aborted")
+ return
+ end
+ end
+
+ filename = editorPrompt(ed, "Open file: ")
+ filename = expand(filename)
+
+ if filename != ""
+ editorOpen(ed, filename)
+ else
+ setStatusMessage(ed, "Open aborted")
+ end
+end
+
+function editorSave(ed::Editor)
+ editorSave(ed, "")
+end
+
+""" Save the contents of the file buffer to disc """
+function editorSave(ed::Editor, path::String)
+ prev_filename = ed.filename
+
+ try
+ if path == ""
+ if ed.filename == ""
+ ed.filename = editorPrompt(ed, "Save as: ")
+ if ed.filename == ""
+ setStatusMessage(ed, "Save aborted")
+ return
+ end
+ end
+ else
+ ed.filename = expand(path)
+ end
+
+ open(ed.filename, "w") do f
+ write(f, rowsToString(ed.rows))
+ end
+ setStatusMessage(ed, "File saved: $(ed.filename)")
+ ed.dirty = false
+ catch Exception
+ # There was an error saving, restore original filename
+ ed.filename = prev_filename
+ setStatusMessage(ed, "Unable to save: $(ed.filename)")
+ end
+end
+
+
+
+##########
+# VISUAL #
+##########
+
+""" Given an arrow input, move the cursor """
+function moveCursor(ed::Editor, key::UInt32)
+ if key == ARROW_LEFT
+ if ed.csr.x > 1
+ ed.csr.x -= 1
+ elseif ed.csr.y > 1
+ # At start of line, move to end of prev line
+ ed.csr.y -= 1
+ ed.csr.x = 1+length(ed.rows[ed.csr.y].chars)
+ end
+
+ elseif key == ARROW_RIGHT
+ onrow = ed.csr.y <= length(ed.rows)
+ if onrow && ed.csr.x <= length(ed.rows[ed.csr.y].chars)
+ ed.csr.x += 1
+ elseif ed.csr.y < length(ed.rows) && ed.csr.x == 1 + length(ed.rows[ed.csr.y].chars)
+ # At end of line, move to next line
+ ed.csr.y += 1
+ ed.csr.x = 1
+ end
+
+ elseif key == ARROW_UP
+ ed.csr.y > 1 && (ed.csr.y -= 1)
+
+ elseif key == ARROW_DOWN
+ ed.csr.y < length(ed.rows) && (ed.csr.y += 1)
+ end
+
+ # Snap to end of line if we are further out
+ rowlen = ed.csr.y < length(ed.rows)+1 ? length(ed.rows[ed.csr.y].chars)+1 : 1
+ ed.csr.x > rowlen && (ed.csr.x = rowlen)
+end
+
+moveCursor(ed, key::Key) = moveCursor(ed, UInt32(key))
+
+
+
+""" Scroll the screen based on the cursor position """
+function scroll(ed::Editor)
+ ed.csr.rx = 1
+ if ed.csr.y <= length(ed.rows)
+ ed.csr.rx = renderX(ed.rows[ed.csr.y], ed.csr.x)
+ end
+
+ # Vertical scrolling
+ if ed.csr.y < ed.rowoff+1
+ ed.rowoff = ed.csr.y-1
+ end
+ if ed.csr.y >= ed.rowoff+1 + ed.height
+ ed.rowoff = ed.csr.y - ed.height
+ end
+
+ # Horizontal scrolling
+ if ed.csr.rx < ed.coloff+1
+ ed.coloff = ed.csr.rx
+ end
+ if ed.csr.rx >= ed.coloff+1 + ed.width
+ ed.coloff = ed.csr.rx - ed.width
+ end
+end
+
+function drawRows(ed::Editor, buf::IOBuffer)
+ for y = 1:ed.height
+ filerow = y + ed.rowoff
+ y != 1 && write(buf, "\r\n")
+
+ write(buf, "\x1b[K") # Clear line
+ if filerow > length(ed.rows)
+ if y == div(ed.height, 3) && ed.width > 40 && length(ed.rows) == 0
+ msg = "Acorn Editor"
+ padding = div(ed.width - length(msg), 2)
+ if padding > 0
+ write(buf, "~")
+ padding -= 1
+ end
+ while (padding -= 1) > 0
+ write(buf, " ")
+ end
+ write(buf, msg)
+ else
+ write(buf, "~");
+ end
+ else
+ len = length(ed.rows[filerow].render) - ed.coloff
+ len = clamp(len, 0, ed.width)
+ write(buf, ed.rows[filerow].render[1+ed.coloff : ed.coloff + len])
+ end
+ end
+ # Write a newline to prepare for status bar
+ write(buf, "\r\n");
+end
+
+function drawStatusBar(ed::Editor, buf::IOBuffer)
+ write(buf, "\x1b[7m") # invert colors
+ col = 1
+
+ # left padding
+ write(buf, ' ')
+ col += 1
+
+ # filename
+ filestatus = string(ed.filename, ed.dirty ? " *" : "")
+
+ for i = 1:min(div(ed.width,2), length(filestatus))
+ write(buf, filestatus[i])
+ col += 1
+ end
+
+ linenum = string(ed.csr.y)
+
+ while col < ed.width - length(linenum)
+ write(buf, ' ')
+ col += 1
+ end
+
+ write(buf, linenum, ' ')
+
+ write(buf, "\x1b[m") # uninvert colors
+
+ # make line for message bar
+ write(buf, "\r\n")
+end
+
+function drawStatusMessage(ed::Editor, buf::IOBuffer)
+ write(buf, "\x1b[K")
+ if time() - ed.status_time < 5.0
+ write(buf, ed.statusmsg[1:min(ed.width, length(ed.statusmsg))])
+ end
+end
+
+function setStatusMessage(ed::Editor, msg::String)
+ ed.statusmsg = msg
+ ed.status_time = time()
+end
+
+function refreshScreen(ed::Editor)
+
+ # Update terminal size
+ ed.height = Base.Terminals.height(ed.term) - 2 # status + msg bar = 2
+ ed.width = Base.Terminals.width(ed.term)
+
+ scroll(ed)
+
+ buf = IOBuffer()
+
+ write(buf, "\x1b[?25l") # ?25l: Hide cursor
+ write(buf, "\x1b[H") # H: Move cursor to top left
+
+ drawRows(ed, buf)
+ drawStatusBar(ed, buf)
+ drawStatusMessage(ed, buf)
+
+ @printf(buf, "\x1b[%d;%dH", ed.csr.y-ed.rowoff,
+ ed.csr.rx-ed.coloff)
+
+ write(buf, "\x1b[?25h") # ?25h: Show cursor
+
+ write(STDOUT, String(take!(buf)))
+end
+
+function editorPrompt(ed::Editor, prompt::String;
+ callback=nothing,
+ buf::String="",
+ showcursor::Bool=true) ::String
+ while true
+ statusmsg = string(prompt, buf)
+ setStatusMessage(ed, string(prompt, buf))
+ refreshScreen(ed)
+
+ if showcursor
+ # Position the cursor at the end of the line
+ @printf(STDOUT, "\x1b[%d;%dH", 999, length(statusmsg)+1)
+ end
+
+ c = Char(readKey())
+
+ if c == '\x1b'
+ setStatusMessage(ed, "")
+ callback != nothing && callback(ed, buf, c)
+ return ""
+ elseif c == '\r'
+ if length(buf) != 0
+ setStatusMessage(ed, "")
+ callback != nothing && callback(ed, buf, c)
+ return buf
+ end
+ elseif UInt32(c) == BACKSPACE && length(buf) > 0
+ buf = buf[1:end-1]
+ elseif !iscntrl(c) && UInt32(c) < 128
+ buf = string(buf, c)
+ end
+
+ callback != nothing && callback(ed, buf, c)
+ end
+end
+
+
+
+
+##############
+## Keyboard ##
+##############
+
+
+function processKeypress(ed::Editor)
+ c = readKey();
+
+ if c == ctrl_key('p')
+ runCommand(ed)
+ elseif (c == ARROW_LEFT
+ || c == ARROW_UP
+ || c == ARROW_RIGHT
+ || c == ARROW_DOWN)
+ moveCursor(ed, c)
+ elseif c == PAGE_UP || c == PAGE_DOWN
+ lines = ed.height
+ while (lines-=1) > 0
+ moveCursor(ed, c == PAGE_UP ? ARROW_UP : ARROW_DOWN)
+ end
+ elseif c == HOME_KEY
+ ed.csr.x = 0
+ elseif c == END_KEY
+ ed.csr.y < length(ed.rows) && (ed.csr.x = length(ed.rows[ed.csr.y].chars))
+ elseif c == UInt32('\r')
+ editorInsertNewline(ed)
+ elseif c == BACKSPACE
+ editorDelChar(ed)
+ elseif c == DEL_KEY
+ moveCursor(ed, ARROW_RIGHT)
+ editorDelChar(ed)
+ elseif c == ctrl_key('l')
+ # Refresh screen
+ return
+ elseif c == UInt32('\x1b')
+ return
+ elseif iscntrl(Char(c)) && isKeyBound(Char(c))
+ runCommand(ed, getKeyBinding(Char(c)))
+ elseif c == UInt32('\t')
+ editorInsertTab(ed)
+ elseif !iscntrl(Char(c))
+ editorInsertChar(ed, c)
+ end
+
+end
+
+
+#####################
+# Editor Operations #
+#####################
+
+function editorInsertChar(ed::Editor, c::UInt32)
+ # The cursor is able to move beyond the last row
+ ed.csr.y == length(ed.rows)+1 && appendRow(ed.rows, "")
+ insertChar!(ed.rows[ed.csr.y], ed.csr.x, Char(c))
+ ed.csr.x += 1
+ ed.dirty = true
+end
+
+function editorInsertTab(ed::Editor)
+ # The cursor is able to move beyond the last row
+ ed.csr.y == length(ed.rows)+1 && appendRow(ed.rows, "")
+
+ # Insert character(s) into the row data
+ mv_fwd = insertTab!(ed.rows[ed.csr.y], ed.csr.x)
+
+ ed.csr.x += mv_fwd
+
+ ed.dirty = true
+end
+
+
+function editorDelChar(ed::Editor)
+ ed.csr.y == length(ed.rows)+1 && return
+ ed.csr.x == 1 && ed.csr.y == 1 && return
+
+ if ed.csr.x > 1
+ deleteChar!(ed.rows[ed.csr.y], ed.csr.x -1)
+ ed.csr.x -= 1
+ ed.dirty = true
+ else
+ # Move cursor to end of prev line
+ ed.csr.x = 1+length(ed.rows[ed.csr.y-1].chars)
+ appendRowString(ed.rows[ed.csr.y-1], ed.rows[ed.csr.y].chars)
+ editorDelRow(ed, ed.csr.y)
+ ed.csr.y -= 1
+ end
+end
+
+function editorInsertRow(ed::Editor, i::Int, str::String)
+ row = Row(str)
+ update!(row)
+ insert!(ed.rows, i, row)
+end
+
+function editorInsertNewline(ed::Editor)
+ if ed.csr.x == 1
+ editorInsertRow(ed, ed.csr.y, "")
+ else
+ row = ed.rows[ed.csr.y]
+ before = row.chars[1:ed.csr.x-1]
+ after = row.chars[ed.csr.x:end]
+ editorInsertRow(ed, ed.csr.y + 1, after)
+ row.chars = before
+ update!(row)
+ end
+ ed.csr.y += 1
+ ed.csr.x = 1
+end
+
+function editorDelRow(ed::Editor, i::Int)
+ i < 1 || i > length(ed.rows) && return
+ deleteat!(ed.rows, i)
+ ed.dirty = true
+end
+
+function editorQuit(ed::Editor; force::Bool=false)
+ if !force && ed.dirty
+ setStatusMessage(ed,
+ "File has unsaved changes. Save changes of use 'quit !' to quit anyway")
+ else
+ write(STDOUT, "\x1b[2J")
+ write(STDOUT, "\x1b[H")
+ ed.quit = true
+ !isinteractive() && exit(0)
+ end
+end
+
+
+
+
+############
+# COMMANDS #
+############
+
+
+function runCommand(ed::Editor)
+ cmd = editorPrompt(ed, "> ")
+ runCommand(ed, strip(cmd))
+end
+
+function runCommand(ed::Editor, command_str::String)
+ cmd_arr = split(command_str, ' ', limit=2)
+
+ # Get the command
+ cmd = strip(cmd_arr[1])
+
+ # Blank, do nothing
+ cmd == "" && return
+
+ # Command must be a valid identifier
+ if !Base.isidentifier(cmd)
+ setStatusMessage(ed, "'$sym' is not a valid command name")
+ return
+ end
+
+ cmd_sym = Symbol(cmd)
+
+ # Get arguments if there are any
+ args = ""
+ if length(cmd_arr) > 1
+ args = cmd_arr[2]
+ end
+
+ # If the command exists, run it
+ if cmd_sym in keys(COMMANDS)
+ # join(args): convert Substring to String
+ runCommand(COMMANDS[cmd_sym], ed, join(args))
+ else
+ setStatusMessage(ed, "'$sym' is not a valid command")
+ end
+end
+
+function runCommand(c::Command, ed::Editor, args::String)
+ c.cmd(ed, args)
+end
diff --git a/src/row.jl b/src/row.jl
new file mode 100644
index 0000000..58b2610
--- /dev/null
+++ b/src/row.jl
@@ -0,0 +1,160 @@
+#############
+# ROW #
+#############
+
+""" Holds file true data and render data """
+mutable struct Row
+ # file data, a row of text
+ chars::String
+ # a row of text as rendered on screen
+ render::String
+end
+
+Row(s::String) = Row(s, "")
+
+""" Update the `render` string using the `chars` string """
+function update!(row::Row)
+ # Count the number of tabs
+ tabs = 0
+ for c in row.chars
+ c == '\t' && (tabs += 1)
+ end
+
+ # Allocate an array of characters
+ updated = Array{Char, 1}(length(row.chars) + tabs*(configGet(:tab_stop)-1))
+
+ # copy the characters into the updated array
+ idx = 1
+ for i in 1:length(row.chars)
+
+ # replace tabs with spaces
+ if row.chars[i] == '\t'
+ updated[idx] = ' '
+ idx += 1
+ while idx % configGet(:tab_stop) != 0;
+ updated[idx] = ' ';
+ idx += 1
+ end
+ else
+ updated[idx] = row.chars[i]
+ idx += 1
+ end
+
+ end
+
+ row.render = join(updated[1:idx-1])
+end
+
+"""Convert a cursor position in the document to a rendered cursor position"""
+function renderX(row::Row, cx::Int)
+ rx = 1
+ for i in 1:cx-1
+ # If there is a tab, move the cursor forward to the tab stop
+ if row.chars[i] == '\t'
+ rx += (configGet(:tab_stop) - 1) - (rx % configGet(:tab_stop))
+ end
+ rx += 1
+ end
+ rx
+end
+
+
+"""opposite of renderX: Convert a render position to a chars position"""
+function charX(row::Row, rx::Int)
+ cur_rx = 1
+ for cx = 1:length(row.chars)
+ if row.chars[cx] == '\t'
+ cur_rx += (configGet(:tab_stop) - 1) - (cur_rx % configGet(:tab_stop))
+ end
+ cur_rx += 1
+
+ cur_rx > rx && return cx
+ end
+ cx
+end
+
+
+""" Insert a char into a string """
+function insert(s::String, i::Int, c::Union{Char,String})
+ if s == ""
+ string(c)
+ else
+ string(s[1:i-1], c, s[i:end])
+ end
+end
+
+""" Delete char from string """
+function delete(s::String, i::Int)
+ i < 1 || i > length(s) && return s
+ string(s[1:i-1], s[i+1:end])
+end
+
+""" Insert a char into the row at a given location """
+function insertChar!(row::Row, i::Int, c::Char)
+ row.chars = insert(row.chars, i, c)
+ update!(row)
+end
+
+""" Insert a tab into the row at a given location. Return the number of chars inserted """
+function insertTab!(row::Row, i::Int)
+ num_chars = 1
+ t = '\t'
+
+ # If we are using spaces, move ahead more
+ if configGet(:expandtab)
+ num_chars = configGet(:tab_stop) - (i % configGet(:tab_stop))
+ t = repeat(" ", num_chars)
+ end
+
+ row.chars = insert(row.chars, i, t)
+ update!(row)
+
+ # Used for positioning the cursor
+ return num_chars
+end
+
+
+""" Delete char from row """
+function deleteChar!(row::Row, i::Int)
+ row.chars = delete(row.chars, i)
+ update!(row)
+end
+
+"""
+Add a row to the end of the document
+initialize the row with the given string
+"""
+function appendRowString(row::Row, str::String)
+ row.chars = string(row.chars, str)
+ update!(row)
+end
+
+
+
+########
+# ROWS #
+########
+
+""" type alias for Array{Row, 1} """
+Rows = Array{Row, 1}
+
+""" delete all rows from a Rows """
+function clearRows(rows::Rows)
+ while length(rows) > 0
+ pop!(rows)
+ end
+end
+
+
+""" Add a row to the end of the document """
+function appendRow(rows::Rows, s::String)
+ row = Row(s)
+ update!(row)
+ push!(rows, row)
+end
+
+
+""" Convert all ROW data to a single string """
+function rowsToString(rows::Rows)
+ join(map(row -> row.chars, rows), '\n')
+end
diff --git a/src/terminal.jl b/src/terminal.jl
new file mode 100644
index 0000000..1c1f3c9
--- /dev/null
+++ b/src/terminal.jl
@@ -0,0 +1,127 @@
+import Base.==
+
+@enum(Key,
+ BACKSPACE = 127,
+ ARROW_LEFT = 1000,
+ ARROW_RIGHT,
+ ARROW_UP,
+ ARROW_DOWN,
+ DEL_KEY,
+ HOME_KEY,
+ END_KEY,
+ PAGE_UP,
+ PAGE_DOWN,
+ S_ARROW_UP,
+ S_ARROW_DOWN,
+ S_ARROW_LEFT,
+ S_ARROW_RIGHT,
+ C_ARROW_UP,
+ C_ARROW_DOWN,
+ C_ARROW_LEFT,
+ C_ARROW_RIGHT)
+
+==(c::UInt32, k::Key) = c == UInt32(k)
+==(k::Key, c::UInt32) = c == UInt32(k)
+==(c::Char, k::Key) = UInt32(c) == UInt32(k)
+==(k::Key, c::Char) = UInt32(c) == UInt32(k)
+
+ctrl_key(c::Char)::UInt32 = UInt32(c) & 0x1f
+
+readNextChar() = Char(read(STDIN,1)[1])
+
+function readKey() ::UInt32
+ c = readNextChar()
+
+ # Escape characters
+ if c == '\x1b'
+ STDIN.buffer.size < 3 && return '\x1b'
+ esc_a = readNextChar()
+ esc_b = readNextChar()
+
+ if esc_a == '['
+ if esc_b >= '0' && esc_b <= '9'
+ STDIN.buffer.size < 4 && return '\x1b'
+ esc_c = readNextChar()
+
+ if esc_c == '~'
+ if esc_b == '1'
+ return HOME_KEY
+ elseif esc_b == '4'
+ return END_KEY
+ elseif esc_b == '3'
+ return DEL_KEY
+ elseif esc_b == '5'
+ return PAGE_UP
+ elseif esc_b == '6'
+ return PAGE_DOWN
+ elseif esc_b == '7'
+ return HOME_KEY
+ elseif esc_b == '8'
+ return END_KEY
+ else
+ return '\x1b'
+ end
+ elseif esc_c == ';'
+ STDIN.buffer.size < 6 && return '\x1b'
+ esc_d = readNextChar()
+ esc_e = readNextChar()
+
+ if esc_d == '2'
+ # shift + arrorw
+ if esc_e == 'A'
+ return S_ARROW_UP
+ elseif esc_e == 'B'
+ return S_ARROW_DOWN
+ elseif esc_e == 'C'
+ return S_ARROW_RIGHT
+ elseif esc_e == 'D'
+ return S_ARROW_LEFT
+ else
+ return '\x1b'
+ end
+ elseif esc_d == '5'
+ # Ctrl + arrow
+ if esc_e == 'A'
+ return C_ARROW_UP
+ elseif esc_e == 'B'
+ return C_ARROW_DOWN
+ elseif esc_e == 'C'
+ return C_ARROW_RIGHT
+ elseif esc_e == 'D'
+ return C_ARROW_LEFT
+ else
+ return '\x1b'
+ end
+ end
+ end
+ else
+ # Arrow keys
+ if esc_b == 'A'
+ return ARROW_UP
+ elseif esc_b == 'B'
+ return ARROW_DOWN
+ elseif esc_b == 'C'
+ return ARROW_RIGHT
+ elseif esc_b == 'D'
+ return ARROW_LEFT
+ elseif esc_b == 'H'
+ return HOME_KEY
+ elseif esc_b == 'F'
+ return END_KEY
+ else
+ return '\x1b'
+ end
+ end
+ elseif esc_a == 'O'
+ if esc_a == 'H'
+ return HOME_KEY
+ elseif esc_a == 'F'
+ return END_KEY
+ end
+ end
+
+ return '\x1b'
+ else
+ return c;
+ end
+end