Merge branch 'master' into lite-xl-windows-dark-theme-title-bar-support
This commit is contained in:
commit
bd1490a007
|
@ -0,0 +1,40 @@
|
|||
local style = require "core.style"
|
||||
local keymap = require "core.keymap"
|
||||
local View = require "core.view"
|
||||
|
||||
local EmptyView = View:extend()
|
||||
|
||||
local function draw_text(x, y, color)
|
||||
local th = style.big_font:get_height()
|
||||
local dh = 2 * th + style.padding.y * 2
|
||||
local x1, y1 = x, y + (dh - th) / 2
|
||||
x = renderer.draw_text(style.big_font, "Lite XL", x1, y1, color)
|
||||
renderer.draw_text(style.font, "version " .. VERSION, x1, y1 + th, color)
|
||||
x = x + style.padding.x
|
||||
renderer.draw_rect(x, y, math.ceil(1 * SCALE), dh, color)
|
||||
local lines = {
|
||||
{ fmt = "%s to run a command", cmd = "core:find-command" },
|
||||
{ fmt = "%s to open a file from the project", cmd = "core:find-file" },
|
||||
{ fmt = "%s to change project folder", cmd = "core:change-project-folder" },
|
||||
{ fmt = "%s to open a project folder", cmd = "core:open-project-folder" },
|
||||
}
|
||||
th = style.font:get_height()
|
||||
y = y + (dh - (th + style.padding.y) * #lines) / 2
|
||||
local w = 0
|
||||
for _, line in ipairs(lines) do
|
||||
local text = string.format(line.fmt, keymap.get_binding(line.cmd))
|
||||
w = math.max(w, renderer.draw_text(style.font, text, x + style.padding.x, y, color))
|
||||
y = y + th + style.padding.y
|
||||
end
|
||||
return w, dh
|
||||
end
|
||||
|
||||
function EmptyView:draw()
|
||||
self:draw_background(style.background)
|
||||
local w, h = draw_text(0, 0, { 0, 0, 0, 0 })
|
||||
local x = self.position.x + math.max(style.padding.x, (self.size.x - w) / 2)
|
||||
local y = self.position.y + (self.size.y - h) / 2
|
||||
draw_text(x, y, style.dim)
|
||||
end
|
||||
|
||||
return EmptyView
|
|
@ -0,0 +1,737 @@
|
|||
local core = require "core"
|
||||
local common = require "core.common"
|
||||
local config = require "core.config"
|
||||
local style = require "core.style"
|
||||
local Object = require "core.object"
|
||||
local EmptyView = require "core.emptyview"
|
||||
local View = require "core.view"
|
||||
|
||||
local Node = Object:extend()
|
||||
|
||||
function Node:new(type)
|
||||
self.type = type or "leaf"
|
||||
self.position = { x = 0, y = 0 }
|
||||
self.size = { x = 0, y = 0 }
|
||||
self.views = {}
|
||||
self.divider = 0.5
|
||||
if self.type == "leaf" then
|
||||
self:add_view(EmptyView())
|
||||
end
|
||||
self.hovered = {x = -1, y = -1 }
|
||||
self.hovered_close = 0
|
||||
self.tab_shift = 0
|
||||
self.tab_offset = 1
|
||||
self.tab_width = style.tab_width
|
||||
self.move_towards = View.move_towards
|
||||
end
|
||||
|
||||
|
||||
function Node:propagate(fn, ...)
|
||||
self.a[fn](self.a, ...)
|
||||
self.b[fn](self.b, ...)
|
||||
end
|
||||
|
||||
|
||||
function Node:on_mouse_moved(x, y, ...)
|
||||
if self.type == "leaf" then
|
||||
self.hovered.x, self.hovered.y = x, y
|
||||
self.active_view:on_mouse_moved(x, y, ...)
|
||||
else
|
||||
self:propagate("on_mouse_moved", x, y, ...)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function Node:on_mouse_released(...)
|
||||
if self.type == "leaf" then
|
||||
self.active_view:on_mouse_released(...)
|
||||
else
|
||||
self:propagate("on_mouse_released", ...)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function Node:consume(node)
|
||||
for k, _ in pairs(self) do self[k] = nil end
|
||||
for k, v in pairs(node) do self[k] = v end
|
||||
end
|
||||
|
||||
|
||||
local type_map = { up="vsplit", down="vsplit", left="hsplit", right="hsplit" }
|
||||
|
||||
-- The "locked" argument below should be in the form {x = <boolean>, y = <boolean>}
|
||||
-- and it indicates if the node want to have a fixed size along the axis where the
|
||||
-- boolean is true. If not it will be expanded to take all the available space.
|
||||
-- The "resizable" flag indicates if, along the "locked" axis the node can be resized
|
||||
-- by the user. If the node is marked as resizable their view should provide a
|
||||
-- set_target_size method.
|
||||
function Node:split(dir, view, locked, resizable)
|
||||
assert(self.type == "leaf", "Tried to split non-leaf node")
|
||||
local node_type = assert(type_map[dir], "Invalid direction")
|
||||
local last_active = core.active_view
|
||||
local child = Node()
|
||||
child:consume(self)
|
||||
self:consume(Node(node_type))
|
||||
self.a = child
|
||||
self.b = Node()
|
||||
if view then self.b:add_view(view) end
|
||||
if locked then
|
||||
assert(type(locked) == 'table')
|
||||
self.b.locked = locked
|
||||
self.b.resizable = resizable or false
|
||||
core.set_active_view(last_active)
|
||||
end
|
||||
if dir == "up" or dir == "left" then
|
||||
self.a, self.b = self.b, self.a
|
||||
return self.a
|
||||
end
|
||||
return self.b
|
||||
end
|
||||
|
||||
function Node:remove_view(root, view)
|
||||
if #self.views > 1 then
|
||||
local idx = self:get_view_idx(view)
|
||||
if idx < self.tab_offset then
|
||||
self.tab_offset = self.tab_offset - 1
|
||||
end
|
||||
table.remove(self.views, idx)
|
||||
if self.active_view == view then
|
||||
self:set_active_view(self.views[idx] or self.views[#self.views])
|
||||
end
|
||||
else
|
||||
local parent = self:get_parent_node(root)
|
||||
local is_a = (parent.a == self)
|
||||
local other = parent[is_a and "b" or "a"]
|
||||
local locked_size_x, locked_size_y = other:get_locked_size()
|
||||
local locked_size
|
||||
if parent.type == "hsplit" then
|
||||
locked_size = locked_size_x
|
||||
else
|
||||
locked_size = locked_size_y
|
||||
end
|
||||
local next_primary
|
||||
if self.is_primary_node then
|
||||
next_primary = core.root_view:select_next_primary_node()
|
||||
end
|
||||
if locked_size or (self.is_primary_node and not next_primary) then
|
||||
self.views = {}
|
||||
self:add_view(EmptyView())
|
||||
else
|
||||
if other == next_primary then
|
||||
next_primary = parent
|
||||
end
|
||||
parent:consume(other)
|
||||
local p = parent
|
||||
while p.type ~= "leaf" do
|
||||
p = p[is_a and "a" or "b"]
|
||||
end
|
||||
p:set_active_view(p.active_view)
|
||||
if self.is_primary_node then
|
||||
next_primary.is_primary_node = true
|
||||
end
|
||||
end
|
||||
end
|
||||
core.last_active_view = nil
|
||||
end
|
||||
|
||||
function Node:close_view(root, view)
|
||||
local do_close = function()
|
||||
self:remove_view(root, view)
|
||||
end
|
||||
view:try_close(do_close)
|
||||
end
|
||||
|
||||
|
||||
function Node:close_active_view(root)
|
||||
self:close_view(root, self.active_view)
|
||||
end
|
||||
|
||||
|
||||
function Node:add_view(view, idx)
|
||||
assert(self.type == "leaf", "Tried to add view to non-leaf node")
|
||||
assert(not self.locked, "Tried to add view to locked node")
|
||||
if self.views[1] and self.views[1]:is(EmptyView) then
|
||||
table.remove(self.views)
|
||||
end
|
||||
table.insert(self.views, idx or (#self.views + 1), view)
|
||||
self:set_active_view(view)
|
||||
end
|
||||
|
||||
|
||||
function Node:set_active_view(view)
|
||||
assert(self.type == "leaf", "Tried to set active view on non-leaf node")
|
||||
self.active_view = view
|
||||
core.set_active_view(view)
|
||||
end
|
||||
|
||||
|
||||
function Node:get_view_idx(view)
|
||||
for i, v in ipairs(self.views) do
|
||||
if v == view then return i end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function Node:get_node_for_view(view)
|
||||
for _, v in ipairs(self.views) do
|
||||
if v == view then return self end
|
||||
end
|
||||
if self.type ~= "leaf" then
|
||||
return self.a:get_node_for_view(view) or self.b:get_node_for_view(view)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function Node:get_parent_node(root)
|
||||
if root.a == self or root.b == self then
|
||||
return root
|
||||
elseif root.type ~= "leaf" then
|
||||
return self:get_parent_node(root.a) or self:get_parent_node(root.b)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function Node:get_children(t)
|
||||
t = t or {}
|
||||
for _, view in ipairs(self.views) do
|
||||
table.insert(t, view)
|
||||
end
|
||||
if self.a then self.a:get_children(t) end
|
||||
if self.b then self.b:get_children(t) end
|
||||
return t
|
||||
end
|
||||
|
||||
|
||||
-- return the width including the padding space and separately
|
||||
-- the padding space itself
|
||||
local function get_scroll_button_width()
|
||||
local w = style.icon_font:get_width(">")
|
||||
local pad = w
|
||||
return w + 2 * pad, pad
|
||||
end
|
||||
|
||||
|
||||
function Node:get_divider_overlapping_point(px, py)
|
||||
if self.type ~= "leaf" then
|
||||
local axis = self.type == "hsplit" and "x" or "y"
|
||||
if self.a:is_resizable(axis) and self.b:is_resizable(axis) then
|
||||
local p = 6
|
||||
local x, y, w, h = self:get_divider_rect()
|
||||
x, y = x - p, y - p
|
||||
w, h = w + p * 2, h + p * 2
|
||||
if px > x and py > y and px < x + w and py < y + h then
|
||||
return self
|
||||
end
|
||||
end
|
||||
return self.a:get_divider_overlapping_point(px, py)
|
||||
or self.b:get_divider_overlapping_point(px, py)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function Node:get_visible_tabs_number()
|
||||
return math.min(#self.views - self.tab_offset + 1, config.max_tabs)
|
||||
end
|
||||
|
||||
|
||||
function Node:get_tab_overlapping_point(px, py)
|
||||
if not self:should_show_tabs() then return nil end
|
||||
local tabs_number = self:get_visible_tabs_number()
|
||||
local x1, y1, w, h = self:get_tab_rect(self.tab_offset)
|
||||
local x2, y2 = self:get_tab_rect(self.tab_offset + tabs_number)
|
||||
if px >= x1 and py >= y1 and px < x2 and py < y1 + h then
|
||||
return math.floor((px - x1) / w) + self.tab_offset
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function Node:should_show_tabs()
|
||||
if self.locked then return false end
|
||||
local dn = core.root_view.dragged_node
|
||||
if #self.views > 1
|
||||
or (dn and dn.dragging) then -- show tabs while dragging
|
||||
return true
|
||||
elseif config.always_show_tabs then
|
||||
return not self.views[1]:is(EmptyView)
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
|
||||
local function close_button_location(x, w)
|
||||
local cw = style.icon_font:get_width("C")
|
||||
local pad = style.padding.y
|
||||
return x + w - pad - cw, cw, pad
|
||||
end
|
||||
|
||||
|
||||
function Node:get_scroll_button_index(px, py)
|
||||
if #self.views == 1 then return end
|
||||
for i = 1, 2 do
|
||||
local x, y, w, h = self:get_scroll_button_rect(i)
|
||||
if px >= x and px < x + w and py >= y and py < y + h then
|
||||
return i
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function Node:tab_hovered_update(px, py)
|
||||
local tab_index = self:get_tab_overlapping_point(px, py)
|
||||
self.hovered_tab = tab_index
|
||||
self.hovered_close = 0
|
||||
self.hovered_scroll_button = 0
|
||||
if tab_index then
|
||||
local x, y, w, h = self:get_tab_rect(tab_index)
|
||||
local cx, cw = close_button_location(x, w)
|
||||
if px >= cx and px < cx + cw and py >= y and py < y + h and config.tab_close_button then
|
||||
self.hovered_close = tab_index
|
||||
end
|
||||
else
|
||||
self.hovered_scroll_button = self:get_scroll_button_index(px, py) or 0
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function Node:get_child_overlapping_point(x, y)
|
||||
local child
|
||||
if self.type == "leaf" then
|
||||
return self
|
||||
elseif self.type == "hsplit" then
|
||||
child = (x < self.b.position.x) and self.a or self.b
|
||||
elseif self.type == "vsplit" then
|
||||
child = (y < self.b.position.y) and self.a or self.b
|
||||
end
|
||||
return child:get_child_overlapping_point(x, y)
|
||||
end
|
||||
|
||||
|
||||
function Node:get_scroll_button_rect(index)
|
||||
local w, pad = get_scroll_button_width()
|
||||
local h = style.font:get_height() + style.padding.y * 2
|
||||
local x = self.position.x + (index == 1 and 0 or self.size.x - w)
|
||||
return x, self.position.y, w, h, pad
|
||||
end
|
||||
|
||||
|
||||
function Node:get_tab_rect(idx)
|
||||
local sbw = get_scroll_button_width()
|
||||
local maxw = self.size.x - 2 * sbw
|
||||
local x0 = self.position.x + sbw
|
||||
local x1 = x0 + common.clamp(self.tab_width * (idx - 1) - self.tab_shift, 0, maxw)
|
||||
local x2 = x0 + common.clamp(self.tab_width * idx - self.tab_shift, 0, maxw)
|
||||
local h = style.font:get_height() + style.padding.y * 2
|
||||
return x1, self.position.y, x2 - x1, h
|
||||
end
|
||||
|
||||
|
||||
function Node:get_divider_rect()
|
||||
local x, y = self.position.x, self.position.y
|
||||
if self.type == "hsplit" then
|
||||
return x + self.a.size.x, y, style.divider_size, self.size.y
|
||||
elseif self.type == "vsplit" then
|
||||
return x, y + self.a.size.y, self.size.x, style.divider_size
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Return two values for x and y axis and each of them is either falsy or a number.
|
||||
-- A falsy value indicate no fixed size along the corresponding direction.
|
||||
function Node:get_locked_size()
|
||||
if self.type == "leaf" then
|
||||
if self.locked then
|
||||
local size = self.active_view.size
|
||||
-- The values below should be either a falsy value or a number
|
||||
local sx = (self.locked and self.locked.x) and size.x
|
||||
local sy = (self.locked and self.locked.y) and size.y
|
||||
return sx, sy
|
||||
end
|
||||
else
|
||||
local x1, y1 = self.a:get_locked_size()
|
||||
local x2, y2 = self.b:get_locked_size()
|
||||
-- The values below should be either a falsy value or a number
|
||||
local sx, sy
|
||||
if self.type == 'hsplit' then
|
||||
if x1 and x2 then
|
||||
local dsx = (x1 < 1 or x2 < 1) and 0 or style.divider_size
|
||||
sx = x1 + x2 + dsx
|
||||
end
|
||||
sy = y1 or y2
|
||||
else
|
||||
if y1 and y2 then
|
||||
local dsy = (y1 < 1 or y2 < 1) and 0 or style.divider_size
|
||||
sy = y1 + y2 + dsy
|
||||
end
|
||||
sx = x1 or x2
|
||||
end
|
||||
return sx, sy
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function Node.copy_position_and_size(dst, src)
|
||||
dst.position.x, dst.position.y = src.position.x, src.position.y
|
||||
dst.size.x, dst.size.y = src.size.x, src.size.y
|
||||
end
|
||||
|
||||
|
||||
-- calculating the sizes is the same for hsplits and vsplits, except the x/y
|
||||
-- axis are swapped; this function lets us use the same code for both
|
||||
local function calc_split_sizes(self, x, y, x1, x2, y1, y2)
|
||||
local ds = ((x1 and x1 < 1) or (x2 and x2 < 1)) and 0 or style.divider_size
|
||||
local n = x1 and x1 + ds or (x2 and self.size[x] - x2 or math.floor(self.size[x] * self.divider))
|
||||
self.a.position[x] = self.position[x]
|
||||
self.a.position[y] = self.position[y]
|
||||
self.a.size[x] = n - ds
|
||||
self.a.size[y] = self.size[y]
|
||||
self.b.position[x] = self.position[x] + n
|
||||
self.b.position[y] = self.position[y]
|
||||
self.b.size[x] = self.size[x] - n
|
||||
self.b.size[y] = self.size[y]
|
||||
end
|
||||
|
||||
|
||||
function Node:update_layout()
|
||||
if self.type == "leaf" then
|
||||
local av = self.active_view
|
||||
if self:should_show_tabs() then
|
||||
local _, _, _, th = self:get_tab_rect(1)
|
||||
av.position.x, av.position.y = self.position.x, self.position.y + th
|
||||
av.size.x, av.size.y = self.size.x, self.size.y - th
|
||||
else
|
||||
Node.copy_position_and_size(av, self)
|
||||
end
|
||||
else
|
||||
local x1, y1 = self.a:get_locked_size()
|
||||
local x2, y2 = self.b:get_locked_size()
|
||||
if self.type == "hsplit" then
|
||||
calc_split_sizes(self, "x", "y", x1, x2)
|
||||
elseif self.type == "vsplit" then
|
||||
calc_split_sizes(self, "y", "x", y1, y2)
|
||||
end
|
||||
self.a:update_layout()
|
||||
self.b:update_layout()
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function Node:scroll_tabs_to_visible()
|
||||
local index = self:get_view_idx(self.active_view)
|
||||
if index then
|
||||
local tabs_number = self:get_visible_tabs_number()
|
||||
if self.tab_offset > index then
|
||||
self.tab_offset = index
|
||||
elseif self.tab_offset + tabs_number - 1 < index then
|
||||
self.tab_offset = index - tabs_number + 1
|
||||
elseif tabs_number < config.max_tabs and self.tab_offset > 1 then
|
||||
self.tab_offset = #self.views - config.max_tabs + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function Node:scroll_tabs(dir)
|
||||
local view_index = self:get_view_idx(self.active_view)
|
||||
if dir == 1 then
|
||||
if self.tab_offset > 1 then
|
||||
self.tab_offset = self.tab_offset - 1
|
||||
local last_index = self.tab_offset + self:get_visible_tabs_number() - 1
|
||||
if view_index > last_index then
|
||||
self:set_active_view(self.views[last_index])
|
||||
end
|
||||
end
|
||||
elseif dir == 2 then
|
||||
local tabs_number = self:get_visible_tabs_number()
|
||||
if self.tab_offset + tabs_number - 1 < #self.views then
|
||||
self.tab_offset = self.tab_offset + 1
|
||||
local view_index = self:get_view_idx(self.active_view)
|
||||
if view_index < self.tab_offset then
|
||||
self:set_active_view(self.views[self.tab_offset])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function Node:target_tab_width()
|
||||
local n = self:get_visible_tabs_number()
|
||||
local w = self.size.x - get_scroll_button_width() * 2
|
||||
return common.clamp(style.tab_width, w / config.max_tabs, w / n)
|
||||
end
|
||||
|
||||
|
||||
function Node:update()
|
||||
if self.type == "leaf" then
|
||||
self:scroll_tabs_to_visible()
|
||||
for _, view in ipairs(self.views) do
|
||||
view:update()
|
||||
end
|
||||
self:tab_hovered_update(self.hovered.x, self.hovered.y)
|
||||
local tab_width = self:target_tab_width()
|
||||
self:move_towards("tab_shift", tab_width * (self.tab_offset - 1))
|
||||
self:move_towards("tab_width", tab_width)
|
||||
else
|
||||
self.a:update()
|
||||
self.b:update()
|
||||
end
|
||||
end
|
||||
|
||||
function Node:draw_tab(text, is_active, is_hovered, is_close_hovered, x, y, w, h, standalone)
|
||||
local ds = style.divider_size
|
||||
local dots_width = style.font:get_width("…")
|
||||
local color = style.dim
|
||||
local padding_y = style.padding.y
|
||||
renderer.draw_rect(x + w, y + padding_y, ds, h - padding_y * 2, style.dim)
|
||||
if standalone then
|
||||
renderer.draw_rect(x-1, y-1, w+2, h+2, style.background2)
|
||||
end
|
||||
if is_active then
|
||||
color = style.text
|
||||
renderer.draw_rect(x, y, w, h, style.background)
|
||||
renderer.draw_rect(x + w, y, ds, h, style.divider)
|
||||
renderer.draw_rect(x - ds, y, ds, h, style.divider)
|
||||
end
|
||||
local cx, cw, cspace = close_button_location(x, w)
|
||||
local show_close_button = ((is_active or is_hovered) and not standalone and config.tab_close_button)
|
||||
if show_close_button then
|
||||
local close_style = is_close_hovered and style.text or style.dim
|
||||
common.draw_text(style.icon_font, close_style, "C", nil, cx, y, 0, h)
|
||||
end
|
||||
if is_hovered then
|
||||
color = style.text
|
||||
end
|
||||
local padx = style.padding.x
|
||||
-- Normally we should substract "cspace" from text_avail_width and from the
|
||||
-- clipping width. It is the padding space we give to the left and right of the
|
||||
-- close button. However, since we are using dots to terminate filenames, we
|
||||
-- choose to ignore "cspace" accepting that the text can possibly "touch" the
|
||||
-- close button.
|
||||
local text_avail_width = cx - x - padx
|
||||
core.push_clip_rect(x, y, cx - x, h)
|
||||
x, w = x + padx, w - padx * 2
|
||||
local align = "center"
|
||||
if style.font:get_width(text) > text_avail_width then
|
||||
align = "left"
|
||||
for i = 1, #text do
|
||||
local reduced_text = text:sub(1, #text - i)
|
||||
if style.font:get_width(reduced_text) + dots_width <= text_avail_width then
|
||||
text = reduced_text .. "…"
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
common.draw_text(style.font, color, text, align, x, y, w, h)
|
||||
core.pop_clip_rect()
|
||||
end
|
||||
|
||||
function Node:draw_tabs()
|
||||
local x, y, w, h, scroll_padding = self:get_scroll_button_rect(1)
|
||||
local ds = style.divider_size
|
||||
local dots_width = style.font:get_width("…")
|
||||
core.push_clip_rect(x, y, self.size.x, h)
|
||||
renderer.draw_rect(x, y, self.size.x, h, style.background2)
|
||||
renderer.draw_rect(x, y + h - ds, self.size.x, ds, style.divider)
|
||||
|
||||
if self.tab_offset > 1 then
|
||||
local button_style = self.hovered_scroll_button == 1 and style.text or style.dim
|
||||
common.draw_text(style.icon_font, button_style, "<", nil, x + scroll_padding, y, 0, h)
|
||||
end
|
||||
|
||||
local tabs_number = self:get_visible_tabs_number()
|
||||
if #self.views > self.tab_offset + tabs_number - 1 then
|
||||
local xrb, yrb, wrb = self:get_scroll_button_rect(2)
|
||||
local button_style = self.hovered_scroll_button == 2 and style.text or style.dim
|
||||
common.draw_text(style.icon_font, button_style, ">", nil, xrb + scroll_padding, yrb, 0, h)
|
||||
end
|
||||
|
||||
for i = self.tab_offset, self.tab_offset + tabs_number - 1 do
|
||||
local view = self.views[i]
|
||||
local x, y, w, h = self:get_tab_rect(i)
|
||||
self:draw_tab(view:get_name(), view == self.active_view,
|
||||
i == self.hovered_tab, i == self.hovered_close,
|
||||
x, y, w, h)
|
||||
end
|
||||
|
||||
core.pop_clip_rect()
|
||||
end
|
||||
|
||||
|
||||
function Node:draw()
|
||||
if self.type == "leaf" then
|
||||
if self:should_show_tabs() then
|
||||
self:draw_tabs()
|
||||
end
|
||||
local pos, size = self.active_view.position, self.active_view.size
|
||||
core.push_clip_rect(pos.x, pos.y, size.x, size.y)
|
||||
self.active_view:draw()
|
||||
core.pop_clip_rect()
|
||||
else
|
||||
local x, y, w, h = self:get_divider_rect()
|
||||
renderer.draw_rect(x, y, w, h, style.divider)
|
||||
self:propagate("draw")
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function Node:is_empty()
|
||||
if self.type == "leaf" then
|
||||
return #self.views == 0 or (#self.views == 1 and self.views[1]:is(EmptyView))
|
||||
else
|
||||
return self.a:is_empty() and self.b:is_empty()
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function Node:close_all_docviews(keep_active)
|
||||
local node_active_view = self.active_view
|
||||
local lost_active_view = false
|
||||
if self.type == "leaf" then
|
||||
local i = 1
|
||||
while i <= #self.views do
|
||||
local view = self.views[i]
|
||||
if view.context == "session" and (not keep_active or view ~= self.active_view) then
|
||||
table.remove(self.views, i)
|
||||
if view == node_active_view then
|
||||
lost_active_view = true
|
||||
end
|
||||
else
|
||||
i = i + 1
|
||||
end
|
||||
end
|
||||
self.tab_offset = 1
|
||||
if #self.views == 0 and self.is_primary_node then
|
||||
-- if we are not the primary view and we had the active view it doesn't
|
||||
-- matter to reattribute the active view because, within the close_all_docviews
|
||||
-- top call, the primary node will take the active view anyway.
|
||||
-- Set the empty view and takes the active view.
|
||||
self:add_view(EmptyView())
|
||||
elseif #self.views > 0 and lost_active_view then
|
||||
-- In practice we never get there but if a view remain we need
|
||||
-- to reset the Node's active view.
|
||||
self:set_active_view(self.views[1])
|
||||
end
|
||||
else
|
||||
self.a:close_all_docviews(keep_active)
|
||||
self.b:close_all_docviews(keep_active)
|
||||
if self.a:is_empty() and not self.a.is_primary_node then
|
||||
self:consume(self.b)
|
||||
elseif self.b:is_empty() and not self.b.is_primary_node then
|
||||
self:consume(self.a)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Returns true for nodes that accept either "proportional" resizes (based on the
|
||||
-- node.divider) or "locked" resizable nodes (along the resize axis).
|
||||
function Node:is_resizable(axis)
|
||||
if self.type == 'leaf' then
|
||||
return not self.locked or not self.locked[axis] or self.resizable
|
||||
else
|
||||
local a_resizable = self.a:is_resizable(axis)
|
||||
local b_resizable = self.b:is_resizable(axis)
|
||||
return a_resizable and b_resizable
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Return true iff it is a locked pane along the rezise axis and is
|
||||
-- declared "resizable".
|
||||
function Node:is_locked_resizable(axis)
|
||||
return self.locked and self.locked[axis] and self.resizable
|
||||
end
|
||||
|
||||
|
||||
function Node:resize(axis, value)
|
||||
-- the application works fine with non-integer values but to have pixel-perfect
|
||||
-- placements of view elements, like the scrollbar, we round the value to be
|
||||
-- an integer.
|
||||
value = math.floor(value)
|
||||
if self.type == 'leaf' then
|
||||
-- If it is not locked we don't accept the
|
||||
-- resize operation here because for proportional panes the resize is
|
||||
-- done using the "divider" value of the parent node.
|
||||
if self:is_locked_resizable(axis) then
|
||||
return self.active_view:set_target_size(axis, value)
|
||||
end
|
||||
else
|
||||
if self.type == (axis == "x" and "hsplit" or "vsplit") then
|
||||
-- we are resizing a node that is splitted along the resize axis
|
||||
if self.a:is_locked_resizable(axis) and self.b:is_locked_resizable(axis) then
|
||||
local rem_value = value - self.a.size[axis]
|
||||
if rem_value >= 0 then
|
||||
return self.b.active_view:set_target_size(axis, rem_value)
|
||||
else
|
||||
self.b.active_view:set_target_size(axis, 0)
|
||||
return self.a.active_view:set_target_size(axis, value)
|
||||
end
|
||||
end
|
||||
else
|
||||
-- we are resizing a node that is splitted along the axis perpendicular
|
||||
-- to the resize axis
|
||||
local a_resizable = self.a:is_resizable(axis)
|
||||
local b_resizable = self.b:is_resizable(axis)
|
||||
if a_resizable and b_resizable then
|
||||
self.a:resize(axis, value)
|
||||
self.b:resize(axis, value)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function Node:get_split_type(mouse_x, mouse_y)
|
||||
local x, y = self.position.x, self.position.y
|
||||
local w, h = self.size.x, self.size.y
|
||||
local _, _, _, tab_h = self:get_scroll_button_rect(1)
|
||||
y = y + tab_h
|
||||
h = h - tab_h
|
||||
|
||||
local local_mouse_x = mouse_x - x
|
||||
local local_mouse_y = mouse_y - y
|
||||
|
||||
if local_mouse_y < 0 then
|
||||
return "tab"
|
||||
else
|
||||
local left_pct = local_mouse_x * 100 / w
|
||||
local top_pct = local_mouse_y * 100 / h
|
||||
if left_pct <= 30 then
|
||||
return "left"
|
||||
elseif left_pct >= 70 then
|
||||
return "right"
|
||||
elseif top_pct <= 30 then
|
||||
return "up"
|
||||
elseif top_pct >= 70 then
|
||||
return "down"
|
||||
end
|
||||
return "middle"
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function Node:get_drag_overlay_tab_position(x, y, dragged_node, dragged_index)
|
||||
local tab_index = self:get_tab_overlapping_point(x, y)
|
||||
if not tab_index then
|
||||
local first_tab_x = self:get_tab_rect(1)
|
||||
if x < first_tab_x then
|
||||
-- mouse before first visible tab
|
||||
tab_index = self.tab_offset or 1
|
||||
else
|
||||
-- mouse after last visible tab
|
||||
tab_index = self:get_visible_tabs_number() + (self.tab_offset - 1 or 0)
|
||||
end
|
||||
end
|
||||
local tab_x, tab_y, tab_w, tab_h = self:get_tab_rect(tab_index)
|
||||
if x > tab_x + tab_w / 2 and tab_index <= #self.views then
|
||||
-- use next tab
|
||||
tab_x = tab_x + tab_w
|
||||
tab_index = tab_index + 1
|
||||
end
|
||||
if self == dragged_node and dragged_index and tab_index > dragged_index then
|
||||
-- the tab we are moving is counted in tab_index
|
||||
tab_index = tab_index - 1
|
||||
tab_x = tab_x - tab_w
|
||||
end
|
||||
return tab_index, tab_x, tab_y, tab_w, tab_h
|
||||
end
|
||||
|
||||
return Node
|
|
@ -1,780 +1,11 @@
|
|||
local core = require "core"
|
||||
local common = require "core.common"
|
||||
local config = require "core.config"
|
||||
local style = require "core.style"
|
||||
local keymap = require "core.keymap"
|
||||
local Object = require "core.object"
|
||||
local Node = require "core.node"
|
||||
local View = require "core.view"
|
||||
local NagView = require "core.nagview"
|
||||
local DocView = require "core.docview"
|
||||
|
||||
|
||||
local EmptyView = View:extend()
|
||||
|
||||
local function draw_text(x, y, color)
|
||||
local th = style.big_font:get_height()
|
||||
local dh = 2 * th + style.padding.y * 2
|
||||
local x1, y1 = x, y + (dh - th) / 2
|
||||
x = renderer.draw_text(style.big_font, "Lite XL", x1, y1, color)
|
||||
renderer.draw_text(style.font, "version " .. VERSION, x1, y1 + th, color)
|
||||
x = x + style.padding.x
|
||||
renderer.draw_rect(x, y, math.ceil(1 * SCALE), dh, color)
|
||||
local lines = {
|
||||
{ fmt = "%s to run a command", cmd = "core:find-command" },
|
||||
{ fmt = "%s to open a file from the project", cmd = "core:find-file" },
|
||||
{ fmt = "%s to change project folder", cmd = "core:change-project-folder" },
|
||||
{ fmt = "%s to open a project folder", cmd = "core:open-project-folder" },
|
||||
}
|
||||
th = style.font:get_height()
|
||||
y = y + (dh - (th + style.padding.y) * #lines) / 2
|
||||
local w = 0
|
||||
for _, line in ipairs(lines) do
|
||||
local text = string.format(line.fmt, keymap.get_binding(line.cmd))
|
||||
w = math.max(w, renderer.draw_text(style.font, text, x + style.padding.x, y, color))
|
||||
y = y + th + style.padding.y
|
||||
end
|
||||
return w, dh
|
||||
end
|
||||
|
||||
function EmptyView:draw()
|
||||
self:draw_background(style.background)
|
||||
local w, h = draw_text(0, 0, { 0, 0, 0, 0 })
|
||||
local x = self.position.x + math.max(style.padding.x, (self.size.x - w) / 2)
|
||||
local y = self.position.y + (self.size.y - h) / 2
|
||||
draw_text(x, y, style.dim)
|
||||
end
|
||||
|
||||
|
||||
|
||||
local Node = Object:extend()
|
||||
|
||||
function Node:new(type)
|
||||
self.type = type or "leaf"
|
||||
self.position = { x = 0, y = 0 }
|
||||
self.size = { x = 0, y = 0 }
|
||||
self.views = {}
|
||||
self.divider = 0.5
|
||||
if self.type == "leaf" then
|
||||
self:add_view(EmptyView())
|
||||
end
|
||||
self.hovered = {x = -1, y = -1 }
|
||||
self.hovered_close = 0
|
||||
self.tab_shift = 0
|
||||
self.tab_offset = 1
|
||||
self.tab_width = style.tab_width
|
||||
self.move_towards = View.move_towards
|
||||
end
|
||||
|
||||
|
||||
function Node:propagate(fn, ...)
|
||||
self.a[fn](self.a, ...)
|
||||
self.b[fn](self.b, ...)
|
||||
end
|
||||
|
||||
|
||||
function Node:on_mouse_moved(x, y, ...)
|
||||
if self.type == "leaf" then
|
||||
self.hovered.x, self.hovered.y = x, y
|
||||
self.active_view:on_mouse_moved(x, y, ...)
|
||||
else
|
||||
self:propagate("on_mouse_moved", x, y, ...)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function Node:on_mouse_released(...)
|
||||
if self.type == "leaf" then
|
||||
self.active_view:on_mouse_released(...)
|
||||
else
|
||||
self:propagate("on_mouse_released", ...)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function Node:consume(node)
|
||||
for k, _ in pairs(self) do self[k] = nil end
|
||||
for k, v in pairs(node) do self[k] = v end
|
||||
end
|
||||
|
||||
|
||||
local type_map = { up="vsplit", down="vsplit", left="hsplit", right="hsplit" }
|
||||
|
||||
-- The "locked" argument below should be in the form {x = <boolean>, y = <boolean>}
|
||||
-- and it indicates if the node want to have a fixed size along the axis where the
|
||||
-- boolean is true. If not it will be expanded to take all the available space.
|
||||
-- The "resizable" flag indicates if, along the "locked" axis the node can be resized
|
||||
-- by the user. If the node is marked as resizable their view should provide a
|
||||
-- set_target_size method.
|
||||
function Node:split(dir, view, locked, resizable)
|
||||
assert(self.type == "leaf", "Tried to split non-leaf node")
|
||||
local node_type = assert(type_map[dir], "Invalid direction")
|
||||
local last_active = core.active_view
|
||||
local child = Node()
|
||||
child:consume(self)
|
||||
self:consume(Node(node_type))
|
||||
self.a = child
|
||||
self.b = Node()
|
||||
if view then self.b:add_view(view) end
|
||||
if locked then
|
||||
assert(type(locked) == 'table')
|
||||
self.b.locked = locked
|
||||
self.b.resizable = resizable or false
|
||||
core.set_active_view(last_active)
|
||||
end
|
||||
if dir == "up" or dir == "left" then
|
||||
self.a, self.b = self.b, self.a
|
||||
return self.a
|
||||
end
|
||||
return self.b
|
||||
end
|
||||
|
||||
function Node:remove_view(root, view)
|
||||
if #self.views > 1 then
|
||||
local idx = self:get_view_idx(view)
|
||||
if idx < self.tab_offset then
|
||||
self.tab_offset = self.tab_offset - 1
|
||||
end
|
||||
table.remove(self.views, idx)
|
||||
if self.active_view == view then
|
||||
self:set_active_view(self.views[idx] or self.views[#self.views])
|
||||
end
|
||||
else
|
||||
local parent = self:get_parent_node(root)
|
||||
local is_a = (parent.a == self)
|
||||
local other = parent[is_a and "b" or "a"]
|
||||
local locked_size_x, locked_size_y = other:get_locked_size()
|
||||
local locked_size
|
||||
if parent.type == "hsplit" then
|
||||
locked_size = locked_size_x
|
||||
else
|
||||
locked_size = locked_size_y
|
||||
end
|
||||
local next_primary
|
||||
if self.is_primary_node then
|
||||
next_primary = core.root_view:select_next_primary_node()
|
||||
end
|
||||
if locked_size or (self.is_primary_node and not next_primary) then
|
||||
self.views = {}
|
||||
self:add_view(EmptyView())
|
||||
else
|
||||
if other == next_primary then
|
||||
next_primary = parent
|
||||
end
|
||||
parent:consume(other)
|
||||
local p = parent
|
||||
while p.type ~= "leaf" do
|
||||
p = p[is_a and "a" or "b"]
|
||||
end
|
||||
p:set_active_view(p.active_view)
|
||||
if self.is_primary_node then
|
||||
next_primary.is_primary_node = true
|
||||
end
|
||||
end
|
||||
end
|
||||
core.last_active_view = nil
|
||||
end
|
||||
|
||||
function Node:close_view(root, view)
|
||||
local do_close = function()
|
||||
self:remove_view(root, view)
|
||||
end
|
||||
view:try_close(do_close)
|
||||
end
|
||||
|
||||
|
||||
function Node:close_active_view(root)
|
||||
self:close_view(root, self.active_view)
|
||||
end
|
||||
|
||||
|
||||
function Node:add_view(view, idx)
|
||||
assert(self.type == "leaf", "Tried to add view to non-leaf node")
|
||||
assert(not self.locked, "Tried to add view to locked node")
|
||||
if self.views[1] and self.views[1]:is(EmptyView) then
|
||||
table.remove(self.views)
|
||||
end
|
||||
table.insert(self.views, idx or (#self.views + 1), view)
|
||||
self:set_active_view(view)
|
||||
end
|
||||
|
||||
|
||||
function Node:set_active_view(view)
|
||||
assert(self.type == "leaf", "Tried to set active view on non-leaf node")
|
||||
self.active_view = view
|
||||
core.set_active_view(view)
|
||||
end
|
||||
|
||||
|
||||
function Node:get_view_idx(view)
|
||||
for i, v in ipairs(self.views) do
|
||||
if v == view then return i end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function Node:get_node_for_view(view)
|
||||
for _, v in ipairs(self.views) do
|
||||
if v == view then return self end
|
||||
end
|
||||
if self.type ~= "leaf" then
|
||||
return self.a:get_node_for_view(view) or self.b:get_node_for_view(view)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function Node:get_parent_node(root)
|
||||
if root.a == self or root.b == self then
|
||||
return root
|
||||
elseif root.type ~= "leaf" then
|
||||
return self:get_parent_node(root.a) or self:get_parent_node(root.b)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function Node:get_children(t)
|
||||
t = t or {}
|
||||
for _, view in ipairs(self.views) do
|
||||
table.insert(t, view)
|
||||
end
|
||||
if self.a then self.a:get_children(t) end
|
||||
if self.b then self.b:get_children(t) end
|
||||
return t
|
||||
end
|
||||
|
||||
|
||||
-- return the width including the padding space and separately
|
||||
-- the padding space itself
|
||||
local function get_scroll_button_width()
|
||||
local w = style.icon_font:get_width(">")
|
||||
local pad = w
|
||||
return w + 2 * pad, pad
|
||||
end
|
||||
|
||||
|
||||
function Node:get_divider_overlapping_point(px, py)
|
||||
if self.type ~= "leaf" then
|
||||
local axis = self.type == "hsplit" and "x" or "y"
|
||||
if self.a:is_resizable(axis) and self.b:is_resizable(axis) then
|
||||
local p = 6
|
||||
local x, y, w, h = self:get_divider_rect()
|
||||
x, y = x - p, y - p
|
||||
w, h = w + p * 2, h + p * 2
|
||||
if px > x and py > y and px < x + w and py < y + h then
|
||||
return self
|
||||
end
|
||||
end
|
||||
return self.a:get_divider_overlapping_point(px, py)
|
||||
or self.b:get_divider_overlapping_point(px, py)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function Node:get_visible_tabs_number()
|
||||
return math.min(#self.views - self.tab_offset + 1, config.max_tabs)
|
||||
end
|
||||
|
||||
|
||||
function Node:get_tab_overlapping_point(px, py)
|
||||
if not self:should_show_tabs() then return nil end
|
||||
local tabs_number = self:get_visible_tabs_number()
|
||||
local x1, y1, w, h = self:get_tab_rect(self.tab_offset)
|
||||
local x2, y2 = self:get_tab_rect(self.tab_offset + tabs_number)
|
||||
if px >= x1 and py >= y1 and px < x2 and py < y1 + h then
|
||||
return math.floor((px - x1) / w) + self.tab_offset
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function Node:should_show_tabs()
|
||||
if self.locked then return false end
|
||||
local dn = core.root_view.dragged_node
|
||||
if #self.views > 1
|
||||
or (dn and dn.dragging) then -- show tabs while dragging
|
||||
return true
|
||||
elseif config.always_show_tabs then
|
||||
return not self.views[1]:is(EmptyView)
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
|
||||
local function close_button_location(x, w)
|
||||
local cw = style.icon_font:get_width("C")
|
||||
local pad = style.padding.y
|
||||
return x + w - pad - cw, cw, pad
|
||||
end
|
||||
|
||||
|
||||
function Node:get_scroll_button_index(px, py)
|
||||
if #self.views == 1 then return end
|
||||
for i = 1, 2 do
|
||||
local x, y, w, h = self:get_scroll_button_rect(i)
|
||||
if px >= x and px < x + w and py >= y and py < y + h then
|
||||
return i
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function Node:tab_hovered_update(px, py)
|
||||
local tab_index = self:get_tab_overlapping_point(px, py)
|
||||
self.hovered_tab = tab_index
|
||||
self.hovered_close = 0
|
||||
self.hovered_scroll_button = 0
|
||||
if tab_index then
|
||||
local x, y, w, h = self:get_tab_rect(tab_index)
|
||||
local cx, cw = close_button_location(x, w)
|
||||
if px >= cx and px < cx + cw and py >= y and py < y + h and config.tab_close_button then
|
||||
self.hovered_close = tab_index
|
||||
end
|
||||
else
|
||||
self.hovered_scroll_button = self:get_scroll_button_index(px, py) or 0
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function Node:get_child_overlapping_point(x, y)
|
||||
local child
|
||||
if self.type == "leaf" then
|
||||
return self
|
||||
elseif self.type == "hsplit" then
|
||||
child = (x < self.b.position.x) and self.a or self.b
|
||||
elseif self.type == "vsplit" then
|
||||
child = (y < self.b.position.y) and self.a or self.b
|
||||
end
|
||||
return child:get_child_overlapping_point(x, y)
|
||||
end
|
||||
|
||||
|
||||
function Node:get_scroll_button_rect(index)
|
||||
local w, pad = get_scroll_button_width()
|
||||
local h = style.font:get_height() + style.padding.y * 2
|
||||
local x = self.position.x + (index == 1 and 0 or self.size.x - w)
|
||||
return x, self.position.y, w, h, pad
|
||||
end
|
||||
|
||||
|
||||
function Node:get_tab_rect(idx)
|
||||
local sbw = get_scroll_button_width()
|
||||
local maxw = self.size.x - 2 * sbw
|
||||
local x0 = self.position.x + sbw
|
||||
local x1 = x0 + common.clamp(self.tab_width * (idx - 1) - self.tab_shift, 0, maxw)
|
||||
local x2 = x0 + common.clamp(self.tab_width * idx - self.tab_shift, 0, maxw)
|
||||
local h = style.font:get_height() + style.padding.y * 2
|
||||
return x1, self.position.y, x2 - x1, h
|
||||
end
|
||||
|
||||
|
||||
function Node:get_divider_rect()
|
||||
local x, y = self.position.x, self.position.y
|
||||
if self.type == "hsplit" then
|
||||
return x + self.a.size.x, y, style.divider_size, self.size.y
|
||||
elseif self.type == "vsplit" then
|
||||
return x, y + self.a.size.y, self.size.x, style.divider_size
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Return two values for x and y axis and each of them is either falsy or a number.
|
||||
-- A falsy value indicate no fixed size along the corresponding direction.
|
||||
function Node:get_locked_size()
|
||||
if self.type == "leaf" then
|
||||
if self.locked then
|
||||
local size = self.active_view.size
|
||||
-- The values below should be either a falsy value or a number
|
||||
local sx = (self.locked and self.locked.x) and size.x
|
||||
local sy = (self.locked and self.locked.y) and size.y
|
||||
return sx, sy
|
||||
end
|
||||
else
|
||||
local x1, y1 = self.a:get_locked_size()
|
||||
local x2, y2 = self.b:get_locked_size()
|
||||
-- The values below should be either a falsy value or a number
|
||||
local sx, sy
|
||||
if self.type == 'hsplit' then
|
||||
if x1 and x2 then
|
||||
local dsx = (x1 < 1 or x2 < 1) and 0 or style.divider_size
|
||||
sx = x1 + x2 + dsx
|
||||
end
|
||||
sy = y1 or y2
|
||||
else
|
||||
if y1 and y2 then
|
||||
local dsy = (y1 < 1 or y2 < 1) and 0 or style.divider_size
|
||||
sy = y1 + y2 + dsy
|
||||
end
|
||||
sx = x1 or x2
|
||||
end
|
||||
return sx, sy
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function copy_position_and_size(dst, src)
|
||||
dst.position.x, dst.position.y = src.position.x, src.position.y
|
||||
dst.size.x, dst.size.y = src.size.x, src.size.y
|
||||
end
|
||||
|
||||
|
||||
-- calculating the sizes is the same for hsplits and vsplits, except the x/y
|
||||
-- axis are swapped; this function lets us use the same code for both
|
||||
local function calc_split_sizes(self, x, y, x1, x2, y1, y2)
|
||||
local ds = ((x1 and x1 < 1) or (x2 and x2 < 1)) and 0 or style.divider_size
|
||||
local n = x1 and x1 + ds or (x2 and self.size[x] - x2 or math.floor(self.size[x] * self.divider))
|
||||
self.a.position[x] = self.position[x]
|
||||
self.a.position[y] = self.position[y]
|
||||
self.a.size[x] = n - ds
|
||||
self.a.size[y] = self.size[y]
|
||||
self.b.position[x] = self.position[x] + n
|
||||
self.b.position[y] = self.position[y]
|
||||
self.b.size[x] = self.size[x] - n
|
||||
self.b.size[y] = self.size[y]
|
||||
end
|
||||
|
||||
|
||||
function Node:update_layout()
|
||||
if self.type == "leaf" then
|
||||
local av = self.active_view
|
||||
if self:should_show_tabs() then
|
||||
local _, _, _, th = self:get_tab_rect(1)
|
||||
av.position.x, av.position.y = self.position.x, self.position.y + th
|
||||
av.size.x, av.size.y = self.size.x, self.size.y - th
|
||||
else
|
||||
copy_position_and_size(av, self)
|
||||
end
|
||||
else
|
||||
local x1, y1 = self.a:get_locked_size()
|
||||
local x2, y2 = self.b:get_locked_size()
|
||||
if self.type == "hsplit" then
|
||||
calc_split_sizes(self, "x", "y", x1, x2)
|
||||
elseif self.type == "vsplit" then
|
||||
calc_split_sizes(self, "y", "x", y1, y2)
|
||||
end
|
||||
self.a:update_layout()
|
||||
self.b:update_layout()
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function Node:scroll_tabs_to_visible()
|
||||
local index = self:get_view_idx(self.active_view)
|
||||
if index then
|
||||
local tabs_number = self:get_visible_tabs_number()
|
||||
if self.tab_offset > index then
|
||||
self.tab_offset = index
|
||||
elseif self.tab_offset + tabs_number - 1 < index then
|
||||
self.tab_offset = index - tabs_number + 1
|
||||
elseif tabs_number < config.max_tabs and self.tab_offset > 1 then
|
||||
self.tab_offset = #self.views - config.max_tabs + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function Node:scroll_tabs(dir)
|
||||
local view_index = self:get_view_idx(self.active_view)
|
||||
if dir == 1 then
|
||||
if self.tab_offset > 1 then
|
||||
self.tab_offset = self.tab_offset - 1
|
||||
local last_index = self.tab_offset + self:get_visible_tabs_number() - 1
|
||||
if view_index > last_index then
|
||||
self:set_active_view(self.views[last_index])
|
||||
end
|
||||
end
|
||||
elseif dir == 2 then
|
||||
local tabs_number = self:get_visible_tabs_number()
|
||||
if self.tab_offset + tabs_number - 1 < #self.views then
|
||||
self.tab_offset = self.tab_offset + 1
|
||||
local view_index = self:get_view_idx(self.active_view)
|
||||
if view_index < self.tab_offset then
|
||||
self:set_active_view(self.views[self.tab_offset])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function Node:target_tab_width()
|
||||
local n = self:get_visible_tabs_number()
|
||||
local w = self.size.x - get_scroll_button_width() * 2
|
||||
return common.clamp(style.tab_width, w / config.max_tabs, w / n)
|
||||
end
|
||||
|
||||
|
||||
function Node:update()
|
||||
if self.type == "leaf" then
|
||||
self:scroll_tabs_to_visible()
|
||||
for _, view in ipairs(self.views) do
|
||||
view:update()
|
||||
end
|
||||
self:tab_hovered_update(self.hovered.x, self.hovered.y)
|
||||
local tab_width = self:target_tab_width()
|
||||
self:move_towards("tab_shift", tab_width * (self.tab_offset - 1))
|
||||
self:move_towards("tab_width", tab_width)
|
||||
else
|
||||
self.a:update()
|
||||
self.b:update()
|
||||
end
|
||||
end
|
||||
|
||||
function Node:draw_tab(text, is_active, is_hovered, is_close_hovered, x, y, w, h, standalone)
|
||||
local ds = style.divider_size
|
||||
local dots_width = style.font:get_width("…")
|
||||
local color = style.dim
|
||||
local padding_y = style.padding.y
|
||||
renderer.draw_rect(x + w, y + padding_y, ds, h - padding_y * 2, style.dim)
|
||||
if standalone then
|
||||
renderer.draw_rect(x-1, y-1, w+2, h+2, style.background2)
|
||||
end
|
||||
if is_active then
|
||||
color = style.text
|
||||
renderer.draw_rect(x, y, w, h, style.background)
|
||||
renderer.draw_rect(x + w, y, ds, h, style.divider)
|
||||
renderer.draw_rect(x - ds, y, ds, h, style.divider)
|
||||
end
|
||||
local cx, cw, cspace = close_button_location(x, w)
|
||||
local show_close_button = ((is_active or is_hovered) and not standalone and config.tab_close_button)
|
||||
if show_close_button then
|
||||
local close_style = is_close_hovered and style.text or style.dim
|
||||
common.draw_text(style.icon_font, close_style, "C", nil, cx, y, 0, h)
|
||||
end
|
||||
if is_hovered then
|
||||
color = style.text
|
||||
end
|
||||
local padx = style.padding.x
|
||||
-- Normally we should substract "cspace" from text_avail_width and from the
|
||||
-- clipping width. It is the padding space we give to the left and right of the
|
||||
-- close button. However, since we are using dots to terminate filenames, we
|
||||
-- choose to ignore "cspace" accepting that the text can possibly "touch" the
|
||||
-- close button.
|
||||
local text_avail_width = cx - x - padx
|
||||
core.push_clip_rect(x, y, cx - x, h)
|
||||
x, w = x + padx, w - padx * 2
|
||||
local align = "center"
|
||||
if style.font:get_width(text) > text_avail_width then
|
||||
align = "left"
|
||||
for i = 1, #text do
|
||||
local reduced_text = text:sub(1, #text - i)
|
||||
if style.font:get_width(reduced_text) + dots_width <= text_avail_width then
|
||||
text = reduced_text .. "…"
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
common.draw_text(style.font, color, text, align, x, y, w, h)
|
||||
core.pop_clip_rect()
|
||||
end
|
||||
|
||||
function Node:draw_tabs()
|
||||
local x, y, w, h, scroll_padding = self:get_scroll_button_rect(1)
|
||||
local ds = style.divider_size
|
||||
local dots_width = style.font:get_width("…")
|
||||
core.push_clip_rect(x, y, self.size.x, h)
|
||||
renderer.draw_rect(x, y, self.size.x, h, style.background2)
|
||||
renderer.draw_rect(x, y + h - ds, self.size.x, ds, style.divider)
|
||||
|
||||
if self.tab_offset > 1 then
|
||||
local button_style = self.hovered_scroll_button == 1 and style.text or style.dim
|
||||
common.draw_text(style.icon_font, button_style, "<", nil, x + scroll_padding, y, 0, h)
|
||||
end
|
||||
|
||||
local tabs_number = self:get_visible_tabs_number()
|
||||
if #self.views > self.tab_offset + tabs_number - 1 then
|
||||
local xrb, yrb, wrb = self:get_scroll_button_rect(2)
|
||||
local button_style = self.hovered_scroll_button == 2 and style.text or style.dim
|
||||
common.draw_text(style.icon_font, button_style, ">", nil, xrb + scroll_padding, yrb, 0, h)
|
||||
end
|
||||
|
||||
for i = self.tab_offset, self.tab_offset + tabs_number - 1 do
|
||||
local view = self.views[i]
|
||||
local x, y, w, h = self:get_tab_rect(i)
|
||||
self:draw_tab(view:get_name(), view == self.active_view,
|
||||
i == self.hovered_tab, i == self.hovered_close,
|
||||
x, y, w, h)
|
||||
end
|
||||
|
||||
core.pop_clip_rect()
|
||||
end
|
||||
|
||||
|
||||
function Node:draw()
|
||||
if self.type == "leaf" then
|
||||
if self:should_show_tabs() then
|
||||
self:draw_tabs()
|
||||
end
|
||||
local pos, size = self.active_view.position, self.active_view.size
|
||||
core.push_clip_rect(pos.x, pos.y, size.x, size.y)
|
||||
self.active_view:draw()
|
||||
core.pop_clip_rect()
|
||||
else
|
||||
local x, y, w, h = self:get_divider_rect()
|
||||
renderer.draw_rect(x, y, w, h, style.divider)
|
||||
self:propagate("draw")
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function Node:is_empty()
|
||||
if self.type == "leaf" then
|
||||
return #self.views == 0 or (#self.views == 1 and self.views[1]:is(EmptyView))
|
||||
else
|
||||
return self.a:is_empty() and self.b:is_empty()
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function Node:close_all_docviews(keep_active)
|
||||
local node_active_view = self.active_view
|
||||
local lost_active_view = false
|
||||
if self.type == "leaf" then
|
||||
local i = 1
|
||||
while i <= #self.views do
|
||||
local view = self.views[i]
|
||||
if view.context == "session" and (not keep_active or view ~= self.active_view) then
|
||||
table.remove(self.views, i)
|
||||
if view == node_active_view then
|
||||
lost_active_view = true
|
||||
end
|
||||
else
|
||||
i = i + 1
|
||||
end
|
||||
end
|
||||
self.tab_offset = 1
|
||||
if #self.views == 0 and self.is_primary_node then
|
||||
-- if we are not the primary view and we had the active view it doesn't
|
||||
-- matter to reattribute the active view because, within the close_all_docviews
|
||||
-- top call, the primary node will take the active view anyway.
|
||||
-- Set the empty view and takes the active view.
|
||||
self:add_view(EmptyView())
|
||||
elseif #self.views > 0 and lost_active_view then
|
||||
-- In practice we never get there but if a view remain we need
|
||||
-- to reset the Node's active view.
|
||||
self:set_active_view(self.views[1])
|
||||
end
|
||||
else
|
||||
self.a:close_all_docviews(keep_active)
|
||||
self.b:close_all_docviews(keep_active)
|
||||
if self.a:is_empty() and not self.a.is_primary_node then
|
||||
self:consume(self.b)
|
||||
elseif self.b:is_empty() and not self.b.is_primary_node then
|
||||
self:consume(self.a)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Returns true for nodes that accept either "proportional" resizes (based on the
|
||||
-- node.divider) or "locked" resizable nodes (along the resize axis).
|
||||
function Node:is_resizable(axis)
|
||||
if self.type == 'leaf' then
|
||||
return not self.locked or not self.locked[axis] or self.resizable
|
||||
else
|
||||
local a_resizable = self.a:is_resizable(axis)
|
||||
local b_resizable = self.b:is_resizable(axis)
|
||||
return a_resizable and b_resizable
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Return true iff it is a locked pane along the rezise axis and is
|
||||
-- declared "resizable".
|
||||
function Node:is_locked_resizable(axis)
|
||||
return self.locked and self.locked[axis] and self.resizable
|
||||
end
|
||||
|
||||
|
||||
function Node:resize(axis, value)
|
||||
-- the application works fine with non-integer values but to have pixel-perfect
|
||||
-- placements of view elements, like the scrollbar, we round the value to be
|
||||
-- an integer.
|
||||
value = math.floor(value)
|
||||
if self.type == 'leaf' then
|
||||
-- If it is not locked we don't accept the
|
||||
-- resize operation here because for proportional panes the resize is
|
||||
-- done using the "divider" value of the parent node.
|
||||
if self:is_locked_resizable(axis) then
|
||||
return self.active_view:set_target_size(axis, value)
|
||||
end
|
||||
else
|
||||
if self.type == (axis == "x" and "hsplit" or "vsplit") then
|
||||
-- we are resizing a node that is splitted along the resize axis
|
||||
if self.a:is_locked_resizable(axis) and self.b:is_locked_resizable(axis) then
|
||||
local rem_value = value - self.a.size[axis]
|
||||
if rem_value >= 0 then
|
||||
return self.b.active_view:set_target_size(axis, rem_value)
|
||||
else
|
||||
self.b.active_view:set_target_size(axis, 0)
|
||||
return self.a.active_view:set_target_size(axis, value)
|
||||
end
|
||||
end
|
||||
else
|
||||
-- we are resizing a node that is splitted along the axis perpendicular
|
||||
-- to the resize axis
|
||||
local a_resizable = self.a:is_resizable(axis)
|
||||
local b_resizable = self.b:is_resizable(axis)
|
||||
if a_resizable and b_resizable then
|
||||
self.a:resize(axis, value)
|
||||
self.b:resize(axis, value)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function Node:get_split_type(mouse_x, mouse_y)
|
||||
local x, y = self.position.x, self.position.y
|
||||
local w, h = self.size.x, self.size.y
|
||||
local _, _, _, tab_h = self:get_scroll_button_rect(1)
|
||||
y = y + tab_h
|
||||
h = h - tab_h
|
||||
|
||||
local local_mouse_x = mouse_x - x
|
||||
local local_mouse_y = mouse_y - y
|
||||
|
||||
if local_mouse_y < 0 then
|
||||
return "tab"
|
||||
else
|
||||
local left_pct = local_mouse_x * 100 / w
|
||||
local top_pct = local_mouse_y * 100 / h
|
||||
if left_pct <= 30 then
|
||||
return "left"
|
||||
elseif left_pct >= 70 then
|
||||
return "right"
|
||||
elseif top_pct <= 30 then
|
||||
return "up"
|
||||
elseif top_pct >= 70 then
|
||||
return "down"
|
||||
end
|
||||
return "middle"
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function Node:get_drag_overlay_tab_position(x, y, dragged_node, dragged_index)
|
||||
local tab_index = self:get_tab_overlapping_point(x, y)
|
||||
if not tab_index then
|
||||
local first_tab_x = self:get_tab_rect(1)
|
||||
if x < first_tab_x then
|
||||
-- mouse before first visible tab
|
||||
tab_index = self.tab_offset or 1
|
||||
else
|
||||
-- mouse after last visible tab
|
||||
tab_index = self:get_visible_tabs_number() + (self.tab_offset - 1 or 0)
|
||||
end
|
||||
end
|
||||
local tab_x, tab_y, tab_w, tab_h = self:get_tab_rect(tab_index)
|
||||
if x > tab_x + tab_w / 2 and tab_index <= #self.views then
|
||||
-- use next tab
|
||||
tab_x = tab_x + tab_w
|
||||
tab_index = tab_index + 1
|
||||
end
|
||||
if self == dragged_node and dragged_index and tab_index > dragged_index then
|
||||
-- the tab we are moving is counted in tab_index
|
||||
tab_index = tab_index - 1
|
||||
tab_x = tab_x - tab_w
|
||||
end
|
||||
return tab_index, tab_x, tab_y, tab_w, tab_h
|
||||
end
|
||||
|
||||
|
||||
local RootView = View:extend()
|
||||
|
||||
function RootView:new()
|
||||
|
@ -1068,7 +299,7 @@ end
|
|||
|
||||
|
||||
function RootView:update()
|
||||
copy_position_and_size(self.root_node, self)
|
||||
Node.copy_position_and_size(self.root_node, self)
|
||||
self.root_node:update()
|
||||
self.root_node:update_layout()
|
||||
|
||||
|
|
|
@ -30,6 +30,7 @@ function StatusView:on_mouse_pressed()
|
|||
and not core.active_view:is(LogView) then
|
||||
command.perform "core:open-log"
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
|
||||
|
|
|
@ -237,8 +237,13 @@ function tokenizer.tokenize(incoming_syntax, text, state)
|
|||
|
||||
-- consume character if we didn't match
|
||||
if not matched then
|
||||
push_token(res, "normal", text:sub(i, i))
|
||||
i = i + 1
|
||||
local n = 0
|
||||
-- reach the next character
|
||||
while text:byte(i + n + 1) and common.is_utf8_cont(text, i + n + 1) do
|
||||
n = n + 1
|
||||
end
|
||||
push_token(res, "normal", text:sub(i, i + n))
|
||||
i = i + n + 1
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -43,6 +43,9 @@ function TreeView:new()
|
|||
self.cache = {}
|
||||
self.tooltip = { x = 0, y = 0, begin = 0, alpha = 0 }
|
||||
|
||||
self.item_icon_width = 0
|
||||
self.item_text_spacing = 0
|
||||
|
||||
local on_dirmonitor_modify = core.on_dirmonitor_modify
|
||||
function core.on_dirmonitor_modify(dir, filepath)
|
||||
if self.cache[dir.name] then
|
||||
|
@ -216,11 +219,11 @@ end
|
|||
function TreeView:on_mouse_pressed(button, x, y, clicks)
|
||||
local caught = TreeView.super.on_mouse_pressed(self, button, x, y, clicks)
|
||||
if caught or button ~= "left" then
|
||||
return
|
||||
return true
|
||||
end
|
||||
local hovered_item = self.hovered_item
|
||||
if not hovered_item then
|
||||
return
|
||||
return false
|
||||
elseif hovered_item.type == "dir" then
|
||||
if keymap.modkeys["ctrl"] and button == "left" then
|
||||
create_directory_in(hovered_item)
|
||||
|
@ -240,6 +243,7 @@ function TreeView:on_mouse_pressed(button, x, y, clicks)
|
|||
core.root_view:open_doc(core.open_doc(doc_filename))
|
||||
end)
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
|
||||
|
@ -260,6 +264,9 @@ function TreeView:update()
|
|||
self.tooltip.alpha = 0
|
||||
end
|
||||
|
||||
self.item_icon_width = style.icon_font:get_width("D")
|
||||
self.item_text_spacing = style.icon_font:get_width("f") / 2
|
||||
|
||||
TreeView.super.update(self)
|
||||
end
|
||||
|
||||
|
@ -288,56 +295,90 @@ function TreeView:draw_tooltip()
|
|||
end
|
||||
|
||||
|
||||
function TreeView:color_for_item(abs_filename)
|
||||
-- other plugins can override this to customize the color of each icon
|
||||
return nil
|
||||
function TreeView:get_item_icon(item, active, hovered)
|
||||
local character = "f"
|
||||
if item.type == "dir" then
|
||||
character = item.expanded and "D" or "d"
|
||||
end
|
||||
local font = style.icon_font
|
||||
local color = style.text
|
||||
if active or hovered then
|
||||
color = style.accent
|
||||
end
|
||||
return character, font, color
|
||||
end
|
||||
|
||||
function TreeView:get_item_text(item, active, hovered)
|
||||
local text = item.name
|
||||
local font = style.font
|
||||
local color = style.text
|
||||
if active or hovered then
|
||||
color = style.accent
|
||||
end
|
||||
return text, font, color
|
||||
end
|
||||
|
||||
|
||||
function TreeView:draw_item_text(item, active, hovered, x, y, w, h)
|
||||
local item_text, item_font, item_color = self:get_item_text(item, active, hovered)
|
||||
common.draw_text(item_font, item_color, item_text, nil, x, y, 0, h)
|
||||
end
|
||||
|
||||
|
||||
function TreeView:draw_item_icon(item, active, hovered, x, y, w, h)
|
||||
local icon_char, icon_font, icon_color = self:get_item_icon(item, active, hovered)
|
||||
common.draw_text(icon_font, icon_color, icon_char, nil, x, y, 0, h)
|
||||
return self.item_icon_width + self.item_text_spacing
|
||||
end
|
||||
|
||||
|
||||
function TreeView:draw_item_body(item, active, hovered, x, y, w, h)
|
||||
x = x + self:draw_item_icon(item, active, hovered, x, y, w, h)
|
||||
self:draw_item_text(item, active, hovered, x, y, w, h)
|
||||
end
|
||||
|
||||
|
||||
function TreeView:draw_item_chevron(item, active, hovered, x, y, w, h)
|
||||
if item.type == "dir" then
|
||||
local chevron_icon = item.expanded and "-" or "+"
|
||||
local chevron_color = hovered and style.accent or style.text
|
||||
common.draw_text(style.icon_font, chevron_color, chevron_icon, nil, x, y, 0, h)
|
||||
end
|
||||
return style.padding.x
|
||||
end
|
||||
|
||||
|
||||
function TreeView:draw_item_background(item, active, hovered, x, y, w, h)
|
||||
if hovered then
|
||||
renderer.draw_rect(x, y, w, h, style.line_highlight)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function TreeView:draw_item(item, active, hovered, x, y, w, h)
|
||||
self:draw_item_background(item, active, hovered, x, y, w, h)
|
||||
|
||||
x = x + item.depth * style.padding.x + style.padding.x
|
||||
x = x + self:draw_item_chevron(item, active, hovered, x, y, w, h)
|
||||
|
||||
self:draw_item_body(item, active, hovered, x, y, w, h)
|
||||
end
|
||||
|
||||
|
||||
function TreeView:draw()
|
||||
self:draw_background(style.background2)
|
||||
|
||||
local icon_width = style.icon_font:get_width("D")
|
||||
local spacing = style.icon_font:get_width("f") / 2
|
||||
local _y, _h = self.position.y, self.size.y
|
||||
|
||||
local doc = core.active_view.doc
|
||||
local active_filename = doc and system.absolute_path(doc.filename or "")
|
||||
|
||||
for item, x,y,w,h in self:each_item() do
|
||||
local color = style.text
|
||||
|
||||
-- highlight active_view doc
|
||||
if item.abs_filename == active_filename then
|
||||
color = style.accent
|
||||
if y + h >= _y and y < _y + _h then
|
||||
self:draw_item(item,
|
||||
item.abs_filename == active_filename,
|
||||
item == self.hovered_item,
|
||||
x, y, w, h)
|
||||
end
|
||||
|
||||
-- hovered item background
|
||||
if item == self.hovered_item then
|
||||
renderer.draw_rect(x, y, w, h, style.line_highlight)
|
||||
color = style.accent
|
||||
end
|
||||
|
||||
-- allow for color overrides
|
||||
local icon_color = self:color_for_item(item.abs_filename) or color
|
||||
|
||||
-- icons
|
||||
x = x + item.depth * style.padding.x + style.padding.x
|
||||
if item.type == "dir" then
|
||||
local icon1 = item.expanded and "-" or "+"
|
||||
local icon2 = item.expanded and "D" or "d"
|
||||
common.draw_text(style.icon_font, color, icon1, nil, x, y, 0, h)
|
||||
x = x + style.padding.x
|
||||
common.draw_text(style.icon_font, icon_color, icon2, nil, x, y, 0, h)
|
||||
x = x + icon_width
|
||||
else
|
||||
x = x + style.padding.x
|
||||
common.draw_text(style.icon_font, icon_color, "f", nil, x, y, 0, h)
|
||||
x = x + icon_width
|
||||
end
|
||||
|
||||
-- text
|
||||
x = x + spacing
|
||||
x = common.draw_text(style.font, color, item.name, nil, x, y, 0, h)
|
||||
end
|
||||
|
||||
self:draw_scrollbar()
|
||||
|
|
16
meson.build
16
meson.build
|
@ -53,21 +53,7 @@ if not get_option('source-only')
|
|||
pcre2_dep = dependency('libpcre2-8')
|
||||
freetype_dep = dependency('freetype2')
|
||||
sdl_dep = dependency('sdl2', method: 'config-tool')
|
||||
reproc_dep = dependency('reproc', fallback: ['reproc', 'reproc_dep'],
|
||||
default_options: [
|
||||
'default_library=static', 'multithreaded=false',
|
||||
'reproc-cpp=false', 'examples=false'
|
||||
]
|
||||
)
|
||||
|
||||
lite_deps = [lua_dep, sdl_dep, reproc_dep, pcre2_dep, libm, libdl, freetype_dep, threads_dep]
|
||||
|
||||
if host_machine.system() == 'windows'
|
||||
# Note that we need to explicitly add the windows socket DLL because
|
||||
# the pkg-config file from reproc does not include it.
|
||||
lite_deps += meson.get_compiler('c').find_library('ws2_32', required: true)
|
||||
lite_deps += meson.get_compiler('c').find_library('dwmapi', required: true)
|
||||
endif
|
||||
lite_deps = [lua_dep, sdl_dep, freetype_dep, pcre2_dep, libm, libdl, threads_dep]
|
||||
endif
|
||||
#===============================================================================
|
||||
# Install Configuration
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
#define API_TYPE_FONT "Font"
|
||||
#define API_TYPE_PROCESS "Process"
|
||||
|
||||
#define API_CONSTANT_DEFINE(L, idx, key, n) (lua_pushnumber(L, n), lua_setfield(L, idx - 1, key))
|
||||
|
||||
void api_load_libs(lua_State *L);
|
||||
|
||||
#ifdef _WIN32
|
||||
|
|
|
@ -1,408 +1,468 @@
|
|||
/**
|
||||
* Basic binding of reproc into Lua.
|
||||
* @copyright Jefferson Gonzalez
|
||||
* @license MIT
|
||||
*/
|
||||
#include "api.h"
|
||||
|
||||
#include <string.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdlib.h>
|
||||
#include <reproc/reproc.h>
|
||||
#include "api.h"
|
||||
#include <SDL.h>
|
||||
|
||||
#if _WIN32
|
||||
// https://stackoverflow.com/questions/60645/overlapped-i-o-on-anonymous-pipe
|
||||
// https://docs.microsoft.com/en-us/windows/win32/procthread/creating-a-child-process-with-redirected-input-and-output
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <errno.h>
|
||||
#include <unistd.h>
|
||||
#include <signal.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
#endif
|
||||
|
||||
#define READ_BUF_SIZE 2048
|
||||
|
||||
#define L_GETTABLE(L, idx, key, conv, def) ( \
|
||||
lua_getfield(L, idx, key), \
|
||||
conv(L, -1, def) \
|
||||
)
|
||||
|
||||
#define L_GETNUM(L, idx, key, def) L_GETTABLE(L, idx, key, luaL_optnumber, def)
|
||||
#define L_GETSTR(L, idx, key, def) L_GETTABLE(L, idx, key, luaL_optstring, def)
|
||||
|
||||
#define L_SETNUM(L, idx, key, n) (lua_pushnumber(L, n), lua_setfield(L, idx - 1, key))
|
||||
|
||||
#define L_RETURN_REPROC_ERROR(L, code) { \
|
||||
lua_pushnil(L); \
|
||||
lua_pushstring(L, reproc_strerror(code)); \
|
||||
lua_pushnumber(L, code); \
|
||||
return 3; \
|
||||
}
|
||||
|
||||
#define ASSERT_MALLOC(ptr) \
|
||||
if (ptr == NULL) \
|
||||
L_RETURN_REPROC_ERROR(L, REPROC_ENOMEM)
|
||||
|
||||
#define ASSERT_REPROC_ERRNO(L, code) { \
|
||||
if (code < 0) \
|
||||
L_RETURN_REPROC_ERROR(L, code) \
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
reproc_t * process;
|
||||
bool running;
|
||||
int returncode;
|
||||
bool running;
|
||||
int returncode, deadline;
|
||||
long pid;
|
||||
#if _WIN32
|
||||
PROCESS_INFORMATION process_information;
|
||||
HANDLE child_pipes[3][2];
|
||||
OVERLAPPED overlapped[2];
|
||||
bool reading[2];
|
||||
char buffer[2][READ_BUF_SIZE];
|
||||
#else
|
||||
int child_pipes[3][2];
|
||||
#endif
|
||||
} process_t;
|
||||
|
||||
// this function should be called instead of reproc_wait
|
||||
static int poll_process(process_t* proc, int timeout)
|
||||
{
|
||||
int ret = reproc_wait(proc->process, timeout);
|
||||
if (ret != REPROC_ETIMEDOUT) {
|
||||
typedef enum {
|
||||
SIGNAL_KILL,
|
||||
SIGNAL_TERM,
|
||||
SIGNAL_INTERRUPT
|
||||
} signal_e;
|
||||
|
||||
typedef enum {
|
||||
WAIT_NONE = 0,
|
||||
WAIT_DEADLINE = -1,
|
||||
WAIT_INFINITE = -2
|
||||
} wait_e;
|
||||
|
||||
typedef enum {
|
||||
STDIN_FD,
|
||||
STDOUT_FD,
|
||||
STDERR_FD,
|
||||
// Special values for redirection.
|
||||
REDIRECT_DEFAULT = -1,
|
||||
REDIRECT_DISCARD = -2,
|
||||
REDIRECT_PARENT = -3,
|
||||
} filed_e;
|
||||
|
||||
#ifdef _WIN32
|
||||
static volatile long PipeSerialNumber;
|
||||
static void close_fd(HANDLE handle) { CloseHandle(handle); }
|
||||
#else
|
||||
static void close_fd(int fd) { close(fd); }
|
||||
#endif
|
||||
|
||||
static bool poll_process(process_t* proc, int timeout) {
|
||||
if (!proc->running)
|
||||
return false;
|
||||
unsigned int ticks = SDL_GetTicks();
|
||||
if (timeout == WAIT_DEADLINE)
|
||||
timeout = proc->deadline;
|
||||
do {
|
||||
#ifdef _WIN32
|
||||
DWORD exit_code = -1;
|
||||
if (!GetExitCodeProcess( proc->process_information.hProcess, &exit_code ) || exit_code != STILL_ACTIVE) {
|
||||
proc->returncode = exit_code;
|
||||
proc->running = false;
|
||||
proc->returncode = ret;
|
||||
}
|
||||
return ret;
|
||||
break;
|
||||
}
|
||||
#else
|
||||
int status;
|
||||
pid_t wait_response = waitpid(proc->pid, &status, WNOHANG);
|
||||
if (wait_response != 0) {
|
||||
proc->running = false;
|
||||
proc->returncode = WEXITSTATUS(status);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
if (timeout)
|
||||
SDL_Delay(5);
|
||||
} while (timeout == WAIT_INFINITE || SDL_GetTicks() - ticks < timeout);
|
||||
if (!proc->running) {
|
||||
close_fd(proc->child_pipes[STDIN_FD ][1]);
|
||||
close_fd(proc->child_pipes[STDOUT_FD][0]);
|
||||
close_fd(proc->child_pipes[STDERR_FD][0]);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static int kill_process(process_t* proc)
|
||||
{
|
||||
int ret = reproc_stop(
|
||||
proc->process,
|
||||
(reproc_stop_actions) {
|
||||
{REPROC_STOP_KILL, 0},
|
||||
{REPROC_STOP_TERMINATE, 0},
|
||||
{REPROC_STOP_NOOP, 0}
|
||||
}
|
||||
);
|
||||
|
||||
if (ret != REPROC_ETIMEDOUT) {
|
||||
proc->running = false;
|
||||
proc->returncode = ret;
|
||||
static bool signal_process(process_t* proc, signal_e sig) {
|
||||
bool terminate = false;
|
||||
#if _WIN32
|
||||
switch(sig) {
|
||||
case SIGNAL_TERM: terminate = GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, GetProcessId(proc->process_information.hProcess)); break;
|
||||
case SIGNAL_KILL: terminate = TerminateProcess(proc->process_information.hProcess, -1); break;
|
||||
case SIGNAL_INTERRUPT: DebugBreakProcess(proc->process_information.hProcess); break;
|
||||
}
|
||||
|
||||
return ret;
|
||||
#else
|
||||
switch (sig) {
|
||||
case SIGNAL_TERM: terminate = kill(proc->pid, SIGTERM) == 1; break;
|
||||
case SIGNAL_KILL: terminate = kill(proc->pid, SIGKILL) == 1; break;
|
||||
case SIGNAL_INTERRUPT: kill(proc->pid, SIGINT); break;
|
||||
}
|
||||
#endif
|
||||
if (terminate)
|
||||
poll_process(proc, WAIT_NONE);
|
||||
return true;
|
||||
}
|
||||
|
||||
static int process_start(lua_State* L)
|
||||
{
|
||||
luaL_checktype(L, 1, LUA_TTABLE);
|
||||
if (lua_isnoneornil(L, 2)) {
|
||||
lua_settop(L, 1); // remove the nil if it's there
|
||||
lua_newtable(L);
|
||||
}
|
||||
luaL_checktype(L, 2, LUA_TTABLE);
|
||||
|
||||
int cmd_len = lua_rawlen(L, 1);
|
||||
const char** cmd = malloc(sizeof(char *) * (cmd_len + 1));
|
||||
ASSERT_MALLOC(cmd);
|
||||
cmd[cmd_len] = NULL;
|
||||
|
||||
for(int i = 0; i < cmd_len; i++) {
|
||||
lua_rawgeti(L, 1, i + 1);
|
||||
|
||||
cmd[i] = luaL_checkstring(L, -1);
|
||||
static int process_start(lua_State* L) {
|
||||
size_t env_len = 0, key_len, val_len;
|
||||
const char *cmd[256], *env[256] = { NULL }, *cwd = NULL;
|
||||
bool detach = false;
|
||||
int deadline = 10, new_fds[3] = { STDIN_FD, STDOUT_FD, STDERR_FD };
|
||||
luaL_checktype(L, 1, LUA_TTABLE);
|
||||
#if LUA_VERSION_NUM > 501
|
||||
lua_len(L, 1);
|
||||
#else
|
||||
lua_pushnumber(L, (int)lua_objlen(L, 1));
|
||||
#endif
|
||||
size_t cmd_len = luaL_checknumber(L, -1); lua_pop(L, 1);
|
||||
size_t arg_len = lua_gettop(L);
|
||||
for (size_t i = 1; i <= cmd_len; ++i) {
|
||||
lua_pushnumber(L, i);
|
||||
lua_rawget(L, 1);
|
||||
cmd[i-1] = luaL_checkstring(L, -1);
|
||||
}
|
||||
cmd[cmd_len] = NULL;
|
||||
if (arg_len > 1) {
|
||||
lua_getfield(L, 2, "env");
|
||||
if (!lua_isnil(L, -1)) {
|
||||
lua_pushnil(L);
|
||||
while (lua_next(L, -2) != 0) {
|
||||
const char* key = luaL_checklstring(L, -2, &key_len);
|
||||
const char* val = luaL_checklstring(L, -1, &val_len);
|
||||
env[env_len] = malloc(key_len+val_len+2);
|
||||
snprintf((char*)env[env_len++], key_len+val_len+2, "%s=%s", key, val);
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
} else
|
||||
lua_pop(L, 1);
|
||||
lua_getfield(L, 2, "detach"); detach = lua_toboolean(L, -1);
|
||||
lua_getfield(L, 2, "timeout"); deadline = luaL_optnumber(L, -1, deadline);
|
||||
lua_getfield(L, 2, "cwd"); cwd = luaL_optstring(L, -1, NULL);
|
||||
lua_getfield(L, 2, "stdin"); new_fds[STDIN_FD] = luaL_optnumber(L, -1, STDIN_FD);
|
||||
lua_getfield(L, 2, "stdout"); new_fds[STDOUT_FD] = luaL_optnumber(L, -1, STDOUT_FD);
|
||||
lua_getfield(L, 2, "stderr"); new_fds[STDERR_FD] = luaL_optnumber(L, -1, STDERR_FD);
|
||||
for (int stream = STDIN_FD; stream <= STDERR_FD; ++stream) {
|
||||
if (new_fds[stream] > STDERR_FD || new_fds[stream] < REDIRECT_PARENT)
|
||||
return luaL_error(L, "redirect to handles, FILE* and paths are not supported");
|
||||
}
|
||||
|
||||
int deadline = L_GETNUM(L, 2, "timeout", 0);
|
||||
const char* cwd =L_GETSTR(L, 2, "cwd", NULL);
|
||||
int redirect_in = L_GETNUM(L, 2, "stdin", REPROC_REDIRECT_DEFAULT);
|
||||
int redirect_out = L_GETNUM(L, 2, "stdout", REPROC_REDIRECT_DEFAULT);
|
||||
int redirect_err = L_GETNUM(L, 2, "stderr", REPROC_REDIRECT_DEFAULT);
|
||||
lua_pop(L, 5); // remove args we just read
|
||||
|
||||
if (
|
||||
redirect_in > REPROC_REDIRECT_STDOUT
|
||||
|| redirect_out > REPROC_REDIRECT_STDOUT
|
||||
|| redirect_err > REPROC_REDIRECT_STDOUT)
|
||||
{
|
||||
lua_pushnil(L);
|
||||
lua_pushliteral(L, "redirect to handles, FILE* and paths are not supported");
|
||||
return 2;
|
||||
}
|
||||
|
||||
// env
|
||||
luaL_getsubtable(L, 2, "env");
|
||||
const char **env = NULL;
|
||||
int env_len = 0;
|
||||
|
||||
lua_pushnil(L);
|
||||
while (lua_next(L, -2) != 0) {
|
||||
env_len++;
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
|
||||
if (env_len > 0) {
|
||||
env = malloc(sizeof(char*) * (env_len + 1));
|
||||
env[env_len] = NULL;
|
||||
|
||||
int i = 0;
|
||||
lua_pushnil(L);
|
||||
while (lua_next(L, -2) != 0) {
|
||||
lua_pushliteral(L, "=");
|
||||
lua_pushvalue(L, -3); // push the key to the top
|
||||
lua_concat(L, 3); // key=value
|
||||
|
||||
env[i++] = luaL_checkstring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
}
|
||||
|
||||
reproc_t* proc = reproc_new();
|
||||
int out = reproc_start(
|
||||
proc,
|
||||
(const char* const*) cmd,
|
||||
(reproc_options) {
|
||||
.working_directory = cwd,
|
||||
.deadline = deadline,
|
||||
.nonblocking = true,
|
||||
.env = {
|
||||
.behavior = REPROC_ENV_EXTEND,
|
||||
.extra = env
|
||||
},
|
||||
.redirect = {
|
||||
.in.type = redirect_in,
|
||||
.out.type = redirect_out,
|
||||
.err.type = redirect_err
|
||||
}
|
||||
env[env_len] = NULL;
|
||||
|
||||
process_t* self = lua_newuserdata(L, sizeof(process_t));
|
||||
memset(self, 0, sizeof(process_t));
|
||||
luaL_setmetatable(L, API_TYPE_PROCESS);
|
||||
self->deadline = deadline;
|
||||
#if _WIN32
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
switch (new_fds[i]) {
|
||||
case REDIRECT_PARENT:
|
||||
switch (i) {
|
||||
case STDIN_FD: self->child_pipes[i][0] = GetStdHandle(STD_INPUT_HANDLE); break;
|
||||
case STDOUT_FD: self->child_pipes[i][1] = GetStdHandle(STD_OUTPUT_HANDLE); break;
|
||||
case STDERR_FD: self->child_pipes[i][1] = GetStdHandle(STD_ERROR_HANDLE); break;
|
||||
}
|
||||
self->child_pipes[i][i == STDIN_FD ? 1 : 0] = INVALID_HANDLE_VALUE;
|
||||
break;
|
||||
case REDIRECT_DISCARD:
|
||||
self->child_pipes[i][0] = INVALID_HANDLE_VALUE;
|
||||
self->child_pipes[i][1] = INVALID_HANDLE_VALUE;
|
||||
break;
|
||||
default: {
|
||||
if (new_fds[i] == i) {
|
||||
char pipeNameBuffer[MAX_PATH];
|
||||
sprintf(pipeNameBuffer, "\\\\.\\Pipe\\RemoteExeAnon.%08lx.%08lx", GetCurrentProcessId(), InterlockedIncrement(&PipeSerialNumber));
|
||||
self->child_pipes[i][0] = CreateNamedPipeA(pipeNameBuffer, PIPE_ACCESS_INBOUND | FILE_FLAG_OVERLAPPED,
|
||||
PIPE_TYPE_BYTE | PIPE_WAIT, 1, READ_BUF_SIZE, READ_BUF_SIZE, 0, NULL);
|
||||
if (self->child_pipes[i][0] == INVALID_HANDLE_VALUE)
|
||||
return luaL_error(L, "Error creating read pipe: %d.", GetLastError());
|
||||
self->child_pipes[i][1] = CreateFileA(pipeNameBuffer, GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
|
||||
if (self->child_pipes[i][1] == INVALID_HANDLE_VALUE) {
|
||||
CloseHandle(self->child_pipes[i][0]);
|
||||
return luaL_error(L, "Error creating write pipe: %d.", GetLastError());
|
||||
}
|
||||
if (!SetHandleInformation(self->child_pipes[i][i == STDIN_FD ? 1 : 0], HANDLE_FLAG_INHERIT, 0) ||
|
||||
!SetHandleInformation(self->child_pipes[i][i == STDIN_FD ? 0 : 1], HANDLE_FLAG_INHERIT, 1))
|
||||
return luaL_error(L, "Error inheriting pipes: %d.", GetLastError());
|
||||
}
|
||||
} break;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
if (new_fds[i] != i) {
|
||||
self->child_pipes[i][0] = self->child_pipes[new_fds[i]][0];
|
||||
self->child_pipes[i][1] = self->child_pipes[new_fds[i]][1];
|
||||
}
|
||||
}
|
||||
STARTUPINFO siStartInfo;
|
||||
memset(&self->process_information, 0, sizeof(self->process_information));
|
||||
memset(&siStartInfo, 0, sizeof(siStartInfo));
|
||||
siStartInfo.cb = sizeof(siStartInfo);
|
||||
siStartInfo.dwFlags |= STARTF_USESTDHANDLES;
|
||||
siStartInfo.hStdInput = self->child_pipes[STDIN_FD][0];
|
||||
siStartInfo.hStdOutput = self->child_pipes[STDOUT_FD][1];
|
||||
siStartInfo.hStdError = self->child_pipes[STDERR_FD][1];
|
||||
char commandLine[32767] = { 0 }, environmentBlock[32767];
|
||||
int offset = 0;
|
||||
strcpy(commandLine, cmd[0]);
|
||||
for (size_t i = 1; i < cmd_len; ++i) {
|
||||
size_t len = strlen(cmd[i]);
|
||||
if (offset + len + 1 >= sizeof(commandLine))
|
||||
break;
|
||||
strcat(commandLine, " ");
|
||||
strcat(commandLine, cmd[i]);
|
||||
}
|
||||
for (size_t i = 0; i < env_len; ++i) {
|
||||
size_t len = strlen(env[i]);
|
||||
if (offset + len >= sizeof(environmentBlock))
|
||||
break;
|
||||
memcpy(&environmentBlock[offset], env[i], len);
|
||||
offset += len;
|
||||
environmentBlock[offset++] = 0;
|
||||
}
|
||||
environmentBlock[offset++] = 0;
|
||||
if (!CreateProcess(NULL, commandLine, NULL, NULL, true, (detach ? DETACHED_PROCESS : CREATE_NO_WINDOW) | CREATE_UNICODE_ENVIRONMENT, env_len > 0 ? environmentBlock : NULL, cwd, &siStartInfo, &self->process_information))
|
||||
return luaL_error(L, "Error creating a process: %d.", GetLastError());
|
||||
self->pid = (long)self->process_information.dwProcessId;
|
||||
if (detach)
|
||||
CloseHandle(self->process_information.hProcess);
|
||||
CloseHandle(self->process_information.hThread);
|
||||
#else
|
||||
for (int i = 0; i < 3; ++i) { // Make only the parents fd's non-blocking. Children should block.
|
||||
if (pipe(self->child_pipes[i]) || fcntl(self->child_pipes[i][i == STDIN_FD ? 1 : 0], F_SETFL, O_NONBLOCK) == -1)
|
||||
return luaL_error(L, "Error creating pipes: %s", strerror(errno));
|
||||
}
|
||||
self->pid = (long)fork();
|
||||
if (self->pid < 0) {
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
close(self->child_pipes[i][0]);
|
||||
close(self->child_pipes[i][1]);
|
||||
}
|
||||
return luaL_error(L, "Error running fork: %s.", strerror(errno));
|
||||
} else if (!self->pid) {
|
||||
for (int stream = 0; stream < 3; ++stream) {
|
||||
if (new_fds[stream] == REDIRECT_DISCARD) { // Close the stream if we don't want it.
|
||||
close(self->child_pipes[stream][stream == STDIN_FD ? 0 : 1]);
|
||||
close(stream);
|
||||
} else if (new_fds[stream] != REDIRECT_PARENT) // Use the parent handles if we redirect to parent.
|
||||
dup2(self->child_pipes[new_fds[stream]][new_fds[stream] == STDIN_FD ? 0 : 1], stream);
|
||||
close(self->child_pipes[stream][stream == STDIN_FD ? 1 : 0]);
|
||||
}
|
||||
if ((!detach || setsid() != -1) && (!cwd || chdir(cwd) != -1))
|
||||
execvp((const char*)cmd[0], (char* const*)cmd);
|
||||
const char* msg = strerror(errno);
|
||||
int result = write(STDERR_FD, msg, strlen(msg)+1);
|
||||
exit(result == strlen(msg)+1 ? -1 : -2);
|
||||
}
|
||||
#endif
|
||||
for (size_t i = 0; i < env_len; ++i)
|
||||
free((char*)env[i]);
|
||||
for (int stream = 0; stream < 3; ++stream)
|
||||
close_fd(self->child_pipes[stream][stream == STDIN_FD ? 0 : 1]);
|
||||
self->running = true;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int g_read(lua_State* L, int stream, unsigned long read_size) {
|
||||
process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS);
|
||||
long length = 0;
|
||||
if (stream != STDOUT_FD && stream != STDERR_FD)
|
||||
return luaL_error(L, "redirect to handles, FILE* and paths are not supported");
|
||||
#if _WIN32
|
||||
int writable_stream_idx = stream - 1;
|
||||
if (self->reading[writable_stream_idx] || !ReadFile(self->child_pipes[stream][0], self->buffer[writable_stream_idx], READ_BUF_SIZE, NULL, &self->overlapped[writable_stream_idx])) {
|
||||
if (self->reading[writable_stream_idx] || GetLastError() == ERROR_IO_PENDING) {
|
||||
self->reading[writable_stream_idx] = true;
|
||||
DWORD bytesTransferred = 0;
|
||||
if (GetOverlappedResult(self->child_pipes[stream][0], &self->overlapped[writable_stream_idx], &bytesTransferred, false)) {
|
||||
self->reading[writable_stream_idx] = false;
|
||||
length = bytesTransferred;
|
||||
memset(&self->overlapped[writable_stream_idx], 0, sizeof(self->overlapped[writable_stream_idx]));
|
||||
}
|
||||
);
|
||||
|
||||
if (out < 0) {
|
||||
reproc_destroy(proc);
|
||||
L_RETURN_REPROC_ERROR(L, out);
|
||||
} else {
|
||||
signal_process(self, SIGNAL_TERM);
|
||||
return 0;
|
||||
}
|
||||
} else {
|
||||
length = self->overlapped[writable_stream_idx].InternalHigh;
|
||||
memset(&self->overlapped[writable_stream_idx], 0, sizeof(self->overlapped[writable_stream_idx]));
|
||||
}
|
||||
|
||||
process_t* self = lua_newuserdata(L, sizeof(process_t));
|
||||
self->process = proc;
|
||||
self->running = true;
|
||||
|
||||
// this is equivalent to using lua_setmetatable()
|
||||
luaL_setmetatable(L, API_TYPE_PROCESS);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int process_strerror(lua_State* L)
|
||||
{
|
||||
int error_code = luaL_checknumber(L, 1);
|
||||
|
||||
if (error_code < 0)
|
||||
lua_pushstring(L, reproc_strerror(error_code));
|
||||
else
|
||||
lua_pushnil(L);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int f_gc(lua_State* L)
|
||||
{
|
||||
process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS);
|
||||
|
||||
if(self->process) {
|
||||
kill_process(self);
|
||||
reproc_destroy(self->process);
|
||||
self->process = NULL;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int f_tostring(lua_State* L)
|
||||
{
|
||||
luaL_checkudata(L, 1, API_TYPE_PROCESS);
|
||||
|
||||
lua_pushliteral(L, API_TYPE_PROCESS);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int f_pid(lua_State* L)
|
||||
{
|
||||
process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS);
|
||||
|
||||
lua_pushnumber(L, reproc_pid(self->process));
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int f_returncode(lua_State *L)
|
||||
{
|
||||
process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS);
|
||||
int ret = poll_process(self, 0);
|
||||
|
||||
if (self->running)
|
||||
lua_pushnil(L);
|
||||
else
|
||||
lua_pushnumber(L, ret);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int g_read(lua_State* L, int stream)
|
||||
{
|
||||
process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS);
|
||||
unsigned long read_size = luaL_optunsigned(L, 2, READ_BUF_SIZE);
|
||||
|
||||
lua_pushlstring(L, self->buffer[writable_stream_idx], length);
|
||||
#else
|
||||
luaL_Buffer b;
|
||||
uint8_t* buffer = (uint8_t*) luaL_buffinitsize(L, &b, read_size);
|
||||
|
||||
int out = reproc_read(
|
||||
self->process,
|
||||
stream,
|
||||
buffer,
|
||||
read_size
|
||||
);
|
||||
|
||||
if (out >= 0)
|
||||
luaL_addsize(&b, out);
|
||||
luaL_buffinit(L, &b);
|
||||
uint8_t* buffer = (uint8_t*)luaL_prepbuffer(&b);
|
||||
length = read(self->child_pipes[stream][0], buffer, read_size > READ_BUF_SIZE ? READ_BUF_SIZE : read_size);
|
||||
if (length == 0 && !poll_process(self, WAIT_NONE))
|
||||
return 0;
|
||||
else if (length < 0 && (errno == EAGAIN || errno == EWOULDBLOCK))
|
||||
length = 0;
|
||||
if (length < 0) {
|
||||
signal_process(self, SIGNAL_TERM);
|
||||
return 0;
|
||||
}
|
||||
luaL_addsize(&b, length);
|
||||
luaL_pushresult(&b);
|
||||
#endif
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (out == REPROC_EPIPE) {
|
||||
kill_process(self);
|
||||
ASSERT_REPROC_ERRNO(L, out);
|
||||
static int f_write(lua_State* L) {
|
||||
process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS);
|
||||
size_t data_size = 0;
|
||||
const char* data = luaL_checklstring(L, 2, &data_size);
|
||||
long length;
|
||||
#if _WIN32
|
||||
DWORD dwWritten;
|
||||
if (!WriteFile(self->child_pipes[STDIN_FD][1], data, data_size, &dwWritten, NULL)) {
|
||||
signal_process(self, SIGNAL_TERM);
|
||||
return luaL_error(L, "error writing to process: %d", GetLastError());
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int f_read_stdout(lua_State* L)
|
||||
{
|
||||
return g_read(L, REPROC_STREAM_OUT);
|
||||
}
|
||||
|
||||
static int f_read_stderr(lua_State* L)
|
||||
{
|
||||
return g_read(L, REPROC_STREAM_ERR);
|
||||
}
|
||||
|
||||
static int f_read(lua_State* L)
|
||||
{
|
||||
int stream = luaL_checknumber(L, 2);
|
||||
lua_remove(L, 2);
|
||||
if (stream > REPROC_STREAM_ERR)
|
||||
L_RETURN_REPROC_ERROR(L, REPROC_EINVAL);
|
||||
|
||||
return g_read(L, stream);
|
||||
}
|
||||
|
||||
static int f_write(lua_State* L)
|
||||
{
|
||||
process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS);
|
||||
|
||||
size_t data_size = 0;
|
||||
const char* data = luaL_checklstring(L, 2, &data_size);
|
||||
|
||||
int out = reproc_write(
|
||||
self->process,
|
||||
(uint8_t*) data,
|
||||
data_size
|
||||
);
|
||||
if (out == REPROC_EPIPE) {
|
||||
kill_process(self);
|
||||
L_RETURN_REPROC_ERROR(L, out);
|
||||
length = dwWritten;
|
||||
#else
|
||||
length = write(self->child_pipes[STDIN_FD][1], data, data_size);
|
||||
if (length < 0 && (errno == EAGAIN || errno == EWOULDBLOCK))
|
||||
length = 0;
|
||||
else if (length < 0) {
|
||||
signal_process(self, SIGNAL_TERM);
|
||||
return luaL_error(L, "error writing to process: %s", strerror(errno));
|
||||
}
|
||||
|
||||
lua_pushnumber(L, out);
|
||||
return 1;
|
||||
#endif
|
||||
lua_pushnumber(L, length);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int f_close_stream(lua_State* L)
|
||||
{
|
||||
process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS);
|
||||
|
||||
int stream = luaL_checknumber(L, 2);
|
||||
int out = reproc_close(self->process, stream);
|
||||
ASSERT_REPROC_ERRNO(L, out);
|
||||
|
||||
lua_pushboolean(L, 1);
|
||||
return 1;
|
||||
static int f_close_stream(lua_State* L) {
|
||||
process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS);
|
||||
int stream = luaL_checknumber(L, 2);
|
||||
close_fd(self->child_pipes[stream][stream == STDIN_FD ? 1 : 0]);
|
||||
lua_pushboolean(L, 1);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int f_wait(lua_State* L)
|
||||
{
|
||||
process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS);
|
||||
|
||||
int timeout = luaL_optnumber(L, 2, 0);
|
||||
|
||||
int ret = poll_process(self, timeout);
|
||||
// negative returncode is also used for signals on POSIX
|
||||
if (ret == REPROC_ETIMEDOUT)
|
||||
L_RETURN_REPROC_ERROR(L, ret);
|
||||
|
||||
lua_pushnumber(L, ret);
|
||||
// Generic stuff below here.
|
||||
static int process_strerror(lua_State* L) {
|
||||
#if _WIN32
|
||||
return 1;
|
||||
#endif
|
||||
int error_code = luaL_checknumber(L, 1);
|
||||
if (error_code < 0)
|
||||
lua_pushstring(L, strerror(error_code));
|
||||
else
|
||||
lua_pushnil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int f_terminate(lua_State* L)
|
||||
{
|
||||
process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS);
|
||||
|
||||
int out = reproc_terminate(self->process);
|
||||
ASSERT_REPROC_ERRNO(L, out);
|
||||
|
||||
poll_process(self, 0);
|
||||
lua_pushboolean(L, 1);
|
||||
|
||||
return 1;
|
||||
static int f_tostring(lua_State* L) {
|
||||
lua_pushliteral(L, API_TYPE_PROCESS);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int f_kill(lua_State* L)
|
||||
{
|
||||
process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS);
|
||||
|
||||
int out = reproc_kill(self->process);
|
||||
ASSERT_REPROC_ERRNO(L, out);
|
||||
|
||||
poll_process(self, 0);
|
||||
lua_pushboolean(L, 1);
|
||||
|
||||
return 1;
|
||||
static int f_pid(lua_State* L) {
|
||||
process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS);
|
||||
lua_pushnumber(L, self->pid);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int f_running(lua_State* L)
|
||||
{
|
||||
process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS);
|
||||
|
||||
poll_process(self, 0);
|
||||
lua_pushboolean(L, self->running);
|
||||
static int f_returncode(lua_State *L) {
|
||||
process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS);
|
||||
if (self->running)
|
||||
return 0;
|
||||
lua_pushnumber(L, self->returncode);
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 1;
|
||||
static int f_read_stdout(lua_State* L) {
|
||||
return g_read(L, STDOUT_FD, luaL_optinteger(L, 2, READ_BUF_SIZE));
|
||||
}
|
||||
|
||||
static int f_read_stderr(lua_State* L) {
|
||||
return g_read(L, STDERR_FD, luaL_optinteger(L, 2, READ_BUF_SIZE));
|
||||
}
|
||||
|
||||
static int f_read(lua_State* L) {
|
||||
return g_read(L, luaL_checknumber(L, 2), luaL_optinteger(L, 3, READ_BUF_SIZE));
|
||||
}
|
||||
|
||||
static int f_wait(lua_State* L) {
|
||||
process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS);
|
||||
int timeout = luaL_optnumber(L, 2, 0);
|
||||
if (poll_process(self, timeout))
|
||||
return 0;
|
||||
lua_pushnumber(L, self->returncode);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int self_signal(lua_State* L, signal_e sig) {
|
||||
process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS);
|
||||
signal_process(self, sig);
|
||||
lua_pushboolean(L, 1);
|
||||
return 1;
|
||||
}
|
||||
static int f_terminate(lua_State* L) { return self_signal(L, SIGNAL_TERM); }
|
||||
static int f_kill(lua_State* L) { return self_signal(L, SIGNAL_KILL); }
|
||||
static int f_interrupt(lua_State* L) { return self_signal(L, SIGNAL_INTERRUPT); }
|
||||
static int f_gc(lua_State* L) { return self_signal(L, SIGNAL_TERM); }
|
||||
|
||||
static int f_running(lua_State* L) {
|
||||
process_t* self = (process_t*)luaL_checkudata(L, 1, API_TYPE_PROCESS);
|
||||
lua_pushboolean(L, self->running);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static const struct luaL_Reg lib[] = {
|
||||
{"start", process_start},
|
||||
{"strerror", process_strerror},
|
||||
{"__gc", f_gc},
|
||||
{"__tostring", f_tostring},
|
||||
{"pid", f_pid},
|
||||
{"returncode", f_returncode},
|
||||
{"read", f_read},
|
||||
{"read_stdout", f_read_stdout},
|
||||
{"read_stderr", f_read_stderr},
|
||||
{"write", f_write},
|
||||
{"close_stream", f_close_stream},
|
||||
{"wait", f_wait},
|
||||
{"terminate", f_terminate},
|
||||
{"kill", f_kill},
|
||||
{"running", f_running},
|
||||
{NULL, NULL}
|
||||
{"__gc", f_gc},
|
||||
{"__tostring", f_tostring},
|
||||
{"start", process_start},
|
||||
{"strerror", process_strerror},
|
||||
{"pid", f_pid},
|
||||
{"returncode", f_returncode},
|
||||
{"read", f_read},
|
||||
{"read_stdout", f_read_stdout},
|
||||
{"read_stderr", f_read_stderr},
|
||||
{"write", f_write},
|
||||
{"close_stream", f_close_stream},
|
||||
{"wait", f_wait},
|
||||
{"terminate", f_terminate},
|
||||
{"kill", f_kill},
|
||||
{"interrupt", f_interrupt},
|
||||
{"running", f_running},
|
||||
{NULL, NULL}
|
||||
};
|
||||
|
||||
int luaopen_process(lua_State *L)
|
||||
{
|
||||
luaL_newmetatable(L, API_TYPE_PROCESS);
|
||||
luaL_setfuncs(L, lib, 0);
|
||||
lua_pushvalue(L, -1);
|
||||
lua_setfield(L, -2, "__index");
|
||||
int luaopen_process(lua_State *L) {
|
||||
luaL_newmetatable(L, API_TYPE_PROCESS);
|
||||
luaL_setfuncs(L, lib, 0);
|
||||
lua_pushvalue(L, -1);
|
||||
lua_setfield(L, -2, "__index");
|
||||
|
||||
// constants
|
||||
L_SETNUM(L, -1, "ERROR_INVAL", REPROC_EINVAL);
|
||||
L_SETNUM(L, -1, "ERROR_TIMEDOUT", REPROC_ETIMEDOUT);
|
||||
L_SETNUM(L, -1, "ERROR_PIPE", REPROC_EPIPE);
|
||||
L_SETNUM(L, -1, "ERROR_NOMEM", REPROC_ENOMEM);
|
||||
L_SETNUM(L, -1, "ERROR_WOULDBLOCK", REPROC_EWOULDBLOCK);
|
||||
API_CONSTANT_DEFINE(L, -1, "WAIT_INFINITE", WAIT_INFINITE);
|
||||
API_CONSTANT_DEFINE(L, -1, "WAIT_DEADLINE", WAIT_DEADLINE);
|
||||
|
||||
L_SETNUM(L, -1, "WAIT_INFINITE", REPROC_INFINITE);
|
||||
L_SETNUM(L, -1, "WAIT_DEADLINE", REPROC_DEADLINE);
|
||||
API_CONSTANT_DEFINE(L, -1, "STREAM_STDIN", STDIN_FD);
|
||||
API_CONSTANT_DEFINE(L, -1, "STREAM_STDOUT", STDOUT_FD);
|
||||
API_CONSTANT_DEFINE(L, -1, "STREAM_STDERR", STDERR_FD);
|
||||
|
||||
L_SETNUM(L, -1, "STREAM_STDIN", REPROC_STREAM_IN);
|
||||
L_SETNUM(L, -1, "STREAM_STDOUT", REPROC_STREAM_OUT);
|
||||
L_SETNUM(L, -1, "STREAM_STDERR", REPROC_STREAM_ERR);
|
||||
API_CONSTANT_DEFINE(L, -1, "REDIRECT_DEFAULT", REDIRECT_DEFAULT);
|
||||
API_CONSTANT_DEFINE(L, -1, "REDIRECT_STDOUT", STDOUT_FD);
|
||||
API_CONSTANT_DEFINE(L, -1, "REDIRECT_STDERR", STDERR_FD);
|
||||
API_CONSTANT_DEFINE(L, -1, "REDIRECT_PARENT", REDIRECT_PARENT); // Redirects to parent's STDOUT/STDERR
|
||||
API_CONSTANT_DEFINE(L, -1, "REDIRECT_DISCARD", REDIRECT_DISCARD); // Closes the filehandle, discarding it.
|
||||
|
||||
L_SETNUM(L, -1, "REDIRECT_DEFAULT", REPROC_REDIRECT_DEFAULT);
|
||||
L_SETNUM(L, -1, "REDIRECT_PIPE", REPROC_REDIRECT_PIPE);
|
||||
L_SETNUM(L, -1, "REDIRECT_PARENT", REPROC_REDIRECT_PARENT);
|
||||
L_SETNUM(L, -1, "REDIRECT_DISCARD", REPROC_REDIRECT_DISCARD);
|
||||
L_SETNUM(L, -1, "REDIRECT_STDOUT", REPROC_REDIRECT_STDOUT);
|
||||
|
||||
return 1;
|
||||
return 1;
|
||||
}
|
||||
|
|
|
@ -108,7 +108,7 @@ static const char *get_key_name(const SDL_Event *e, char *buf) {
|
|||
!(e->key.keysym.mod & KMOD_NUM)) {
|
||||
return numpad[scancode - SDL_SCANCODE_KP_1];
|
||||
} else {
|
||||
strcpy(buf, SDL_GetKeyName(e->key.keysym.sym));
|
||||
strcpy(buf, SDL_GetScancodeName(e->key.keysym.scancode));
|
||||
str_tolower(buf);
|
||||
return buf;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue