parent
ace6110807
commit
fe9ab1c922
@ -0,0 +1,369 @@ |
||||
local core = require "core" |
||||
local keymap = require "core.keymap" |
||||
local command = require "core.command" |
||||
local common = require "core.common" |
||||
local config = require "core.config" |
||||
local style = require "core.style" |
||||
local View = require "core.view" |
||||
|
||||
config.console_size = 250 * SCALE |
||||
config.max_console_lines = 200 |
||||
config.autoscroll_console = true |
||||
|
||||
local files = { |
||||
script = core.temp_filename(".sh"), |
||||
script2 = core.temp_filename(".sh"), |
||||
output = core.temp_filename(), |
||||
complete = core.temp_filename(), |
||||
} |
||||
|
||||
local console = {} |
||||
|
||||
local views = {} |
||||
local pending_threads = {} |
||||
local thread_active = false |
||||
local output = nil |
||||
local output_id = 0 |
||||
local visible = false |
||||
|
||||
function console.clear() |
||||
output = { { text = "", time = 0 } } |
||||
end |
||||
|
||||
|
||||
local function read_file(filename, offset) |
||||
local fp = io.open(filename, "rb") |
||||
fp:seek("set", offset or 0) |
||||
local res = fp:read("*a") |
||||
fp:close() |
||||
return res |
||||
end |
||||
|
||||
|
||||
local function write_file(filename, text) |
||||
local fp = io.open(filename, "w") |
||||
fp:write(text) |
||||
fp:close() |
||||
end |
||||
|
||||
|
||||
local function lines(text) |
||||
return (text .. "\n"):gmatch("(.-)\n") |
||||
end |
||||
|
||||
|
||||
local function push_output(str, opt) |
||||
local first = true |
||||
for line in lines(str) do |
||||
if first then |
||||
line = table.remove(output).text .. line |
||||
end |
||||
line = line:gsub("\x1b%[[%d;]+m", "") -- strip ANSI colors |
||||
table.insert(output, { |
||||
text = line, |
||||
time = os.time(), |
||||
icon = line:find(opt.error_pattern) and "!" |
||||
or line:find(opt.warning_pattern) and "i", |
||||
file_pattern = opt.file_pattern, |
||||
}) |
||||
if #output > config.max_console_lines then |
||||
table.remove(output, 1) |
||||
for view in pairs(views) do |
||||
view:on_line_removed() |
||||
end |
||||
end |
||||
first = false |
||||
end |
||||
output_id = output_id + 1 |
||||
core.redraw = true |
||||
end |
||||
|
||||
|
||||
local function init_opt(opt) |
||||
local res = { |
||||
command = "", |
||||
file_pattern = "[^?:%s]+%.[^?:%s]+", |
||||
error_pattern = "error", |
||||
warning_pattern = "warning", |
||||
on_complete = function() end, |
||||
} |
||||
for k, v in pairs(res) do |
||||
res[k] = opt[k] or v |
||||
end |
||||
return res |
||||
end |
||||
|
||||
|
||||
function console.run(opt) |
||||
opt = init_opt(opt) |
||||
|
||||
local function thread() |
||||
-- init script file(s) |
||||
write_file(files.script, string.format([[ |
||||
%s |
||||
touch %q |
||||
]], opt.command, files.complete)) |
||||
local shell = os.getenv("SHELL") or '/bin/sh' |
||||
system.exec(string.format("%q %q >%q 2>&1", shell, files.script, files.output)) |
||||
|
||||
-- checks output file for change and reads |
||||
local last_size = 0 |
||||
local function check_output_file() |
||||
local info = system.get_file_info(files.output) |
||||
if info and info.size > last_size then |
||||
local text = read_file(files.output, last_size) |
||||
push_output(text, opt) |
||||
last_size = info.size |
||||
end |
||||
end |
||||
|
||||
-- read output file until we get a file indicating completion |
||||
while not system.get_file_info(files.complete) do |
||||
check_output_file() |
||||
coroutine.yield(0.1) |
||||
end |
||||
check_output_file() |
||||
if output[#output].text ~= "" then |
||||
push_output("\n", opt) |
||||
end |
||||
push_output("!DIVIDER\n", opt) |
||||
|
||||
-- clean up and finish |
||||
for _, file in pairs(files) do |
||||
os.remove(file) |
||||
end |
||||
opt.on_complete() |
||||
|
||||
-- handle pending thread |
||||
local pending = table.remove(pending_threads, 1) |
||||
if pending then |
||||
core.add_thread(pending) |
||||
else |
||||
thread_active = false |
||||
end |
||||
end |
||||
|
||||
-- push/init thread |
||||
if thread_active then |
||||
table.insert(pending_threads, thread) |
||||
else |
||||
core.add_thread(thread) |
||||
thread_active = true |
||||
end |
||||
|
||||
-- make sure static console is visible if it's the only ConsoleView |
||||
local count = 0 |
||||
for _ in pairs(views) do count = count + 1 end |
||||
if count == 1 then visible = true end |
||||
end |
||||
|
||||
|
||||
|
||||
local ConsoleView = View:extend() |
||||
|
||||
function ConsoleView:new() |
||||
ConsoleView.super.new(self) |
||||
self.scrollable = true |
||||
self.hovered_idx = -1 |
||||
views[self] = true |
||||
end |
||||
|
||||
|
||||
function ConsoleView:try_close(...) |
||||
ConsoleView.super.try_close(self, ...) |
||||
views[self] = nil |
||||
end |
||||
|
||||
|
||||
function ConsoleView:get_name() |
||||
return "Console" |
||||
end |
||||
|
||||
|
||||
function ConsoleView:get_line_height() |
||||
return style.code_font:get_height() * config.line_height |
||||
end |
||||
|
||||
|
||||
function ConsoleView:get_line_count() |
||||
return #output - (output[#output].text == "" and 1 or 0) |
||||
end |
||||
|
||||
|
||||
function ConsoleView:get_scrollable_size() |
||||
return self:get_line_count() * self:get_line_height() + style.padding.y * 2 |
||||
end |
||||
|
||||
|
||||
function ConsoleView:get_visible_line_range() |
||||
local lh = self:get_line_height() |
||||
local min = math.max(1, math.floor(self.scroll.y / lh)) |
||||
return min, min + math.floor(self.size.y / lh) + 1 |
||||
end |
||||
|
||||
|
||||
function ConsoleView:on_mouse_moved(mx, my, ...) |
||||
ConsoleView.super.on_mouse_moved(self, mx, my, ...) |
||||
self.hovered_idx = 0 |
||||
for i, item, x,y,w,h in self:each_visible_line() do |
||||
if mx >= x and my >= y and mx < x + w and my < y + h then |
||||
if item.text:find(item.file_pattern) then |
||||
self.hovered_idx = i |
||||
end |
||||
break |
||||
end |
||||
end |
||||
end |
||||
|
||||
|
||||
local function resolve_file(name) |
||||
if system.get_file_info(name) then |
||||
return name |
||||
end |
||||
local filenames = {} |
||||
for _, f in ipairs(core.project_files) do |
||||
table.insert(filenames, f.filename) |
||||
end |
||||
local t = common.fuzzy_match(filenames, name) |
||||
return t[1] |
||||
end |
||||
|
||||
|
||||
function ConsoleView:on_line_removed() |
||||
local diff = self:get_line_height() |
||||
self.scroll.y = self.scroll.y - diff |
||||
self.scroll.to.y = self.scroll.to.y - diff |
||||
end |
||||
|
||||
|
||||
function ConsoleView:on_mouse_pressed(...) |
||||
local caught = ConsoleView.super.on_mouse_pressed(self, ...) |
||||
if caught then |
||||
return |
||||
end |
||||
local item = output[self.hovered_idx] |
||||
if item then |
||||
local file, line, col = item.text:match(item.file_pattern) |
||||
local resolved_file = resolve_file(file) |
||||
if not resolved_file then |
||||
core.error("Couldn't resolve file \"%s\"", file) |
||||
return |
||||
end |
||||
core.try(function() |
||||
core.set_active_view(core.last_active_view) |
||||
local dv = core.root_view:open_doc(core.open_doc(resolved_file)) |
||||
if line then |
||||
dv.doc:set_selection(line, col or 0) |
||||
dv:scroll_to_line(line, false, true) |
||||
end |
||||
end) |
||||
end |
||||
end |
||||
|
||||
|
||||
function ConsoleView:each_visible_line() |
||||
return coroutine.wrap(function() |
||||
local x, y = self:get_content_offset() |
||||
local lh = self:get_line_height() |
||||
local min, max = self:get_visible_line_range() |
||||
y = y + lh * (min - 1) + style.padding.y |
||||
max = math.min(max, self:get_line_count()) |
||||
|
||||
for i = min, max do |
||||
local item = output[i] |
||||
if not item then break end |
||||
coroutine.yield(i, item, x, y, self.size.x, lh) |
||||
y = y + lh |
||||
end |
||||
end) |
||||
end |
||||
|
||||
|
||||
function ConsoleView:update(...) |
||||
if self.last_output_id ~= output_id then |
||||
if config.autoscroll_console then |
||||
self.scroll.to.y = self:get_scrollable_size() |
||||
end |
||||
self.last_output_id = output_id |
||||
end |
||||
ConsoleView.super.update(self, ...) |
||||
end |
||||
|
||||
|
||||
function ConsoleView:draw() |
||||
self:draw_background(style.background) |
||||
local icon_w = style.icon_font:get_width("!") |
||||
|
||||
for i, item, x, y, w, h in self:each_visible_line() do |
||||
local tx = x + style.padding.x |
||||
local time = os.date("%H:%M:%S", item.time) |
||||
local color = style.text |
||||
if self.hovered_idx == i then |
||||
color = style.accent |
||||
renderer.draw_rect(x, y, w, h, style.line_highlight) |
||||
end |
||||
if item.text == "!DIVIDER" then |
||||
local w = style.font:get_width(time) |
||||
renderer.draw_rect(tx, y + h / 2, w, math.ceil(SCALE * 1), style.dim) |
||||
else |
||||
tx = common.draw_text(style.font, style.dim, time, "left", tx, y, w, h) |
||||
tx = tx + style.padding.x |
||||
if item.icon then |
||||
common.draw_text(style.icon_font, color, item.icon, "left", tx, y, w, h) |
||||
end |
||||
tx = tx + icon_w + style.padding.x |
||||
common.draw_text(style.code_font, color, item.text, "left", tx, y, w, h) |
||||
end |
||||
end |
||||
|
||||
self:draw_scrollbar(self) |
||||
end |
||||
|
||||
|
||||
-- init static bottom-of-screen console |
||||
local view = ConsoleView() |
||||
local node = core.root_view:get_active_node() |
||||
node:split("down", view, true) |
||||
|
||||
function view:update(...) |
||||
local dest = visible and config.console_size or 0 |
||||
self:move_towards(self.size, "y", dest) |
||||
ConsoleView.update(self, ...) |
||||
end |
||||
|
||||
|
||||
local last_command = "" |
||||
|
||||
command.add(nil, { |
||||
["console:reset-output"] = function() |
||||
output = { { text = "", time = 0 } } |
||||
end, |
||||
|
||||
["console:open-console"] = function() |
||||
local node = core.root_view:get_active_node() |
||||
node:add_view(ConsoleView()) |
||||
end, |
||||
|
||||
["console:toggle"] = function() |
||||
visible = not visible |
||||
end, |
||||
|
||||
["console:run"] = function() |
||||
core.command_view:set_text(last_command, true) |
||||
core.command_view:enter("Run Console Command", function(cmd) |
||||
console.run { command = cmd } |
||||
last_command = cmd |
||||
end) |
||||
end |
||||
}) |
||||
|
||||
keymap.add { |
||||
["ctrl+."] = "console:toggle", |
||||
["ctrl+shift+."] = "console:run", |
||||
} |
||||
|
||||
-- for `workspace` plugin: |
||||
package.loaded["plugins.console.view"] = ConsoleView |
||||
|
||||
console.clear() |
||||
return console |
Loading…
Reference in new issue