395 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
			
		
		
	
	
			395 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
-- configs
 | 
						|
local LogColors = { [LogDebug] = 'pink',
 | 
						|
                    [LogInfo] = 'white',
 | 
						|
                    [LogWarning] = 'yellow',
 | 
						|
                    [LogError] = 'red' }
 | 
						|
local MaxLogLines = 128
 | 
						|
local MaxHistory = 1000
 | 
						|
 | 
						|
local oldenv = getfenv(0)
 | 
						|
setfenv(0, _G)
 | 
						|
_G.commandEnv = runinsandbox('commands')
 | 
						|
setfenv(0, oldenv)
 | 
						|
 | 
						|
-- private variables
 | 
						|
local terminalWindow
 | 
						|
local terminalButton
 | 
						|
local logLocked = false
 | 
						|
local commandTextEdit
 | 
						|
local terminalBuffer
 | 
						|
local commandHistory = { }
 | 
						|
local currentHistoryIndex = 0
 | 
						|
local poped = false
 | 
						|
local oldPos
 | 
						|
local oldSize
 | 
						|
local firstShown = false
 | 
						|
local flushEvent
 | 
						|
local cachedLines = {}
 | 
						|
local disabled = false
 | 
						|
local allLines = {}
 | 
						|
 | 
						|
-- private functions
 | 
						|
local function navigateCommand(step)
 | 
						|
  if commandTextEdit:isMultiline() then
 | 
						|
    return
 | 
						|
  end
 | 
						|
 | 
						|
  local numCommands = #commandHistory
 | 
						|
  if numCommands > 0 then
 | 
						|
    currentHistoryIndex = math.min(math.max(currentHistoryIndex + step, 0), numCommands)
 | 
						|
    if currentHistoryIndex > 0 then
 | 
						|
      local command = commandHistory[numCommands - currentHistoryIndex + 1]
 | 
						|
      commandTextEdit:setText(command)
 | 
						|
      commandTextEdit:setCursorPos(-1)
 | 
						|
    else
 | 
						|
      commandTextEdit:clearText()
 | 
						|
    end
 | 
						|
  end
 | 
						|
end
 | 
						|
 | 
						|
local function completeCommand()
 | 
						|
  local cursorPos = commandTextEdit:getCursorPos()
 | 
						|
  if cursorPos == 0 then return end
 | 
						|
 | 
						|
  local commandBegin = commandTextEdit:getText():sub(1, cursorPos)
 | 
						|
  local possibleCommands = {}
 | 
						|
 | 
						|
  -- create a list containing all globals
 | 
						|
  local allVars = table.copy(_G)
 | 
						|
  table.merge(allVars, commandEnv)
 | 
						|
 | 
						|
  -- match commands
 | 
						|
  for k,v in pairs(allVars) do
 | 
						|
    if k:sub(1, cursorPos) == commandBegin then
 | 
						|
      table.insert(possibleCommands, k)
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  -- complete command with one match
 | 
						|
  if #possibleCommands == 1 then
 | 
						|
    commandTextEdit:setText(possibleCommands[1])
 | 
						|
    commandTextEdit:setCursorPos(-1)
 | 
						|
  -- show command matches
 | 
						|
  elseif #possibleCommands > 0 then
 | 
						|
    print('>> ' .. commandBegin)
 | 
						|
 | 
						|
    -- expand command
 | 
						|
    local expandedComplete = commandBegin
 | 
						|
    local done = false
 | 
						|
    while not done do
 | 
						|
      cursorPos = #commandBegin+1
 | 
						|
      if #possibleCommands[1] < cursorPos then
 | 
						|
        break
 | 
						|
      end
 | 
						|
      expandedComplete = commandBegin .. possibleCommands[1]:sub(cursorPos, cursorPos)
 | 
						|
      for i,v in ipairs(possibleCommands) do
 | 
						|
        if v:sub(1, #expandedComplete) ~= expandedComplete then
 | 
						|
          done = true
 | 
						|
        end
 | 
						|
      end
 | 
						|
      if not done then
 | 
						|
        commandBegin = expandedComplete
 | 
						|
      end
 | 
						|
    end
 | 
						|
    commandTextEdit:setText(commandBegin)
 | 
						|
      commandTextEdit:setCursorPos(-1)
 | 
						|
 | 
						|
    for i,v in ipairs(possibleCommands) do
 | 
						|
      print(v)
 | 
						|
    end
 | 
						|
  end
 | 
						|
end
 | 
						|
 | 
						|
local function doCommand(textWidget)
 | 
						|
  local currentCommand = textWidget:getText()
 | 
						|
  executeCommand(currentCommand)
 | 
						|
  textWidget:clearText()
 | 
						|
  return true
 | 
						|
end
 | 
						|
 | 
						|
local function addNewline(textWidget)
 | 
						|
  if not textWidget:isOn() then
 | 
						|
    textWidget:setOn(true)
 | 
						|
  end
 | 
						|
  textWidget:appendText('\n')
 | 
						|
end
 | 
						|
 | 
						|
local function onCommandChange(textWidget, newText, oldText)
 | 
						|
  local _, newLineCount = string.gsub(newText, '\n', '\n')
 | 
						|
  textWidget:setHeight((newLineCount + 1) * textWidget.baseHeight)
 | 
						|
 | 
						|
  if newLineCount == 0 and textWidget:isOn() then
 | 
						|
    textWidget:setOn(false)
 | 
						|
  end
 | 
						|
end
 | 
						|
 | 
						|
local function onLog(level, message, time)
 | 
						|
  if disabled then return end
 | 
						|
  -- avoid logging while reporting logs (would cause a infinite loop)
 | 
						|
  if logLocked then return end
 | 
						|
 | 
						|
  logLocked = true
 | 
						|
  addLine(message, LogColors[level])
 | 
						|
  logLocked = false
 | 
						|
end
 | 
						|
 | 
						|
-- public functions
 | 
						|
function init()
 | 
						|
  terminalWindow = g_ui.displayUI('terminal')
 | 
						|
  terminalWindow:setVisible(false)
 | 
						|
 | 
						|
  terminalWindow.onDoubleClick = popWindow
 | 
						|
 | 
						|
  terminalButton = modules.client_topmenu.addLeftButton('terminalButton', tr('Terminal') .. ' (Ctrl + T)', '/images/topbuttons/terminal', toggle)
 | 
						|
  terminalButton:setOn(false)
 | 
						|
  g_keyboard.bindKeyDown('Ctrl+T', toggle)
 | 
						|
 | 
						|
  commandHistory = g_settings.getList('terminal-history')
 | 
						|
 | 
						|
  commandTextEdit = terminalWindow:getChildById('commandTextEdit')
 | 
						|
  commandTextEdit:setHeight(commandTextEdit.baseHeight)
 | 
						|
  connect(commandTextEdit, {onTextChange = onCommandChange})
 | 
						|
  g_keyboard.bindKeyPress('Up', function() navigateCommand(1) end, commandTextEdit)
 | 
						|
  g_keyboard.bindKeyPress('Down', function() navigateCommand(-1) end, commandTextEdit)
 | 
						|
  g_keyboard.bindKeyPress('Ctrl+C',
 | 
						|
    function()
 | 
						|
      if commandTextEdit:hasSelection() or not terminalSelectText:hasSelection() then return false end
 | 
						|
      g_window.setClipboardText(terminalSelectText:getSelection())
 | 
						|
    return true
 | 
						|
    end, commandTextEdit)
 | 
						|
  g_keyboard.bindKeyDown('Tab', completeCommand, commandTextEdit)
 | 
						|
  g_keyboard.bindKeyPress('Shift+Enter', addNewline, commandTextEdit)
 | 
						|
  g_keyboard.bindKeyDown('Enter', doCommand, commandTextEdit)
 | 
						|
  g_keyboard.bindKeyDown('Escape', hide, terminalWindow)
 | 
						|
 | 
						|
  terminalBuffer = terminalWindow:getChildById('terminalBuffer')
 | 
						|
  terminalSelectText = terminalWindow:getChildById('terminalSelectText')
 | 
						|
  terminalSelectText.onDoubleClick = popWindow
 | 
						|
  terminalSelectText.onMouseWheel = function(a,b,c) terminalBuffer:onMouseWheel(b,c) end
 | 
						|
  terminalBuffer.onScrollChange = function(self, value) terminalSelectText:setTextVirtualOffset(value) end
 | 
						|
 | 
						|
  g_logger.setOnLog(onLog)
 | 
						|
 | 
						|
  if not g_app.isRunning() then
 | 
						|
    g_logger.fireOldMessages()
 | 
						|
  elseif _G.terminalLines then
 | 
						|
    for _,line in pairs(_G.terminalLines) do
 | 
						|
      addLine(line.text, line.color)
 | 
						|
    end
 | 
						|
  end
 | 
						|
end
 | 
						|
 | 
						|
function terminate()
 | 
						|
  g_settings.setList('terminal-history', commandHistory)
 | 
						|
 | 
						|
  removeEvent(flushEvent)
 | 
						|
 | 
						|
  if poped then
 | 
						|
    oldPos = terminalWindow:getPosition()
 | 
						|
    oldSize = terminalWindow:getSize()
 | 
						|
  end
 | 
						|
  local settings = {
 | 
						|
    size = oldSize,
 | 
						|
    pos = oldPos,
 | 
						|
    poped = poped
 | 
						|
  }
 | 
						|
  g_settings.setNode('terminal-window', settings)
 | 
						|
 | 
						|
  g_keyboard.unbindKeyDown('Ctrl+T')
 | 
						|
  g_logger.setOnLog(nil)
 | 
						|
  terminalWindow:destroy()
 | 
						|
  terminalButton:destroy()
 | 
						|
  commandEnv = nil
 | 
						|
  _G.terminalLines = allLines
 | 
						|
end
 | 
						|
 | 
						|
function hideButton()
 | 
						|
  --terminalButton:hide()
 | 
						|
end
 | 
						|
 | 
						|
function popWindow()
 | 
						|
  if poped then
 | 
						|
    oldPos = terminalWindow:getPosition()
 | 
						|
    oldSize = terminalWindow:getSize()
 | 
						|
    terminalWindow:fill('parent')
 | 
						|
    terminalWindow:setOn(false)
 | 
						|
    terminalWindow:getChildById('bottomResizeBorder'):disable()
 | 
						|
    terminalWindow:getChildById('rightResizeBorder'):disable()
 | 
						|
    terminalWindow:getChildById('titleBar'):hide()
 | 
						|
    terminalWindow:getChildById('terminalScroll'):setMarginTop(0)
 | 
						|
    terminalWindow:getChildById('terminalScroll'):setMarginBottom(0)
 | 
						|
    terminalWindow:getChildById('terminalScroll'):setMarginRight(0)
 | 
						|
    poped = false
 | 
						|
  else
 | 
						|
    terminalWindow:breakAnchors()
 | 
						|
    terminalWindow:setOn(true)
 | 
						|
    local size = oldSize or { width = g_window.getWidth()/2.5, height = g_window.getHeight()/4 }
 | 
						|
    terminalWindow:setSize(size)
 | 
						|
    local pos = oldPos or { x = 0, y = g_window.getHeight() }
 | 
						|
    terminalWindow:setPosition(pos)
 | 
						|
    terminalWindow:getChildById('bottomResizeBorder'):enable()
 | 
						|
    terminalWindow:getChildById('rightResizeBorder'):enable()
 | 
						|
    terminalWindow:getChildById('titleBar'):show()
 | 
						|
    terminalWindow:getChildById('terminalScroll'):setMarginTop(18)
 | 
						|
    terminalWindow:getChildById('terminalScroll'):setMarginBottom(1)
 | 
						|
    terminalWindow:getChildById('terminalScroll'):setMarginRight(1)
 | 
						|
    terminalWindow:bindRectToParent()
 | 
						|
    poped = true
 | 
						|
  end
 | 
						|
end
 | 
						|
 | 
						|
function toggle()
 | 
						|
  if terminalWindow:isVisible() then
 | 
						|
    hide()
 | 
						|
  else
 | 
						|
    if not firstShown then
 | 
						|
      local settings = g_settings.getNode('terminal-window')
 | 
						|
      if settings then
 | 
						|
        if settings.size then oldSize = settings.size end
 | 
						|
        if settings.pos then oldPos = settings.pos end
 | 
						|
        if settings.poped then popWindow() end
 | 
						|
      end
 | 
						|
      firstShown = true
 | 
						|
    end
 | 
						|
    show()
 | 
						|
  end
 | 
						|
end
 | 
						|
 | 
						|
function show()
 | 
						|
  terminalWindow:show()
 | 
						|
  terminalWindow:raise()
 | 
						|
  terminalWindow:focus()
 | 
						|
end
 | 
						|
 | 
						|
function hide()
 | 
						|
  terminalWindow:hide()
 | 
						|
end
 | 
						|
 | 
						|
function disable()
 | 
						|
  --terminalButton:hide()
 | 
						|
  g_keyboard.unbindKeyDown('Ctrl+T')
 | 
						|
  disabled = true
 | 
						|
end
 | 
						|
 | 
						|
function flushLines()
 | 
						|
  local numLines = terminalBuffer:getChildCount() + #cachedLines
 | 
						|
  local fulltext = terminalSelectText:getText()
 | 
						|
 | 
						|
  for _,line in pairs(cachedLines) do
 | 
						|
    -- delete old lines if needed
 | 
						|
    if numLines > MaxLogLines then
 | 
						|
      local firstChild = terminalBuffer:getChildByIndex(1)
 | 
						|
      if firstChild then
 | 
						|
        local len = #firstChild:getText()
 | 
						|
        firstChild:destroy()
 | 
						|
        table.remove(allLines, 1)
 | 
						|
        fulltext = string.sub(fulltext, len)
 | 
						|
      end
 | 
						|
    end
 | 
						|
 | 
						|
    local label = g_ui.createWidget('TerminalLabel', terminalBuffer)
 | 
						|
    label:setId('terminalLabel' .. numLines)
 | 
						|
    label:setText(line.text)
 | 
						|
    label:setColor(line.color)
 | 
						|
 | 
						|
    table.insert(allLines, {text=line.text,color=line.color})
 | 
						|
 | 
						|
    fulltext = fulltext .. '\n' .. line.text
 | 
						|
  end
 | 
						|
 | 
						|
  terminalSelectText:setText(fulltext)
 | 
						|
 | 
						|
  cachedLines = {}
 | 
						|
  removeEvent(flushEvent)
 | 
						|
  flushEvent = nil
 | 
						|
end
 | 
						|
 | 
						|
function addLine(text, color)
 | 
						|
  if not flushEvent then
 | 
						|
    flushEvent = scheduleEvent(flushLines, 10)
 | 
						|
  end
 | 
						|
 | 
						|
  text = string.gsub(text, '\t', '    ')
 | 
						|
  table.insert(cachedLines, {text=text, color=color})
 | 
						|
end
 | 
						|
 | 
						|
function terminalPrint(value)
 | 
						|
  if type(value) == "table" then
 | 
						|
    return print(json.encode(value, 2))
 | 
						|
  end
 | 
						|
  print(tostring(value))
 | 
						|
end
 | 
						|
 | 
						|
function executeCommand(command)
 | 
						|
  if command == nil or #string.gsub(command, '\n', '') == 0 then return end
 | 
						|
 | 
						|
  -- add command line
 | 
						|
  addLine("> " .. command, "#ffffff")
 | 
						|
  if g_game.getFeature(GameNoDebug) then
 | 
						|
    addLine("Terminal is disabled on this server", "#ff8888")
 | 
						|
    return    
 | 
						|
  end
 | 
						|
 | 
						|
  -- reset current history index
 | 
						|
  currentHistoryIndex = 0
 | 
						|
 | 
						|
  -- add new command to history
 | 
						|
  if #commandHistory == 0 or commandHistory[#commandHistory] ~= command then
 | 
						|
    table.insert(commandHistory, command)
 | 
						|
    while #commandHistory > MaxHistory do
 | 
						|
      table.remove(commandHistory, 1)
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  -- detect and convert commands with simple syntax
 | 
						|
  local realCommand
 | 
						|
  if string.sub(command, 1, 1) == '=' then
 | 
						|
    realCommand = 'modules.client_terminal.terminalPrint(' .. string.sub(command,2) .. ')'
 | 
						|
  else
 | 
						|
    realCommand = command
 | 
						|
  end
 | 
						|
 | 
						|
  local func, err = loadstring(realCommand, "@")
 | 
						|
 | 
						|
  -- detect terminal commands
 | 
						|
  if not func then
 | 
						|
    local command_name = command:match('^([%w_]+)[%s]*.*')
 | 
						|
    if command_name then
 | 
						|
      local args = string.split(command:match('^[%w_]+[%s]*(.*)'), ' ')
 | 
						|
      if commandEnv[command_name] and type(commandEnv[command_name]) == 'function' then
 | 
						|
        func = function() modules.client_terminal.commandEnv[command_name](unpack(args)) end
 | 
						|
      elseif command_name == command then
 | 
						|
        addLine('ERROR: command not found', 'red')
 | 
						|
        return
 | 
						|
      end
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  -- check for syntax errors
 | 
						|
  if not func then
 | 
						|
    addLine('ERROR: incorrect lua syntax: ' .. err:sub(5), 'red')
 | 
						|
    return
 | 
						|
  end
 | 
						|
  
 | 
						|
  commandEnv['player'] = g_game.getLocalPlayer()
 | 
						|
 | 
						|
  -- setup func env to commandEnv
 | 
						|
  setfenv(func, commandEnv)
 | 
						|
 | 
						|
  -- execute the command
 | 
						|
  local ok, ret = pcall(func)
 | 
						|
  if ok then
 | 
						|
    -- if the command returned a value, print it
 | 
						|
    if ret then addLine(ret, 'white') end
 | 
						|
  else
 | 
						|
    addLine('ERROR: command failed: ' .. ret, 'red')
 | 
						|
  end
 | 
						|
end
 | 
						|
 | 
						|
function clear()
 | 
						|
  terminalBuffer:destroyChildren()
 | 
						|
  terminalSelectText:setText('')
 | 
						|
  cachedLines = {}
 | 
						|
  allLines = {}
 | 
						|
end
 |