libtmux for OCaml
OCaml-based tmux client
tmux client library for OCaml. Control tmux, connect to and create servers, navigate sessions, windows, and panes, send keys, watch for changes via control mode, capture pane contents
Capability: Navigate tmux via a typed object graph
open Libtmuxmodule Client = Libtmux_unix.Clientmodule Workspace = Libtmux_unix.Workspacemodule Config = Libtmux_workspace.Configmodule Plan = Libtmux_workspace.Planmodule Execution = Libtmux_workspace.Execution
let get = function | Ok value -> value | Error error -> failwith (Format.asprintf "%a" Error.pp error)
let capture_graph client (initial : Created.t) spawn = let linked = get (Client.create_session client ~name:"linked" ~window_name:"linked-home" ~spawn ()) in let pane = Snapshot.find_pane (get (Client.capture client)) (Pane.Target.id linked.pane) |> Option.get in let text = "showcase ready" in ignore (get (Client.wait_for_text ~timeout:0.5 client pane ~text)); get (Client.link_window client initial.window ~session:linked.session ~index:7); get (Client.capture client)
let print_graph snapshot = Snapshot.sessions snapshot |> Iarray.iter (fun session -> Session.window_links session |> Iarray.iter (fun link -> let window = Window_link.window link in Printf.printf "%s/%d %s: %d pane(s)\n" (Session.name session) (Window_link.index link) (Window.name window) (Iarray.length (Window.panes window)))); print_endline "graph: 2 sessions, 2 windows, 3 links, 2 panes"
let with_demo binary use = let directory = Filename.temp_dir "libtmux-ocaml-showcase-" "" in Fun.protect ~finally:(fun () -> Sys.readdir directory |> Array.iter (fun name -> Sys.remove (Filename.concat directory name)); Unix.rmdir directory) (fun () -> let socket = Filename.concat directory "server" in (* Give this private daemon a lifetime even if startup fails. *) let config_file = Filename.concat directory "tmux.conf" in Out_channel.with_open_text config_file (fun output -> output_string output "set-option -s exit-unattached on\n"); let wrapper = Filename.concat directory "tmux" in Out_channel.with_open_text wrapper (fun output -> Printf.fprintf output "#!/bin/sh\nexec %s -f %s \"$@\"\n" (Filename.quote binary) (Filename.quote config_file)); Unix.chmod wrapper 0o700; let channel = Unix.open_process_args_in binary [| binary; "-V" |] in let version = Fun.protect ~finally:(fun () -> if Unix.close_process_in channel <> Unix.WEXITED 0 then failwith "tmux version discovery failed") (fun () -> String.trim (input_line channel)) in if not (String.starts_with ~prefix:"tmux " version) then failwith "invalid tmux version"; let version = String.sub version 5 (String.length version - 5) in let ready = Filename.concat directory "ready.txt" in Out_channel.with_open_text ready (fun output -> output_string output "showcase ready\n"); let spawn = Spawn.make (Exec { program = "/bin/cat"; args = [ "-u"; ready; "-" ] }) in let config = match Config.make ~directory ~session:"showcase" [ Config.window ~name:"editor" [ Config.pane ~spawn () ] ] with | Ok value -> value | Error error -> failwith (Format.asprintf "%a" Config.pp_error error) in let plan = match Plan.make ~endpoint:(Endpoint.socket_path socket) ~version ~observed:None config with | Ok value -> value | Error error -> failwith (Format.asprintf "%a" Plan.pp_error error) in let operation = get (Workspace.submit_bootstrap ~binary:wrapper plan) in Fun.protect ~finally:(fun () -> get (Workspace.close operation)) (fun () -> let receipt = get (Workspace.await operation) in if receipt.status <> Execution.Succeeded then failwith "workspace setup failed"; let client = Option.get (Workspace.client operation) in let initial = List.hd (Iarray.get receipt.steps 0).created in Printf.printf "socket=%s\n%!" socket; let pane = Snapshot.find_pane (get (Client.capture client)) (Pane.Target.id initial.pane) |> Option.get in ignore (get (Client.wait_for_text ~timeout:0.5 client pane ~text:"showcase ready")); use client initial spawn))
let () = let binary = if Array.length Sys.argv = 2 then Sys.argv.(1) else "tmux" in let snapshot = with_demo binary capture_graph in print_graph snapshotopen Libtmuxmodule Client = Libtmux_unix.Clientmodule Workspace = Libtmux_unix.Workspacemodule Config = Libtmux_workspace.Configmodule Plan = Libtmux_workspace.Planmodule Execution = Libtmux_workspace.Execution
let get = function | Ok value -> value | Error error -> failwith (Format.asprintf "%a" Error.pp error)
let capture_graph client (initial : Created.t) spawn = let linked = get (Client.create_session client ~name:"linked" ~window_name:"linked-home" ~spawn ()) in let pane = Snapshot.find_pane (get (Client.capture client)) (Pane.Target.id linked.pane) |> Option.get in let text = "showcase ready" in ignore (get (Client.wait_for_text ~timeout:0.5 client pane ~text)); get (Client.link_window client initial.window ~session:linked.session ~index:7); get (Client.capture client)
let print_graph snapshot = Snapshot.sessions snapshot |> Iarray.iter (fun session -> Session.window_links session |> Iarray.iter (fun link -> let window = Window_link.window link in Printf.printf "%s/%d %s: %d pane(s)\n" (Session.name session) (Window_link.index link) (Window.name window) (Iarray.length (Window.panes window)))); print_endline "graph: 2 sessions, 2 windows, 3 links, 2 panes"
let with_demo binary use = let directory = Filename.temp_dir "libtmux-ocaml-showcase-" "" in Fun.protect ~finally:(fun () -> Sys.readdir directory |> Array.iter (fun name -> Sys.remove (Filename.concat directory name)); Unix.rmdir directory) (fun () -> let socket = Filename.concat directory "server" in (* Give this private daemon a lifetime even if startup fails. *) let config_file = Filename.concat directory "tmux.conf" in Out_channel.with_open_text config_file (fun output -> output_string output "set-option -s exit-unattached on\n"); let wrapper = Filename.concat directory "tmux" in Out_channel.with_open_text wrapper (fun output -> Printf.fprintf output "#!/bin/sh\nexec %s -f %s \"$@\"\n" (Filename.quote binary) (Filename.quote config_file)); Unix.chmod wrapper 0o700; let channel = Unix.open_process_args_in binary [| binary; "-V" |] in let version = Fun.protect ~finally:(fun () -> if Unix.close_process_in channel <> Unix.WEXITED 0 then failwith "tmux version discovery failed") (fun () -> String.trim (input_line channel)) in if not (String.starts_with ~prefix:"tmux " version) then failwith "invalid tmux version"; let version = String.sub version 5 (String.length version - 5) in let ready = Filename.concat directory "ready.txt" in Out_channel.with_open_text ready (fun output -> output_string output "showcase ready\n"); let spawn = Spawn.make (Exec { program = "/bin/cat"; args = [ "-u"; ready; "-" ] }) in let config = match Config.make ~directory ~session:"showcase" [ Config.window ~name:"editor" [ Config.pane ~spawn () ] ] with | Ok value -> value | Error error -> failwith (Format.asprintf "%a" Config.pp_error error) in let plan = match Plan.make ~endpoint:(Endpoint.socket_path socket) ~version ~observed:None config with | Ok value -> value | Error error -> failwith (Format.asprintf "%a" Plan.pp_error error) in let operation = get (Workspace.submit_bootstrap ~binary:wrapper plan) in Fun.protect ~finally:(fun () -> get (Workspace.close operation)) (fun () -> let receipt = get (Workspace.await operation) in if receipt.status <> Execution.Succeeded then failwith "workspace setup failed"; let client = Option.get (Workspace.client operation) in let initial = List.hd (Iarray.get receipt.steps 0).created in Printf.printf "socket=%s\n%!" socket; let pane = Snapshot.find_pane (get (Client.capture client)) (Pane.Target.id initial.pane) |> Option.get in ignore (get (Client.wait_for_text ~timeout:0.5 client pane ~text:"showcase ready")); use client initial spawn))
let () = let binary = if Array.length Sys.argv = 2 then Sys.argv.(1) else "tmux" in let snapshot = with_demo binary capture_graph in print_graph snapshotopen Libtmuxmodule Client = Libtmux_unix.Clientmodule Workspace = Libtmux_unix.Workspacemodule Config = Libtmux_workspace.Configmodule Plan = Libtmux_workspace.Planmodule Execution = Libtmux_workspace.Execution
let get = function | Ok value -> value | Error error -> failwith (Format.asprintf "%a" Error.pp error)
let capture_graph client (initial : Created.t) spawn = let linked = get (Client.create_session client ~name:"linked" ~window_name:"linked-home" ~spawn ()) in let pane = Snapshot.find_pane (get (Client.capture client)) (Pane.Target.id linked.pane) |> Option.get in let text = "showcase ready" in ignore (get (Client.wait_for_text ~timeout:0.5 client pane ~text)); get (Client.link_window client initial.window ~session:linked.session ~index:7); get (Client.capture client)
let print_graph snapshot = Snapshot.sessions snapshot |> Iarray.iter (fun session -> Session.window_links session |> Iarray.iter (fun link -> let window = Window_link.window link in Printf.printf "%s/%d %s: %d pane(s)\n" (Session.name session) (Window_link.index link) (Window.name window) (Iarray.length (Window.panes window)))); assert (Iarray.length (Snapshot.sessions snapshot) = 2); assert (Iarray.length (Snapshot.windows snapshot) = 2); assert (Iarray.length (Snapshot.window_links snapshot) = 3); assert (Iarray.length (Snapshot.panes snapshot) = 2); print_endline "graph: 2 sessions, 2 windows, 3 links, 2 panes"
let with_demo binary use = let directory = Filename.temp_dir "libtmux-ocaml-showcase-" "" in Fun.protect ~finally:(fun () -> Sys.readdir directory |> Array.iter (fun name -> Sys.remove (Filename.concat directory name)); Unix.rmdir directory) (fun () -> let socket = Filename.concat directory "server" in (* Give this private daemon a lifetime even if startup fails. *) let config_file = Filename.concat directory "tmux.conf" in Out_channel.with_open_text config_file (fun output -> output_string output "set-option -s exit-unattached on\n"); let wrapper = Filename.concat directory "tmux" in Out_channel.with_open_text wrapper (fun output -> Printf.fprintf output "#!/bin/sh\nexec %s -f %s \"$@\"\n" (Filename.quote binary) (Filename.quote config_file)); Unix.chmod wrapper 0o700; let channel = Unix.open_process_args_in binary [| binary; "-V" |] in let version = Fun.protect ~finally:(fun () -> if Unix.close_process_in channel <> Unix.WEXITED 0 then failwith "tmux version discovery failed") (fun () -> String.trim (input_line channel)) in if not (String.starts_with ~prefix:"tmux " version) then failwith "invalid tmux version"; let version = String.sub version 5 (String.length version - 5) in let ready = Filename.concat directory "ready.txt" in Out_channel.with_open_text ready (fun output -> output_string output "showcase ready\n"); let spawn = Spawn.make (Exec { program = "/bin/cat"; args = [ "-u"; ready; "-" ] }) in let config = match Config.make ~directory ~session:"showcase" [ Config.window ~name:"editor" [ Config.pane ~spawn () ] ] with | Ok value -> value | Error error -> failwith (Format.asprintf "%a" Config.pp_error error) in let plan = match Plan.make ~endpoint:(Endpoint.socket_path socket) ~version ~observed:None config with | Ok value -> value | Error error -> failwith (Format.asprintf "%a" Plan.pp_error error) in let operation = get (Workspace.submit_bootstrap ~binary:wrapper plan) in Fun.protect ~finally:(fun () -> get (Workspace.close operation)) (fun () -> let receipt = get (Workspace.await operation) in if receipt.status <> Execution.Succeeded then failwith "workspace setup failed"; let client = Option.get (Workspace.client operation) in let initial = List.hd (Iarray.get receipt.steps 0).created in Printf.printf "socket=%s\n%!" socket; let pane = Snapshot.find_pane (get (Client.capture client)) (Pane.Target.id initial.pane) |> Option.get in ignore (get (Client.wait_for_text ~timeout:0.5 client pane ~text:"showcase ready")); use client initial spawn))
let () = let binary = if Array.length Sys.argv = 2 then Sys.argv.(1) else "tmux" in let snapshot = with_demo binary capture_graph in print_graph snapshotopen Libtmuxmodule Client = Libtmux_unix.Clientmodule Workspace = Libtmux_unix.Workspacemodule Config = Libtmux_workspace.Configmodule Plan = Libtmux_workspace.Planmodule Execution = Libtmux_workspace.Execution
let get = function | Ok value -> value | Error error -> failwith (Format.asprintf "%a" Error.pp error)
let capture_graph client (initial : Created.t) spawn = let linked = get (Client.create_session client ~name:"linked" ~window_name:"linked-home" ~spawn ()) in let pane = Snapshot.find_pane (get (Client.capture client)) (Pane.Target.id linked.pane) |> Option.get in let text = "showcase ready" in ignore (get (Client.wait_for_text ~timeout:0.5 client pane ~text)); get (Client.link_window client initial.window ~session:linked.session ~index:7); get (Client.capture client)
let print_graph snapshot = Snapshot.sessions snapshot |> Iarray.iter (fun session -> Session.window_links session |> Iarray.iter (fun link -> let window = Window_link.window link in Printf.printf "%s/%d %s: %d pane(s)\n" (Session.name session) (Window_link.index link) (Window.name window) (Iarray.length (Window.panes window)))); assert (Iarray.length (Snapshot.sessions snapshot) = 2); assert (Iarray.length (Snapshot.windows snapshot) = 2); assert (Iarray.length (Snapshot.window_links snapshot) = 3); assert (Iarray.length (Snapshot.panes snapshot) = 2); print_endline "graph: 2 sessions, 2 windows, 3 links, 2 panes"
let with_demo binary use = let directory = Filename.temp_dir "libtmux-ocaml-showcase-" "" in Fun.protect ~finally:(fun () -> Sys.readdir directory |> Array.iter (fun name -> Sys.remove (Filename.concat directory name)); Unix.rmdir directory) (fun () -> let socket = Filename.concat directory "server" in (* Give this private daemon a lifetime even if startup fails. *) let config_file = Filename.concat directory "tmux.conf" in Out_channel.with_open_text config_file (fun output -> output_string output "set-option -s exit-unattached on\n"); let wrapper = Filename.concat directory "tmux" in Out_channel.with_open_text wrapper (fun output -> Printf.fprintf output "#!/bin/sh\nexec %s -f %s \"$@\"\n" (Filename.quote binary) (Filename.quote config_file)); Unix.chmod wrapper 0o700; let channel = Unix.open_process_args_in binary [| binary; "-V" |] in let version = Fun.protect ~finally:(fun () -> if Unix.close_process_in channel <> Unix.WEXITED 0 then failwith "tmux version discovery failed") (fun () -> String.trim (input_line channel)) in if not (String.starts_with ~prefix:"tmux " version) then failwith "invalid tmux version"; let version = String.sub version 5 (String.length version - 5) in let ready = Filename.concat directory "ready.txt" in Out_channel.with_open_text ready (fun output -> output_string output "showcase ready\n"); let spawn = Spawn.make (Exec { program = "/bin/cat"; args = [ "-u"; ready; "-" ] }) in let config = match Config.make ~directory ~session:"showcase" [ Config.window ~name:"editor" [ Config.pane ~spawn () ] ] with | Ok value -> value | Error error -> failwith (Format.asprintf "%a" Config.pp_error error) in let plan = match Plan.make ~endpoint:(Endpoint.socket_path socket) ~version ~observed:None config with | Ok value -> value | Error error -> failwith (Format.asprintf "%a" Plan.pp_error error) in let operation = get (Workspace.submit_bootstrap ~binary:wrapper plan) in Fun.protect ~finally:(fun () -> get (Workspace.close operation)) (fun () -> let receipt = get (Workspace.await operation) in if receipt.status <> Execution.Succeeded then failwith "workspace setup failed"; let client = Option.get (Workspace.client operation) in let initial = List.hd (Iarray.get receipt.steps 0).created in Printf.printf "socket=%s\n%!" socket; let pane = Snapshot.find_pane (get (Client.capture client)) (Pane.Target.id initial.pane) |> Option.get in ignore (get (Client.wait_for_text ~timeout:0.5 client pane ~text:"showcase ready")); use client initial spawn))
let () = let binary = if Array.length Sys.argv = 2 then Sys.argv.(1) else "tmux" in let snapshot = with_demo binary capture_graph in print_graph snapshotRun this example
OCaml 5.5.2, Dune 3.24.2, Linux and tmux 3.3a through 3.8. Install libtmux, libtmux-workspace and libtmux-unix in the active opam switch, then save these files at their shown paths.
Save the program above as examples/showcase_graph.ml in a new directory, along with the files below.
Every view retains protected cleanup. A private exit-unattached configuration ends the owned daemon after its clients close. The program stops its private tmux server and removes any temporary files it created when it finishes. Every view already unwraps result values and retains protected cleanup. The detailed view also checks the captured graph cardinalities.
dune-project
(lang dune 3.24)examples/dune
(executable (name showcase_graph) (libraries libtmux libtmux-workspace libtmux-unix unix))Setup and run
opam exec -- dune exec examples/showcase_graph.exeExpected output
graph: 2 sessions, 2 windows, 3 links, 2 panesopen Libtmuxmodule Client = Libtmux_unix.Clientmodule Workspace = Libtmux_unix.Workspacemodule Config = Libtmux_workspace.Configmodule Plan = Libtmux_workspace.Planmodule Execution = Libtmux_workspace.Execution
let get = function | Ok value -> value | Error error -> failwith (Format.asprintf "%a" Error.pp error)
let capture_graph client (initial : Created.t) spawn = let linked = get (Client.create_session client ~name:"linked" ~window_name:"linked-home" ~spawn ()) in let pane = Snapshot.find_pane (get (Client.capture client)) (Pane.Target.id linked.pane) |> Option.get in let text = "showcase ready" in ignore (get (Client.wait_for_text ~timeout:0.5 client pane ~text)); get (Client.link_window client initial.window ~session:linked.session ~index:7); get (Client.capture client)
let print_graph snapshot = Snapshot.sessions snapshot |> Iarray.iter (fun session -> Session.window_links session |> Iarray.iter (fun link -> let window = Window_link.window link in Printf.printf "%s/%d %s: %d pane(s)\n" (Session.name session) (Window_link.index link) (Window.name window) (Iarray.length (Window.panes window)))); assert (Iarray.length (Snapshot.sessions snapshot) = 2); assert (Iarray.length (Snapshot.windows snapshot) = 2); assert (Iarray.length (Snapshot.window_links snapshot) = 3); assert (Iarray.length (Snapshot.panes snapshot) = 2); print_endline "graph: 2 sessions, 2 windows, 3 links, 2 panes"
let with_demo binary use = let directory = Filename.temp_dir "libtmux-ocaml-showcase-" "" in Fun.protect ~finally:(fun () -> Sys.readdir directory |> Array.iter (fun name -> Sys.remove (Filename.concat directory name)); Unix.rmdir directory) (fun () -> let socket = Filename.concat directory "server" in (* Give this private daemon a lifetime even if startup fails. *) let config_file = Filename.concat directory "tmux.conf" in Out_channel.with_open_text config_file (fun output -> output_string output "set-option -s exit-unattached on\n"); let wrapper = Filename.concat directory "tmux" in Out_channel.with_open_text wrapper (fun output -> Printf.fprintf output "#!/bin/sh\nexec %s -f %s \"$@\"\n" (Filename.quote binary) (Filename.quote config_file)); Unix.chmod wrapper 0o700; let channel = Unix.open_process_args_in binary [| binary; "-V" |] in let version = Fun.protect ~finally:(fun () -> if Unix.close_process_in channel <> Unix.WEXITED 0 then failwith "tmux version discovery failed") (fun () -> String.trim (input_line channel)) in if not (String.starts_with ~prefix:"tmux " version) then failwith "invalid tmux version"; let version = String.sub version 5 (String.length version - 5) in let ready = Filename.concat directory "ready.txt" in Out_channel.with_open_text ready (fun output -> output_string output "showcase ready\n"); let spawn = Spawn.make (Exec { program = "/bin/cat"; args = [ "-u"; ready; "-" ] }) in let config = match Config.make ~directory ~session:"showcase" [ Config.window ~name:"editor" [ Config.pane ~spawn () ] ] with | Ok value -> value | Error error -> failwith (Format.asprintf "%a" Config.pp_error error) in let plan = match Plan.make ~endpoint:(Endpoint.socket_path socket) ~version ~observed:None config with | Ok value -> value | Error error -> failwith (Format.asprintf "%a" Plan.pp_error error) in let operation = get (Workspace.submit_bootstrap ~binary:wrapper plan) in Fun.protect ~finally:(fun () -> get (Workspace.close operation)) (fun () -> let receipt = get (Workspace.await operation) in if receipt.status <> Execution.Succeeded then failwith "workspace setup failed"; let client = Option.get (Workspace.client operation) in let initial = List.hd (Iarray.get receipt.steps 0).created in Printf.printf "socket=%s\n%!" socket; let pane = Snapshot.find_pane (get (Client.capture client)) (Pane.Target.id initial.pane) |> Option.get in ignore (get (Client.wait_for_text ~timeout:0.5 client pane ~text:"showcase ready")); use client initial spawn))
let () = let binary = if Array.length Sys.argv = 2 then Sys.argv.(1) else "tmux" in let snapshot = with_demo binary capture_graph in print_graph snapshotAPIs used
tmux 3.3+
The whole program, examples/showcase_graph.ml. The port's CI runs it in .github/workflows/ocaml.yml: opam exec -- python3 test/showcase_test.py "$TMUX_BINARY".
open Libtmuxmodule Client = Libtmux_unix.Clientmodule Workspace = Libtmux_unix.Workspacemodule Config = Libtmux_workspace.Configmodule Plan = Libtmux_workspace.Planmodule Execution = Libtmux_workspace.Execution
let get = function | Ok value -> value | Error error -> failwith (Format.asprintf "%a" Error.pp error)
let watch_output binary blocking (initial : Created.t) _spawn = Eio_main.run @@ fun env -> Eio.Switch.run @@ fun sw -> let module Client = Libtmux_eio.Client in let module Subscription = Libtmux_eio.Subscription in let client = get (Client.connect ~sw ~binary ~process_mgr:(Eio.Stdenv.process_mgr env) ~mono_clock:(Eio.Stdenv.mono_clock env) (Source.endpoint (Libtmux_unix.Client.source blocking))) in Fun.protect ~finally:(fun () -> Eio.Cancel.protect (fun () -> get (Client.close client))) (fun () -> let pane = Snapshot.find_pane (get (Client.capture client)) (Pane.Target.id initial.pane) |> Option.get in let subscription = get (Client.subscribe ~capacity_bytes:4096 client pane) in Fun.protect ~finally:(fun () -> Eio.Cancel.protect (fun () -> get (Subscription.close subscription))) (fun () -> let cursor, () = Eio.Fiber.pair (fun () -> match Subscription.wait_for_bytes ~timeout:0.5 subscription ~text:"changed" with | Ok cursor -> cursor | Error (Output.Failure error) -> failwith (Format.asprintf "%a" Error.pp error) | Error (Output.Ended reason) -> failwith (Format.asprintf "%a" Output.reason_pp reason)) (fun () -> get (Client.send_text client (Pane.target pane) ~text:"changed"); get (Client.send_keys client (Pane.target pane) [ Key.Enter ])) in assert (cursor.offset >= 7L); print_endline "observed: changed"))
let with_demo binary use = let directory = Filename.temp_dir "libtmux-ocaml-showcase-" "" in Fun.protect ~finally:(fun () -> Sys.readdir directory |> Array.iter (fun name -> Sys.remove (Filename.concat directory name)); Unix.rmdir directory) (fun () -> let socket = Filename.concat directory "server" in (* Give this private daemon a lifetime even if startup fails. *) let config_file = Filename.concat directory "tmux.conf" in Out_channel.with_open_text config_file (fun output -> output_string output "set-option -s exit-unattached on\n"); let wrapper = Filename.concat directory "tmux" in Out_channel.with_open_text wrapper (fun output -> Printf.fprintf output "#!/bin/sh\nexec %s -f %s \"$@\"\n" (Filename.quote binary) (Filename.quote config_file)); Unix.chmod wrapper 0o700; let channel = Unix.open_process_args_in binary [| binary; "-V" |] in let version = Fun.protect ~finally:(fun () -> if Unix.close_process_in channel <> Unix.WEXITED 0 then failwith "tmux version discovery failed") (fun () -> String.trim (input_line channel)) in if not (String.starts_with ~prefix:"tmux " version) then failwith "invalid tmux version"; let version = String.sub version 5 (String.length version - 5) in let ready = Filename.concat directory "ready.txt" in Out_channel.with_open_text ready (fun output -> output_string output "showcase ready\n"); let spawn = Spawn.make (Exec { program = "/bin/cat"; args = [ "-u"; ready; "-" ] }) in let config = match Config.make ~directory ~session:"showcase" [ Config.window ~name:"editor" [ Config.pane ~spawn () ] ] with | Ok value -> value | Error error -> failwith (Format.asprintf "%a" Config.pp_error error) in let plan = match Plan.make ~endpoint:(Endpoint.socket_path socket) ~version ~observed:None config with | Ok value -> value | Error error -> failwith (Format.asprintf "%a" Plan.pp_error error) in let operation = get (Workspace.submit_bootstrap ~binary:wrapper plan) in Fun.protect ~finally:(fun () -> get (Workspace.close operation)) (fun () -> let receipt = get (Workspace.await operation) in if receipt.status <> Execution.Succeeded then failwith "workspace setup failed"; let client = Option.get (Workspace.client operation) in let initial = List.hd (Iarray.get receipt.steps 0).created in Printf.printf "socket=%s\n%!" socket; let pane = Snapshot.find_pane (get (Client.capture client)) (Pane.Target.id initial.pane) |> Option.get in ignore (get (Client.wait_for_text ~timeout:0.5 client pane ~text:"showcase ready")); use client initial spawn))
let () = let binary = if Array.length Sys.argv = 2 then Sys.argv.(1) else "tmux" in with_demo binary (watch_output binary) tmux 3.3+
The whole program, examples/showcase_watch.ml. The port's CI runs it in .github/workflows/ocaml.yml: opam exec -- python3 test/showcase_test.py "$TMUX_BINARY".
open Libtmuxmodule Client = Libtmux_unix.Clientmodule Workspace = Libtmux_unix.Workspacemodule Config = Libtmux_workspace.Configmodule Plan = Libtmux_workspace.Planmodule Execution = Libtmux_workspace.Execution
let get = function | Ok value -> value | Error error -> failwith (Format.asprintf "%a" Error.pp error)
let send_keys client (initial : Created.t) _spawn = let text = "literal: $HOME; #{pane_id}" in let pane = Snapshot.find_pane (get (Client.capture client)) (Pane.Target.id initial.pane) |> Option.get in get (Client.send_text client (Pane.target pane) ~text); get (Client.send_keys client (Pane.target pane) [ Key.Enter ]); ignore (get (Client.wait_for_text ~timeout:0.5 client pane ~text)); Printf.printf "sent: %s\n" text
let with_demo binary use = let directory = Filename.temp_dir "libtmux-ocaml-showcase-" "" in Fun.protect ~finally:(fun () -> Sys.readdir directory |> Array.iter (fun name -> Sys.remove (Filename.concat directory name)); Unix.rmdir directory) (fun () -> let socket = Filename.concat directory "server" in (* Give this private daemon a lifetime even if startup fails. *) let config_file = Filename.concat directory "tmux.conf" in Out_channel.with_open_text config_file (fun output -> output_string output "set-option -s exit-unattached on\n"); let wrapper = Filename.concat directory "tmux" in Out_channel.with_open_text wrapper (fun output -> Printf.fprintf output "#!/bin/sh\nexec %s -f %s \"$@\"\n" (Filename.quote binary) (Filename.quote config_file)); Unix.chmod wrapper 0o700; let channel = Unix.open_process_args_in binary [| binary; "-V" |] in let version = Fun.protect ~finally:(fun () -> if Unix.close_process_in channel <> Unix.WEXITED 0 then failwith "tmux version discovery failed") (fun () -> String.trim (input_line channel)) in if not (String.starts_with ~prefix:"tmux " version) then failwith "invalid tmux version"; let version = String.sub version 5 (String.length version - 5) in let ready = Filename.concat directory "ready.txt" in Out_channel.with_open_text ready (fun output -> output_string output "showcase ready\n"); let spawn = Spawn.make (Exec { program = "/bin/cat"; args = [ "-u"; ready; "-" ] }) in let config = match Config.make ~directory ~session:"showcase" [ Config.window ~name:"editor" [ Config.pane ~spawn () ] ] with | Ok value -> value | Error error -> failwith (Format.asprintf "%a" Config.pp_error error) in let plan = match Plan.make ~endpoint:(Endpoint.socket_path socket) ~version ~observed:None config with | Ok value -> value | Error error -> failwith (Format.asprintf "%a" Plan.pp_error error) in let operation = get (Workspace.submit_bootstrap ~binary:wrapper plan) in Fun.protect ~finally:(fun () -> get (Workspace.close operation)) (fun () -> let receipt = get (Workspace.await operation) in if receipt.status <> Execution.Succeeded then failwith "workspace setup failed"; let client = Option.get (Workspace.client operation) in let initial = List.hd (Iarray.get receipt.steps 0).created in Printf.printf "socket=%s\n%!" socket; let pane = Snapshot.find_pane (get (Client.capture client)) (Pane.Target.id initial.pane) |> Option.get in ignore (get (Client.wait_for_text ~timeout:0.5 client pane ~text:"showcase ready")); use client initial spawn))
let () = let binary = if Array.length Sys.argv = 2 then Sys.argv.(1) else "tmux" in with_demo binary send_keys tmux 3.3+
The whole program, examples/showcase_send_keys.ml. The port's CI runs it in .github/workflows/ocaml.yml: opam exec -- python3 test/showcase_test.py "$TMUX_BINARY".
open Libtmuxmodule Client = Libtmux_unix.Clientmodule Workspace = Libtmux_unix.Workspacemodule Config = Libtmux_workspace.Configmodule Plan = Libtmux_workspace.Planmodule Execution = Libtmux_workspace.Execution
let get = function | Ok value -> value | Error error -> failwith (Format.asprintf "%a" Error.pp error)
let rename_window client (initial : Created.t) _spawn = let before = Snapshot.find_window (get (Client.capture client)) (Window.Target.id initial.window) |> Option.get in get (Client.rename_window client initial.window ~name:"editor-main"); let after = Snapshot.find_window (get (Client.capture client)) (Window.id before) |> Option.get in assert (Window.name before = "editor"); assert (Window.name after = "editor-main"); Printf.printf "renamed: %s\n" (Window.name after)
let with_demo binary use = let directory = Filename.temp_dir "libtmux-ocaml-showcase-" "" in Fun.protect ~finally:(fun () -> Sys.readdir directory |> Array.iter (fun name -> Sys.remove (Filename.concat directory name)); Unix.rmdir directory) (fun () -> let socket = Filename.concat directory "server" in (* Give this private daemon a lifetime even if startup fails. *) let config_file = Filename.concat directory "tmux.conf" in Out_channel.with_open_text config_file (fun output -> output_string output "set-option -s exit-unattached on\n"); let wrapper = Filename.concat directory "tmux" in Out_channel.with_open_text wrapper (fun output -> Printf.fprintf output "#!/bin/sh\nexec %s -f %s \"$@\"\n" (Filename.quote binary) (Filename.quote config_file)); Unix.chmod wrapper 0o700; let channel = Unix.open_process_args_in binary [| binary; "-V" |] in let version = Fun.protect ~finally:(fun () -> if Unix.close_process_in channel <> Unix.WEXITED 0 then failwith "tmux version discovery failed") (fun () -> String.trim (input_line channel)) in if not (String.starts_with ~prefix:"tmux " version) then failwith "invalid tmux version"; let version = String.sub version 5 (String.length version - 5) in let ready = Filename.concat directory "ready.txt" in Out_channel.with_open_text ready (fun output -> output_string output "showcase ready\n"); let spawn = Spawn.make (Exec { program = "/bin/cat"; args = [ "-u"; ready; "-" ] }) in let config = match Config.make ~directory ~session:"showcase" [ Config.window ~name:"editor" [ Config.pane ~spawn () ] ] with | Ok value -> value | Error error -> failwith (Format.asprintf "%a" Config.pp_error error) in let plan = match Plan.make ~endpoint:(Endpoint.socket_path socket) ~version ~observed:None config with | Ok value -> value | Error error -> failwith (Format.asprintf "%a" Plan.pp_error error) in let operation = get (Workspace.submit_bootstrap ~binary:wrapper plan) in Fun.protect ~finally:(fun () -> get (Workspace.close operation)) (fun () -> let receipt = get (Workspace.await operation) in if receipt.status <> Execution.Succeeded then failwith "workspace setup failed"; let client = Option.get (Workspace.client operation) in let initial = List.hd (Iarray.get receipt.steps 0).created in Printf.printf "socket=%s\n%!" socket; let pane = Snapshot.find_pane (get (Client.capture client)) (Pane.Target.id initial.pane) |> Option.get in ignore (get (Client.wait_for_text ~timeout:0.5 client pane ~text:"showcase ready")); use client initial spawn))
let () = let binary = if Array.length Sys.argv = 2 then Sys.argv.(1) else "tmux" in with_demo binary rename_window tmux 3.3+
The whole program, examples/showcase_rename_window.ml. The port's CI runs it in .github/workflows/ocaml.yml: opam exec -- python3 test/showcase_test.py "$TMUX_BINARY".
open Libtmuxmodule Client = Libtmux_unix.Clientmodule Workspace = Libtmux_unix.Workspacemodule Config = Libtmux_workspace.Configmodule Plan = Libtmux_workspace.Planmodule Execution = Libtmux_workspace.Execution
let get = function | Ok value -> value | Error error -> failwith (Format.asprintf "%a" Error.pp error)
let create_window client (initial : Created.t) spawn = let created = get (Client.create_window client initial.session ~name:"logs" ~spawn) in let window = Snapshot.find_window (get (Client.capture client)) (Window.Target.id created.window) |> Option.get in assert (Window.name window = "logs"); Printf.printf "created: %s\n" (Window.name window)
let with_demo binary use = let directory = Filename.temp_dir "libtmux-ocaml-showcase-" "" in Fun.protect ~finally:(fun () -> Sys.readdir directory |> Array.iter (fun name -> Sys.remove (Filename.concat directory name)); Unix.rmdir directory) (fun () -> let socket = Filename.concat directory "server" in (* Give this private daemon a lifetime even if startup fails. *) let config_file = Filename.concat directory "tmux.conf" in Out_channel.with_open_text config_file (fun output -> output_string output "set-option -s exit-unattached on\n"); let wrapper = Filename.concat directory "tmux" in Out_channel.with_open_text wrapper (fun output -> Printf.fprintf output "#!/bin/sh\nexec %s -f %s \"$@\"\n" (Filename.quote binary) (Filename.quote config_file)); Unix.chmod wrapper 0o700; let channel = Unix.open_process_args_in binary [| binary; "-V" |] in let version = Fun.protect ~finally:(fun () -> if Unix.close_process_in channel <> Unix.WEXITED 0 then failwith "tmux version discovery failed") (fun () -> String.trim (input_line channel)) in if not (String.starts_with ~prefix:"tmux " version) then failwith "invalid tmux version"; let version = String.sub version 5 (String.length version - 5) in let ready = Filename.concat directory "ready.txt" in Out_channel.with_open_text ready (fun output -> output_string output "showcase ready\n"); let spawn = Spawn.make (Exec { program = "/bin/cat"; args = [ "-u"; ready; "-" ] }) in let config = match Config.make ~directory ~session:"showcase" [ Config.window ~name:"editor" [ Config.pane ~spawn () ] ] with | Ok value -> value | Error error -> failwith (Format.asprintf "%a" Config.pp_error error) in let plan = match Plan.make ~endpoint:(Endpoint.socket_path socket) ~version ~observed:None config with | Ok value -> value | Error error -> failwith (Format.asprintf "%a" Plan.pp_error error) in let operation = get (Workspace.submit_bootstrap ~binary:wrapper plan) in Fun.protect ~finally:(fun () -> get (Workspace.close operation)) (fun () -> let receipt = get (Workspace.await operation) in if receipt.status <> Execution.Succeeded then failwith "workspace setup failed"; let client = Option.get (Workspace.client operation) in let initial = List.hd (Iarray.get receipt.steps 0).created in Printf.printf "socket=%s\n%!" socket; let pane = Snapshot.find_pane (get (Client.capture client)) (Pane.Target.id initial.pane) |> Option.get in ignore (get (Client.wait_for_text ~timeout:0.5 client pane ~text:"showcase ready")); use client initial spawn))
let () = let binary = if Array.length Sys.argv = 2 then Sys.argv.(1) else "tmux" in with_demo binary create_windowAPIs used
tmux 3.3+
The whole program, examples/showcase_create_window.ml. The port's CI runs it in .github/workflows/ocaml.yml: opam exec -- python3 test/showcase_test.py "$TMUX_BINARY".
open Libtmuxmodule Client = Libtmux_unix.Clientmodule Workspace = Libtmux_unix.Workspacemodule Config = Libtmux_workspace.Configmodule Plan = Libtmux_workspace.Planmodule Execution = Libtmux_workspace.Execution
let get = function | Ok value -> value | Error error -> failwith (Format.asprintf "%a" Error.pp error)
let move_window client (initial : Created.t) spawn = let moving = get (Client.create_window client initial.session ~name:"build" ~spawn) in let destination = get (Client.create_session client ~name:"destination" ~window_name:"landing" ~spawn ()) in get (Client.move_window_at client initial.session ~from_index:moving.window_index ~session:destination.session ~index:7); let window = Snapshot.find_window (get (Client.capture client)) (Window.Target.id moving.window) |> Option.get in let link = Query.exactly_one (Iarray.to_seq (Window.links window)) |> Result.get_ok in assert (Window_link.index link = 7); assert ( Session.Id.equal (Session.id (Window_link.session link)) (Session.Target.id destination.session)); Printf.printf "moved: %s at %d\n" (Window.name window) (Window_link.index link)
let with_demo binary use = let directory = Filename.temp_dir "libtmux-ocaml-showcase-" "" in Fun.protect ~finally:(fun () -> Sys.readdir directory |> Array.iter (fun name -> Sys.remove (Filename.concat directory name)); Unix.rmdir directory) (fun () -> let socket = Filename.concat directory "server" in (* Give this private daemon a lifetime even if startup fails. *) let config_file = Filename.concat directory "tmux.conf" in Out_channel.with_open_text config_file (fun output -> output_string output "set-option -s exit-unattached on\n"); let wrapper = Filename.concat directory "tmux" in Out_channel.with_open_text wrapper (fun output -> Printf.fprintf output "#!/bin/sh\nexec %s -f %s \"$@\"\n" (Filename.quote binary) (Filename.quote config_file)); Unix.chmod wrapper 0o700; let channel = Unix.open_process_args_in binary [| binary; "-V" |] in let version = Fun.protect ~finally:(fun () -> if Unix.close_process_in channel <> Unix.WEXITED 0 then failwith "tmux version discovery failed") (fun () -> String.trim (input_line channel)) in if not (String.starts_with ~prefix:"tmux " version) then failwith "invalid tmux version"; let version = String.sub version 5 (String.length version - 5) in let ready = Filename.concat directory "ready.txt" in Out_channel.with_open_text ready (fun output -> output_string output "showcase ready\n"); let spawn = Spawn.make (Exec { program = "/bin/cat"; args = [ "-u"; ready; "-" ] }) in let config = match Config.make ~directory ~session:"showcase" [ Config.window ~name:"editor" [ Config.pane ~spawn () ] ] with | Ok value -> value | Error error -> failwith (Format.asprintf "%a" Config.pp_error error) in let plan = match Plan.make ~endpoint:(Endpoint.socket_path socket) ~version ~observed:None config with | Ok value -> value | Error error -> failwith (Format.asprintf "%a" Plan.pp_error error) in let operation = get (Workspace.submit_bootstrap ~binary:wrapper plan) in Fun.protect ~finally:(fun () -> get (Workspace.close operation)) (fun () -> let receipt = get (Workspace.await operation) in if receipt.status <> Execution.Succeeded then failwith "workspace setup failed"; let client = Option.get (Workspace.client operation) in let initial = List.hd (Iarray.get receipt.steps 0).created in Printf.printf "socket=%s\n%!" socket; let pane = Snapshot.find_pane (get (Client.capture client)) (Pane.Target.id initial.pane) |> Option.get in ignore (get (Client.wait_for_text ~timeout:0.5 client pane ~text:"showcase ready")); use client initial spawn))
let () = let binary = if Array.length Sys.argv = 2 then Sys.argv.(1) else "tmux" in with_demo binary move_window tmux 3.3+
The whole program, examples/showcase_move_window.ml. The port's CI runs it in .github/workflows/ocaml.yml: opam exec -- python3 test/showcase_test.py "$TMUX_BINARY".
open Libtmuxmodule Client = Libtmux_unix.Clientmodule Workspace = Libtmux_unix.Workspacemodule Config = Libtmux_workspace.Configmodule Plan = Libtmux_workspace.Planmodule Execution = Libtmux_workspace.Execution
let get = function | Ok value -> value | Error error -> failwith (Format.asprintf "%a" Error.pp error)
let set_layout client (initial : Created.t) spawn = ignore (get (Client.split_pane client initial.pane ~direction:Geometry.Right ~spawn)); ignore (get (Client.split_pane client initial.pane ~direction:Geometry.Right ~spawn)); get (Client.select_layout client initial.window Layout.Even_horizontal); let window = Snapshot.find_window (get (Client.capture client)) (Window.Target.id initial.window) |> Option.get in let panes = Window.panes window in assert (Iarray.length panes = 3); assert ( Iarray.for_all (fun pane -> Pane.top pane = 0 && Pane.height pane = Window.height window) panes); assert ( Iarray.fold_left (fun width pane -> width + Pane.width pane) 2 panes = Window.width window); print_endline "layout: 3 horizontal panes"
let with_demo binary use = let directory = Filename.temp_dir "libtmux-ocaml-showcase-" "" in Fun.protect ~finally:(fun () -> Sys.readdir directory |> Array.iter (fun name -> Sys.remove (Filename.concat directory name)); Unix.rmdir directory) (fun () -> let socket = Filename.concat directory "server" in (* Give this private daemon a lifetime even if startup fails. *) let config_file = Filename.concat directory "tmux.conf" in Out_channel.with_open_text config_file (fun output -> output_string output "set-option -s exit-unattached on\n"); let wrapper = Filename.concat directory "tmux" in Out_channel.with_open_text wrapper (fun output -> Printf.fprintf output "#!/bin/sh\nexec %s -f %s \"$@\"\n" (Filename.quote binary) (Filename.quote config_file)); Unix.chmod wrapper 0o700; let channel = Unix.open_process_args_in binary [| binary; "-V" |] in let version = Fun.protect ~finally:(fun () -> if Unix.close_process_in channel <> Unix.WEXITED 0 then failwith "tmux version discovery failed") (fun () -> String.trim (input_line channel)) in if not (String.starts_with ~prefix:"tmux " version) then failwith "invalid tmux version"; let version = String.sub version 5 (String.length version - 5) in let ready = Filename.concat directory "ready.txt" in Out_channel.with_open_text ready (fun output -> output_string output "showcase ready\n"); let spawn = Spawn.make (Exec { program = "/bin/cat"; args = [ "-u"; ready; "-" ] }) in let config = match Config.make ~directory ~session:"showcase" [ Config.window ~name:"editor" [ Config.pane ~spawn () ] ] with | Ok value -> value | Error error -> failwith (Format.asprintf "%a" Config.pp_error error) in let plan = match Plan.make ~endpoint:(Endpoint.socket_path socket) ~version ~observed:None config with | Ok value -> value | Error error -> failwith (Format.asprintf "%a" Plan.pp_error error) in let operation = get (Workspace.submit_bootstrap ~binary:wrapper plan) in Fun.protect ~finally:(fun () -> get (Workspace.close operation)) (fun () -> let receipt = get (Workspace.await operation) in if receipt.status <> Execution.Succeeded then failwith "workspace setup failed"; let client = Option.get (Workspace.client operation) in let initial = List.hd (Iarray.get receipt.steps 0).created in Printf.printf "socket=%s\n%!" socket; let pane = Snapshot.find_pane (get (Client.capture client)) (Pane.Target.id initial.pane) |> Option.get in ignore (get (Client.wait_for_text ~timeout:0.5 client pane ~text:"showcase ready")); use client initial spawn))
let () = let binary = if Array.length Sys.argv = 2 then Sys.argv.(1) else "tmux" in with_demo binary set_layout tmux 3.3+
The whole program, examples/showcase_layout.ml. The port's CI runs it in .github/workflows/ocaml.yml: opam exec -- python3 test/showcase_test.py "$TMUX_BINARY".
Install libtmux
Source installation; no registry release. Requires OCaml 5.5.2, Linux and tmux 3.3a through 3.8.
Workspace Manager
Install libtmux-workspace-cli
The workspace package is separate from the core library.
Build the CLI from source, then use tmux-workspace apply with a validated JSON workspace.
Build the workspace from a program
let config = match Config.make ~directory ~session:"showcase" [ Config.window ~name:"editor" [ Config.pane ~spawn () ] ] with | Ok value -> value | Error error -> failwith (Format.asprintf "%a" Config.pp_error error)inlet plan = match Plan.make ~endpoint:(Endpoint.socket_path socket) ~version ~observed:None config with | Ok value -> value | Error error -> failwith (Format.asprintf "%a" Plan.pp_error error)inlet operation = get (Workspace.submit_bootstrap ~binary:wrapper plan) in
let receipt = get (Workspace.await operation) inif receipt.status <> Execution.Succeeded then failwith "workspace setup failed";let client = Option.get (Workspace.client operation) inlet initial = List.hd (Iarray.get receipt.steps 0).created in
Excerpt of examples/showcase_graph.ml,
which opam exec -- python3 test/showcase_test.py "$TMUX_BINARY" runs in the repository's CI
(.github/workflows/ocaml.yml).
Topics
Subject-by-subject, with OCaml in every code block.
Guides
Task guides for OCaml.
Examples
Complete programs, sourced from the code each port tests.
Concepts
The domain model every port shares.