aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md1
-rw-r--r--plugins/primary_selection.lua187
2 files changed, 188 insertions, 0 deletions
diff --git a/README.md b/README.md
index 74b75f8..50c49b1 100644
--- a/README.md
+++ b/README.md
@@ -147,6 +147,7 @@ _Note: if you make a pull request, the table should be updated and kept in alpha
| [`openfilelocation`](plugins/openfilelocation.lua?raw=1) | Opens the parent directory of the current file in the file manager |
| [`openselected`](plugins/openselected.lua?raw=1) | Opens the selected filename or url |
| [`pdfview`](plugins/pdfview.lua?raw=1) | PDF preview for TeX files |
+| [`primary_selection`](plugins/primary_selection.lua?raw=1) | Adds middle mouse click copy/paste (primary selection). To use this plugin, `xclip` must be installed. |
| [`rainbowparen`](plugins/rainbowparen.lua?raw=1) | Show nesting of parentheses with rainbow colours |
| [`regexreplacepreview`](plugins/regexreplacepreview.lua?raw=1) | Allows for you to write a regex and its replacement in one go, and live preview the results. |
| [`restoretabs`](plugins/restoretabs.lua?raw=1) | Keep a list of recently closed tabs, and restore the tab in order on cntrl+shift+t. |
diff --git a/plugins/primary_selection.lua b/plugins/primary_selection.lua
new file mode 100644
index 0000000..388caf0
--- /dev/null
+++ b/plugins/primary_selection.lua
@@ -0,0 +1,187 @@
+-- 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
+ 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(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
+ core.active_view.doc:text_input(table.concat(text))
+ end
+ end
+})
+
+keymap.add({
+ ["1mclick"] = "primary-selection:paste"
+})
+