aboutsummaryrefslogtreecommitdiff
-- mod-version:3
local core = require "core"
local Doc = require "core.doc"
local command = require "core.command"
local keymap = require "core.keymap"
local config = require "core.config"
local common = require "core.common"

local function string_to_cmd(s)
  local result = {}
  for match in s:gmatch("%g+") do
    table.insert(result, match)
  end
  return result
end

config.plugins.primary_selection = common.merge({
  command_in = { "xclip", "-in", "-selection", "primary" }, -- Command to use to copy the selection
  command_out = { "xclip", "-out", "-selection", "primary" }, -- Command to use to obtain the selection
  set_cursor = true, -- Set cursor on middle mouse click
  min_copy_time = 0.150, -- How much time to delay setting the selection; in seconds
  config_spec = {
    name = "Primary selection",
    {
      label = "Command copy",
      description = "Command to use to copy the selection.",
      path = "_command_in",
      type = "string",
      default = "xclip -in -selection primary",
      on_apply = function(value)
        config.plugins.primary_selection.command_in = string_to_cmd(value)
      end,
    },
    {
      label = "Command paste",
      description = "Command to use to obtain the selection.",
      path = "_command_out",
      type = "string",
      default = "xclip -out -selection primary",
      on_apply = function(value)
        config.plugins.primary_selection.command_out = string_to_cmd(value)
      end,
    },
    {
      label = "Set cursor",
      description = "Set cursor on middle mouse click.",
      path = "set_cursor",
      type = "toggle",
      default = true,
    },
    {
      label = "Copy timeout",
      description = "How much time to delay setting the selection; in milliseconds.",
      path = "min_copy_time_ms",
      type = "number",
      default = 150,
      min = 0,
      step = 50,
      on_apply = function(value)
        config.plugins.primary_selection.min_copy_time = value / 1000
      end
    },
  }
}, config.plugins.primary_selection)


local last_selection_data
--[[
 = {
  time = nil,
  line1 = nil,
  col1 = nil,
  line2 = nil,
  col2 = nil,
  doc = nil,
}
]]

local xclip_copy
local function delayed_copy()
  while true do
    local data = last_selection_data
    if not data then return end
    local current_time = system.get_time()
    local diff_time = current_time - data.time
    -- Check if enough time has passed since last selection change
    if diff_time >= config.plugins.primary_selection.min_copy_time then
      if xclip_copy then xclip_copy:terminate() end
      if not config.plugins.primary_selection.command_in
       or #config.plugins.primary_selection.command_in == 0 then
        core.warn("No primary selection copy command set")
        break
      end
      xclip_copy = process.start(config.plugins.primary_selection.command_in)
      if not xclip_copy then
        core.warn("Unable to start copy command")
        break
      end
      local text = data.doc:get_text(data.line1, data.col1, data.line2, data.col2)
      local nbytes = #text
      local total_written = 0
      -- In some rare cases xclip isn't fast enough so we need to retry sending the data
      local retry = 3
      repeat
        local written, err = xclip_copy:write(text)
        if written == 0 or not written then
          if retry > 0 then
            retry = retry - 1
            coroutine.yield(((3-retry) ^ 2) * 0.05)
          else
            core.error("Error while setting primary selection. "..(err or ""))
            break
          end
        else
          retry = 3
        end
        total_written = total_written + written
        text = string.sub(text, written + 1)
      until total_written >= nbytes
      xclip_copy:close_stream(process.STREAM_STDIN)
      -- We need to leave the process running as killing it would destroy the copied buffer
      break
    end
    coroutine.yield()
  end
  last_selection_data = nil
end


local doc_set_selections = Doc.set_selections
function Doc:set_selections(...)
  local result = doc_set_selections(self, ...)
  local line1, col1, line2, col2
  line1, col1, line2, col2 = self:get_selection()
  if line1 ~= line2 or col1 ~= col2 then
    if not last_selection_data then
      -- Start "timer" to confirm the selection only after `min_copy_time` has passed
      core.add_thread(delayed_copy)
      last_selection_data = { }
    end
    -- We could extract the text here, but it is a potentially heavy operation,
    -- so we do it only when we're actually confirming the selection.
    -- The drawback is that if the selection is overwritten/deleted,
    -- it is either never sent, or is different than expected.
    -- TODO: Confirm the selection on text change.
    last_selection_data.time  = system.get_time()
    last_selection_data.line1 = line1
    last_selection_data.col1  = col1
    last_selection_data.line2 = line2
    last_selection_data.col2  = col2
    last_selection_data.doc   = self
  end
  return result
end


command.add("core.docview", {
  ["primary-selection:paste"] = function(dv, x, y, clicks, ...)
    if not config.plugins.primary_selection.command_out
     or #config.plugins.primary_selection.command_out == 0 then
      core.warn("No primary selection paste command set")
      return
    end
    if x and config.plugins.primary_selection.set_cursor then
      -- TODO: There must be a better way to do this
      core.on_event("mousepressed", "left", x, y, clicks, ...)
      core.on_event("mousereleased", "left", x, y, clicks, ...)
    end
    local xclip = process.start(config.plugins.primary_selection.command_out)
    if not xclip then
      core.warn("Unable to start paste command")
      return
    end
    local text = {}
    repeat
      local buffer = xclip:read_stdout()
      table.insert(text, buffer or "")
    until not buffer
    if #text > 0 then
      dv.doc:text_input(table.concat(text))
    end
  end
})

keymap.add({
  ["1mclick"] = "primary-selection:paste"
})