aboutsummaryrefslogtreecommitdiff
path: root/plugins/primary_selection.lua
blob: e061b764cfe6363bd61232f62500182260fd2a9e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
-- 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"
})