290 lines
6.5 KiB
Crystal
290 lines
6.5 KiB
Crystal
require "json"
|
|
require "semantic_version"
|
|
require "./tmux_style_printer"
|
|
|
|
def to_tmux_string(value)
|
|
# TODO tmux syntax to escape quotes
|
|
"\"\#{#{value}}\""
|
|
end
|
|
|
|
def to_tmux_number(value)
|
|
"\#{#{value}}"
|
|
end
|
|
|
|
def to_tmux_nullable_number(value)
|
|
"\#{?#{value},\#{#{value}},null}"
|
|
end
|
|
|
|
def to_tmux_bool(value)
|
|
"\#{?#{value},true,false}"
|
|
end
|
|
|
|
def build_tmux_format(hash)
|
|
fields = hash.map do |field, type|
|
|
if type == String
|
|
"\"#{field}\": #{to_tmux_string(field)}"
|
|
elsif type == Int32
|
|
"\"#{field}\": #{to_tmux_number(field)}"
|
|
elsif type == Int32 | Nil
|
|
"\"#{field}\": #{to_tmux_nullable_number(field)}"
|
|
elsif type == Bool
|
|
"\"#{field}\": #{to_tmux_bool(field)}"
|
|
end
|
|
end
|
|
|
|
"{#{fields.join(",")}}"
|
|
end
|
|
|
|
# TODO maybe use system everywhere?
|
|
|
|
# rubocop:disable Metrics/ClassLength
|
|
class Tmux
|
|
class Shell
|
|
def initialize
|
|
@sh = Process.new("/bin/sh", input: :pipe, output: :pipe, error: :close)
|
|
end
|
|
|
|
def exec(cmd)
|
|
ch = Channel(String).new
|
|
|
|
spawn do
|
|
output = ""
|
|
while line = @sh.output.read_line
|
|
break if line == "cmd-end"
|
|
|
|
output += "#{line}\n"
|
|
end
|
|
|
|
ch.send(output)
|
|
end
|
|
|
|
@sh.input.print("#{cmd}; echo cmd-end\n")
|
|
@sh.input.flush
|
|
output = ch.receive
|
|
output
|
|
end
|
|
end
|
|
|
|
struct Pane
|
|
include JSON::Serializable
|
|
|
|
property pane_id : String
|
|
property window_id : String
|
|
property pane_width : Int32
|
|
property pane_height : Int32
|
|
property pane_current_path : String
|
|
property pane_in_mode : Bool
|
|
property scroll_position : Int32 | Nil
|
|
property window_zoomed_flag : Bool
|
|
end
|
|
|
|
struct Window
|
|
include JSON::Serializable
|
|
|
|
property window_id : String
|
|
property window_width : Int32
|
|
property window_height : Int32
|
|
property pane_id : String
|
|
property pane_tty : String
|
|
end
|
|
|
|
# TODO make a macro or something
|
|
PANE_FORMAT = build_tmux_format({
|
|
pane_id: String,
|
|
window_id: String,
|
|
pane_width: Int32,
|
|
pane_height: Int32,
|
|
pane_current_path: String,
|
|
pane_in_mode: Bool,
|
|
scroll_position: Int32 | Nil,
|
|
window_zoomed_flag: Bool,
|
|
})
|
|
|
|
WINDOW_FORMAT = build_tmux_format({
|
|
window_id: String,
|
|
window_width: Int32,
|
|
window_height: Int32,
|
|
pane_id: String,
|
|
pane_tty: String,
|
|
})
|
|
|
|
@panes : Array(Pane) | Nil
|
|
@version : SemanticVersion
|
|
|
|
def self.tmux_version_to_semver(version_string)
|
|
match = version_string.match(/(?<major>[1-9]+[0-9]*)\.(?<minor>[0-9]+)(?<patch_letter>[a-z]+)?/)
|
|
|
|
raise "Invalid tmux version #{version_string}" unless match
|
|
|
|
major = match["major"].not_nil!
|
|
minor = match["minor"].not_nil!
|
|
patch_letter = match["patch_letter"]?
|
|
|
|
if patch_letter.nil?
|
|
patch = 0
|
|
else
|
|
patch = patch_letter[0].ord - 'a'.ord + 1
|
|
end
|
|
|
|
SemanticVersion.parse("#{major}.#{minor}.#{patch}")
|
|
end
|
|
|
|
def initialize(version_string)
|
|
@sh = Shell.new
|
|
@version = Tmux.tmux_version_to_semver(version_string)
|
|
end
|
|
|
|
def panes : Array(Pane)
|
|
exec("list-panes -a -F '#{PANE_FORMAT}'").chomp.split("\n").map do |pane|
|
|
Pane.from_json(pane)
|
|
end
|
|
end
|
|
|
|
def find_pane_by_id(id) : Pane | Nil
|
|
output = exec("display-message -t '#{id}' -F '#{PANE_FORMAT}' -p").chomp
|
|
|
|
return nil if output.empty?
|
|
|
|
Pane.from_json(output)
|
|
end
|
|
|
|
def capture_pane(pane : Pane, join = true)
|
|
if pane.pane_in_mode && !pane.scroll_position.nil?
|
|
scroll_position = pane.scroll_position.not_nil!
|
|
start_line = -scroll_position.to_i
|
|
end_line = pane.pane_height.to_i - scroll_position.to_i - 1
|
|
|
|
exec("capture-pane #{join ? "-J" : ""} -p -t '#{pane.pane_id}' -S #{start_line} -E #{end_line}").chomp
|
|
else
|
|
exec("capture-pane #{join ? "-J" : ""} -p -t '#{pane.pane_id}'").chomp
|
|
end
|
|
end
|
|
|
|
def create_window(name, cmd, _pane_width, _pane_height)
|
|
output = exec("new-window -P -d -n '#{name}' -F '#{WINDOW_FORMAT}' '#{cmd}'").chomp
|
|
|
|
Window.from_json(output)
|
|
end
|
|
|
|
def swap_panes(src_id, dst_id)
|
|
args = ["swap-pane", "-d", "-s", src_id, "-t", dst_id]
|
|
|
|
if @version >= Tmux.tmux_version_to_semver("3.1")
|
|
args << "-Z"
|
|
end
|
|
|
|
exec(args.join(" "))
|
|
end
|
|
|
|
def kill_pane(id)
|
|
exec("kill-pane -t #{id}")
|
|
end
|
|
|
|
def kill_window(id)
|
|
exec("kill-window -t #{id}")
|
|
end
|
|
|
|
# TODO: this command is version dependant D:
|
|
def resize_window(window_id, width, height)
|
|
exec(["resize-window", "-t", window_id, "-x", width.to_s, "-y", height.to_s].join(' '))
|
|
end
|
|
|
|
# TODO: this command is version dependant D:
|
|
def resize_pane(pane_id, width, height)
|
|
exec(["resize-pane", "-t", pane_id, "-x", width.to_s, "-y", height.to_s].join(' '))
|
|
end
|
|
|
|
def set_window_option(name, value)
|
|
exec(["set-window-option", name, value].join(' '))
|
|
end
|
|
|
|
def set_key_table(table)
|
|
exec(["set-window-option", "key-table", table].join(' '))
|
|
exec(["switch-client", "-T", table].join(' '))
|
|
end
|
|
|
|
def disable_prefix
|
|
set_global_option("prefix", "None")
|
|
set_global_option("prefix2", "None")
|
|
end
|
|
|
|
def set_global_option(name, value)
|
|
exec(Process.quote(["set-option", "-g", name, value]))
|
|
end
|
|
|
|
def get_global_option(name)
|
|
exec(["show", "-gqv", name].join(' ')).chomp
|
|
end
|
|
|
|
def set_buffer(value)
|
|
return unless value
|
|
|
|
if @version >= Tmux.tmux_version_to_semver("3.2")
|
|
args = ["load-buffer", "-w", "-"]
|
|
else
|
|
args = ["load-buffer", "-"]
|
|
end
|
|
|
|
# To avoid shell escaping nightmares, we'll use Process and write directly to stdin
|
|
cmd = Process.new(
|
|
tmux,
|
|
args,
|
|
input: :pipe,
|
|
output: :pipe,
|
|
error: :pipe,
|
|
)
|
|
|
|
cmd.input.print(value)
|
|
cmd.input.flush
|
|
|
|
cmd.wait
|
|
|
|
nil
|
|
end
|
|
|
|
def select_pane(id)
|
|
exec(["select-pane", "-t", id].join(' '))
|
|
end
|
|
|
|
def zoom_pane(id)
|
|
exec(["resize-pane", "-Z", "-t", id].join(' '))
|
|
end
|
|
|
|
# TODO
|
|
def parse_style(style)
|
|
style_printer.print(style).chomp
|
|
end
|
|
|
|
def style_printer
|
|
@style_printer ||= TmuxStylePrinter.new
|
|
end
|
|
|
|
def tmux
|
|
"tmux"
|
|
end
|
|
|
|
def build_tmux_output_format(fields)
|
|
fields.map { |field| format("\#{%<field>s}", field: field) }.join(";")
|
|
end
|
|
|
|
def parse_tmux_formatted_output(output)
|
|
output.split("\n").map do |line|
|
|
fields = line.split(";")
|
|
yield fields
|
|
end
|
|
end
|
|
|
|
def socket_flag_value
|
|
return ENV["FINGERS_TMUX_SOCKET"] if ENV["FINGERS_TMUX_SOCKET"]
|
|
socket
|
|
end
|
|
|
|
def display_message(msg, delay = 100)
|
|
exec(Process.quote(["display-message", "-d", delay.to_s, msg]))
|
|
end
|
|
|
|
def exec(cmd)
|
|
@sh.exec("#{tmux} #{cmd}")
|
|
end
|
|
end
|