diff --git a/README.md b/README.md index d2c4b4c..396dbf4 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,22 @@ Just call the following function to edit commands inside the list lua require('harpoon.cmd-ui').toggle_quick_menu() ``` +#### Tmux Integration +Harpoon also supports all terminal operations (see above) with tmux terminals. +The configuration for using tmux is exactly the same as the config for using nvim +terminals. To use tmux terminals instead of nvim terminals, simply replace +`harpoon.term` with `harpoon.tmux` in your require statement. + +For example: + +```lua +-- goes to the first tmux window +lua require("harpoon.tmux").gotoTerminal(1) + +-- sends a command to the first tmux window +lua require("harpoon.tmux").sendCommand(1, "ls -la") +``` + ### Setup Setup should be called once. @@ -146,6 +162,7 @@ require("harpoon").setup({ save_on_toggle = false, save_on_change = true, enter_on_sendcmd = false, + tmux_autoclose_windows = false, excluded_filetypes = { "harpoon" } }, ... your other configs ... @@ -160,6 +177,8 @@ require("harpoon").setup({ what I have found). * `enter_on_sendcmd` will set harpoon to run the command immediately as it's passed to the terminal when calling `sendCommand`. +* `tmux_autoclose_windows` will close any tmux windows harpoon that harpoon creates + when you close Neovim. * `excluded_filetypes` filetypes that you want to prevent from adding to the harpoon list menu. #### Preconfigured Terminal Commands diff --git a/lua/harpoon/init.lua b/lua/harpoon/init.lua index 2acc54a..735e294 100644 --- a/lua/harpoon/init.lua +++ b/lua/harpoon/init.lua @@ -17,12 +17,12 @@ local M = {} term = { cmds = { } - ... is there antyhnig that could be options? + ... is there anything that could be options? }, mark = { marks = { } - ... is there antyhnig that could be options? + ... is there anything that could be options? } } }, @@ -157,6 +157,7 @@ M.setup = function(config) ["save_on_toggle"] = false, ["save_on_change"] = true, ["enter_on_sendcmd"] = false, + ["tmux_autoclose_windows"] = false, ["excluded_filetypes"] = { "harpoon" }, }, }, expand_dir(c_config), expand_dir(u_config), expand_dir(config)) diff --git a/lua/harpoon/term.lua b/lua/harpoon/term.lua index fef47ec..0a0681c 100644 --- a/lua/harpoon/term.lua +++ b/lua/harpoon/term.lua @@ -9,7 +9,7 @@ local function create_terminal(create_with) if not create_with then create_with = ":terminal" end - log.trace("_create_terminal(): Init:", create_with) + log.trace("term: _create_terminal(): Init:", create_with) local current_id = vim.api.nvim_get_current_buf() vim.cmd(create_with) @@ -32,7 +32,7 @@ local function create_terminal(create_with) end local function find_terminal(args) - log.trace("_find_terminal(): Terminal:", args) + log.trace("term: _find_terminal(): Terminal:", args) if type(args) == "number" then args = { idx = args } end @@ -40,6 +40,7 @@ local function find_terminal(args) if not term_handle or not vim.api.nvim_buf_is_valid(term_handle.buf_id) then local buf_id, term_id = create_terminal(args.create_with) if buf_id == nil then + error("Failed to find and create terminal.") return end @@ -63,14 +64,14 @@ local function get_first_empty_slot() end M.gotoTerminal = function(idx) - log.trace("gotoTerminal(): Terminal:", idx) + log.trace("term: gotoTerminal(): Terminal:", idx) local term_handle = find_terminal(idx) vim.api.nvim_set_current_buf(term_handle.buf_id) end M.sendCommand = function(idx, cmd, ...) - log.trace("sendCommand(): Terminal:", idx) + log.trace("term: sendCommand(): Terminal:", idx) local term_handle = find_terminal(idx) if type(cmd) == "number" then @@ -88,7 +89,7 @@ M.sendCommand = function(idx, cmd, ...) end M.clear_all = function() - log.trace("clear_all(): Clearing all terminals.") + log.trace("term: clear_all(): Clearing all terminals.") for _, term in ipairs(terminals) do vim.api.nvim_buf_delete(term.buf_id, { force = true }) end diff --git a/lua/harpoon/tmux.lua b/lua/harpoon/tmux.lua new file mode 100644 index 0000000..06c168e --- /dev/null +++ b/lua/harpoon/tmux.lua @@ -0,0 +1,210 @@ +local harpoon = require("harpoon") +local log = require("harpoon.dev").log +local global_config = harpoon.get_global_settings() +local utils = require("harpoon.utils") + +local M = {} +local tmux_windows = {} + +if global_config.tmux_autoclose_windows then + vim.cmd([[ + augroup HARPOON_TMUX + autocmd! + autocmd VimLeave * :lua require('harpoon.tmux').clear_all() + ]]) +end + +local function create_terminal() + log.trace("tmux: _create_terminal())") + + local window_id + + -- Create a new tmux window and store the window id + local out, ret, _ = utils.get_os_command_output({ + "tmux", + "new-window", + "-P", + "-F", + "#{pane_id}", + }, vim.loop.cwd()) + + if ret == 0 then + window_id = out[1]:sub(2) + end + + if window_id == nil then + log.error("tmux: _create_terminal(): window_id is nil") + return nil + end + + return window_id +end + +-- Checks if the tmux window with the given window id exists +local function terminal_exists(window_id) + log.trace("_terminal_exists(): Window:", window_id) + + local exists = false + + local window_list, _, _ = utils.get_os_command_output({ + "tmux", + "list-windows", + }, vim.loop.cwd()) + + -- This has to be done this way because tmux has-session does not give + -- updated results + for _, line in pairs(window_list) do + local window_info = utils.split_string(line, "@")[1] + + if string.find(window_info, window_id) then + exists = true + end + end + + return exists +end + +local function find_terminal(args) + log.trace("tmux: _find_terminal(): Window:", args) + + if type(args) == "number" then + args = { idx = args } + end + + local window_handle = tmux_windows[args.idx] + local window_exists + + if window_handle then + window_exists = terminal_exists(window_handle.window_id) + end + + if not window_handle or not window_exists then + local window_id = create_terminal() + + if window_id == nil then + error("Failed to find and create tmux window.") + return + end + + window_handle = { + window_id = window_id, + } + + tmux_windows[args.idx] = window_handle + end + + return window_handle +end + +local function get_first_empty_slot() + log.trace("_get_first_empty_slot()") + for idx, cmd in pairs(harpoon.get_term_config().cmds) do + if cmd == "" then + return idx + end + end + return M.get_length() + 1 +end + +M.gotoTerminal = function(idx) + log.trace("tmux: gotoTerminal(): Window:", idx) + local window_handle = find_terminal(idx) + + utils.get_os_command_output({ + "tmux", + "select-window", + "-t", + "%" .. window_handle.window_id, + }, vim.loop.cwd()) +end + +M.sendCommand = function(idx, cmd, ...) + log.trace("tmux: sendCommand(): Window:", idx) + local window_handle = find_terminal(idx) + + if type(cmd) == "number" then + cmd = harpoon.get_term_config().cmds[cmd] + end + + if global_config.enter_on_sendcmd then + cmd = cmd .. "\n" + end + + if cmd then + log.debug("sendCommand:", cmd) + + -- Send the command to the given tmux window (creates it if it doesn't exist) + local _, _, _ = utils.get_os_command_output({ + "tmux", + "send-keys", + "-t", + "%" .. window_handle.window_id, + string.format(cmd, ...), + }, vim.loop.cwd()) + end +end + +M.clear_all = function() + log.trace("tmux: clear_all(): Clearing all tmux windows.") + + for _, window in pairs(tmux_windows) do + -- Delete the current tmux window + utils.get_os_command_output({ + "tmux", + "kill-window", + "-t", + "%" .. window.window_id, + }, vim.loop.cwd()) + end + + tmux_windows = {} +end + +M.get_length = function() + log.trace("_get_length()") + return table.maxn(harpoon.get_term_config().cmds) +end + +M.valid_index = function(idx) + if idx == nil or idx > M.get_length() or idx <= 0 then + return false + end + return true +end + +M.emit_changed = function() + log.trace("_emit_changed()") + if harpoon.get_global_settings().save_on_change then + harpoon.save() + end +end + +M.add_cmd = function(cmd) + log.trace("add_cmd()") + local found_idx = get_first_empty_slot() + harpoon.get_term_config().cmds[found_idx] = cmd + M.emit_changed() +end + +M.rm_cmd = function(idx) + log.trace("rm_cmd()") + if not M.valid_index(idx) then + log.debug("rm_cmd(): no cmd exists for index", idx) + return + end + table.remove(harpoon.get_term_config().cmds, idx) + M.emit_changed() +end + +M.set_cmd_list = function(new_list) + log.trace("set_cmd_list(): New list:", new_list) + for k in pairs(harpoon.get_term_config().cmds) do + harpoon.get_term_config().cmds[k] = nil + end + for k, v in pairs(new_list) do + harpoon.get_term_config().cmds[k] = v + end + M.emit_changed() +end + +return M diff --git a/lua/harpoon/utils.lua b/lua/harpoon/utils.lua index dd40766..f5a7101 100644 --- a/lua/harpoon/utils.lua +++ b/lua/harpoon/utils.lua @@ -1,11 +1,40 @@ local Path = require("plenary.path") local data_path = vim.fn.stdpath("data") +local Job = require("plenary.job") local M = { data_path = data_path, normalize_path = function(item) return Path:new(item):make_relative(vim.loop.cwd()) end, + get_os_command_output = function(cmd, cwd) + if type(cmd) ~= "table" then + print("Harpoon: [get_os_command_output]: cmd has to be a table") + return {} + end + local command = table.remove(cmd, 1) + local stderr = {} + local stdout, ret = Job + :new({ + command = command, + args = cmd, + cwd = cwd, + on_stderr = function(_, data) + table.insert(stderr, data) + end, + }) + :sync() + return stdout, ret, stderr + end, + split_string = function(str, delimiter) + local result = {} + + for match in (str .. delimiter):gmatch("(.-)" .. delimiter) do + table.insert(result, match) + end + + return result + end, is_white_space = function(str) return str:gsub("%s", "") == "" end,