Current section
Files
Jump to
Current section
Files
src/i3_client.erl
-module(i3_client).-moduledoc """> #### Warning {: .warning } >> The documentation, especially on successful return values, is> intentionally vague. This library doesn't do any meaningful> processing of message responses or events before handing them over> to the user. The main principle is that responses are maps but the> events are kept as binaries. Events can potentially be big and> shouldn't be copied carelessly during fan out. > > The official i3 IPC interface> [documentation](https://i3wm.org/docs/ipc.html) should be your> primary guide regarding what's included in successful responses and> events but also make sure to consult your variant server's> documentation because there can be differences.""".-export([ init/1, publish/3, handle_connect/1, handle_disconnect/1, handle_call/3, handle_info/2, handle_result/2]).-export([ child_spec/0, child_spec/1, start/1, start/2, start_link/1, start_link/2, start_monitor/1, start_monitor/2, run_command/2, run_command/3, get_workspaces/1, get_workspaces/2, get_outputs/1, get_outputs/2, get_tree/1, get_tree/2, get_marks/1, get_marks/2, get_bar_config/1, get_bar_config/2, get_bar_config/3, get_version/1, get_version/2, get_binding_modes/1, get_binding_modes/2, get_config/1, get_config/2, send_tick/2, send_tick/3, sync/2, sync/3, get_binding_state/1, get_binding_state/2, get_inputs/1, get_inputs/2, get_seats/1, get_seats/2, subscribe/2, subscribe/3]).-include("i3_client_codec.hrl").-define(timeout, 5000).-define(client_opts, [auto_reconnect]).-type start_opt() :: {path, binary()} | {auto_reconnect, boolean()} | {reconnect_backoff, boolean()} | {sync_connect, boolean()} | gen_statem:start_opt().-type start_ret() :: gen_statem:start_ret().-type server_ref() :: gen_statem:server_ref().-type server_name() :: gen_statem:server_name().-type message_opt() :: {timeout, timeout()}.-type message_ret() :: message_ret(map()).-type message_ret(Ret) :: {ok, Ret} | {error, enotsup | {badmsg, map()}}.-behaviour(i3_client_connection).-doc """Equivalent to [`child_spec([])`](`child_spec/1`).""".child_spec() -> child_spec([]).-doc """Returns a child specification for i3 connection process.The process name can be provided as a `name` option to keep itconsistent with Elixir's convention.""".child_spec(Opts0) -> Start = case lists:keytake(name, 1, Opts0) of {value, {name, Name}, Opts1} -> {i3_client, start_link, [Name, Opts1]}; false -> {i3_client, start_link, [Opts0]} end, #{ id => i3_client_connection, start => Start }.-doc """Start an i3 client connection, neither linked nor registered.""".-spec start([start_opt()]) -> start_ret().start(Opts) -> {Args, _} = i3_client_common:keystake(?client_opts, Opts), i3_client_connection:start(?MODULE, Args, Opts).-doc """Start an i3 client connection, registered but not linked.""".-spec start(server_name(), [start_opt()]) -> start_ret().start(ServerName, Opts) -> {Args, _} = i3_client_common:keystake(?client_opts, Opts), i3_client_connection:start_link(ServerName, ?MODULE, Args, Opts).-doc """Start an i3 client connection, linked but not registered.""".-spec start_link([start_opt()]) -> start_ret().start_link(Opts) -> {Args, _} = i3_client_common:keystake(?client_opts, Opts), i3_client_connection:start_link(?MODULE, Args, Opts).-doc """Start an i3 client connection, linked and registered.""".-spec start_link(server_name(), [start_opt()]) -> start_ret().start_link(ServerName, Opts) -> {Args, _} = i3_client_common:keystake(?client_opts, Opts), i3_client_connection:start_link(ServerName, ?MODULE, Args, Opts).-doc """Start an i3 client connection, monitored but not registered.""".-spec start_monitor([start_opt()]) -> start_ret().start_monitor(Opts) -> {Args, _} = i3_client_common:keystake(?client_opts, Opts), i3_client_connection:start_monitor(?MODULE, Args, Opts).-doc """Start an i3 client connection, monitored and registered.""".-spec start_monitor(server_name(), [start_opt()]) -> start_ret().start_monitor(ServerName, Opts) -> {Args, _} = i3_client_common:keystake(?client_opts, Opts), i3_client_connection:start_monitor(ServerName, ?MODULE, Args, Opts).-doc """Equivalent to [`run_command(Server, Command, [])`](`run_command/3`).""".-spec run_command(server_ref(), binary()) -> message_ret([map()]).run_command(Server, Command) -> run_command(Server, Command, []).-doc """Runs the command as an i3 command.""".-spec run_command(server_ref(), binary(), [message_opt()]) -> message_ret([map()]).run_command(Server, Command, Opts) -> i3_client_connection:call( Server, {?run_command, Command}, proplists:get_value(timeout, Opts, ?timeout) ).-doc """Equivalent to [`get_workspaces(Server, [])`](`get_workspaces/2`).""".-spec get_workspaces(server_ref()) -> message_ret([map()]).get_workspaces(Server) -> get_workspaces(Server, []).-doc """Gets the list of current workspaces.""".-spec get_workspaces(server_ref(), [message_opt()]) -> message_ret([map()]).get_workspaces(Server, Opts) -> i3_client_connection:call( Server, {?get_workspaces, ?nopayload}, proplists:get_value(timeout, Opts, ?timeout) ).-doc """Equivalent to [`get_outputs(Server, [])`](`get_outputs/2`).""".-spec get_outputs(server_ref()) -> message_ret([map()]).get_outputs(Server) -> get_outputs(Server, []).-doc """Gets the list of current outputs.""".-spec get_outputs(server_ref(), [message_opt()]) -> message_ret([map()]).get_outputs(Server, Opts) -> i3_client_connection:call( Server, {?get_outputs, ?nopayload}, proplists:get_value(timeout, Opts, ?timeout) ).-doc """Equivalent to [`get_tree(Server, [])`](`get_tree/2`).""".-spec get_tree(server_ref()) -> message_ret().get_tree(Server) -> get_tree(Server, []).-doc """Gets the window manager's layout tree.""".-spec get_tree(server_ref(), [message_opt()]) -> message_ret().get_tree(Server, Opts) -> i3_client_connection:call( Server, {?get_tree, ?nopayload}, proplists:get_value(timeout, Opts, ?timeout) ).-doc """Equivalent to [`get_marks(Server, [])`](`get_marks/2`).""".-spec get_marks(server_ref()) -> message_ret([binary()]).get_marks(Server) -> get_marks(Server, []).-doc """Gets the names of all currently set marks.""".-spec get_marks(server_ref(), [message_opt()]) -> message_ret([binary()]).get_marks(Server, Opts) -> i3_client_connection:call( Server, {?get_marks, ?nopayload}, proplists:get_value(timeout, Opts, ?timeout) ).-doc """Equivalent to [`get_bar_config(Server, [])`](`get_bar_config/2`).""".-spec get_bar_config(server_ref()) -> message_ret([binary()]).get_bar_config(Server) -> get_bar_config(Server, []).-doc """Equivalent to [`get_bar_config(Server, <<"">>,Opts)`](`get_bar_config/3`) or [`get_bar_config(Server, BarId,[])`](`get_bar_config/3`).""".-spec get_bar_config(server_ref(), binary() | [message_opt()]) -> message_ret([binary()] | map()).get_bar_config(Server, Opts) when is_list(Opts) -> get_bar_config(Server, <<"">>, Opts);get_bar_config(Server, BarId) when is_binary(BarId) -> get_bar_config(Server, BarId, []).-doc """Gets a particular bar configuration or the names of all barconfigurations.""".-spec get_bar_config(server_ref(), binary(), [message_opt()]) -> message_ret([binary()] | map()).get_bar_config(Server, BarId, Opts) when is_binary(BarId) and is_list(Opts) -> i3_client_connection:call( Server, {?get_bar_config, BarId}, proplists:get_value(timeout, Opts, ?timeout) ).-doc """Equivalent to [`get_version(Server, [])`](`get_version/2`).""".-spec get_version(server_ref()) -> message_ret().get_version(Server) -> get_version(Server, []).-doc """Gets the i3 IPC server's version.""".-spec get_version(server_ref(), [message_opt()]) -> message_ret().get_version(Server, Opts) -> i3_client_connection:call( Server, {?get_version, ?nopayload}, proplists:get_value(timeout, Opts, ?timeout) ).-doc """Equivalent to [`get_binding_modes(Server,[])`](`get_binding_modes/2`).""".-spec get_binding_modes(server_ref()) -> message_ret([binary()]).get_binding_modes(Server) -> get_binding_modes(Server, []).-doc """Gets the names of all currently configured binding modes.""".-spec get_binding_modes(server_ref(), [message_opt()]) -> message_ret([binary()]).get_binding_modes(Server, Opts) -> i3_client_connection:call( Server, {?get_binding_modes, ?nopayload}, proplists:get_value(timeout, Opts, ?timeout) ).-doc """Equivalent to [`get_config(Server, [])`](`get_config/2`).""".-spec get_config(server_ref()) -> message_ret().get_config(Server) -> get_config(Server, []).-doc """Gets the i3 IPC server's last loaded config.""".-spec get_config(server_ref(), [message_opt()]) -> message_ret().get_config(Server, Opts) -> i3_client_connection:call( Server, {?get_config, ?nopayload}, proplists:get_value(timeout, Opts, ?timeout) ).-doc """Equivalent to [`send_tick(Server, Payload, [])`](`send_tick/3`).""".-spec send_tick(server_ref(), binary()) -> term().send_tick(Server, Payload) when is_binary(Payload) -> send_tick(Server, Payload, []).-doc """Sends a tick event with the specified payload.""".-spec send_tick(server_ref(), binary(), [message_opt()]) -> term().send_tick(Server, Payload, Opts) -> i3_client_connection:call( Server, {?send_tick, Payload}, proplists:get_value(timeout, Opts, ?timeout) ).-doc """Equivalent to [`sync(Server, Payload, [])`](`sync/3`).""".-spec sync(server_ref(), #{rnd => non_neg_integer(), window => non_neg_integer()}) -> message_ret().sync(Server, #{rnd := Rnd, window := Win} = Payload) when is_integer(Rnd) and is_integer(Win) -> sync(Server, Payload, []).-doc """Sends an i3 sync event with the specified random value to thespecified window.> #### Info {: .info } > On Sway, this command will just return a failure map since it does> not make sense to implement due to the X11 nature of the command.""".-spec sync(server_ref(), #{rnd => non_neg_integer(), window => non_neg_integer()}, [message_opt()]) -> message_ret().sync(Server, #{rnd := Rnd, window := Win} = Payload, Opts) when is_integer(Rnd) and is_integer(Win)-> i3_client_connection:call( Server, {?sync, Payload}, proplists:get_value(timeout, Opts, ?timeout) ).-doc """Equivalent to [`get_binding_state(Server,[])`](`get_binding_state/2`).""".-spec get_binding_state(server_ref()) -> message_ret().get_binding_state(Server) -> get_binding_state(Server, []).-doc """Gets the current binding state, i.e. the currently active binding modename.""".-spec get_binding_state(server_ref(), [message_opt()]) -> message_ret().get_binding_state(Server, Opts) -> i3_client_connection:call( Server, {?get_binding_state, ?nopayload}, proplists:get_value(timeout, Opts, ?timeout) ).-doc """Equivalent to [`get_inputs(Server, [])`](`get_inputs/2`).""".-spec get_inputs(server_ref()) -> message_ret([map()]).get_inputs(Server) -> get_inputs(Server, []).-doc """Gets a list of the input devices currently available.> #### Warning {: .warning }> This message type is supported only in Sway.""".-spec get_inputs(server_ref(), [message_opt()]) -> message_ret([map()]).get_inputs(Server, Opts) -> i3_client_connection:call( Server, {?get_inputs, ?nopayload}, proplists:get_value(timeout, Opts, ?timeout) ).-doc """Equivalent to [`get_seats(Server, [])`](`get_seats/2`).""".-spec get_seats(server_ref()) -> message_ret([map()]).get_seats(Server) -> get_seats(Server, []).-doc """Gets a list of the currently configured seats.> #### Warning {: .warning }> This message type is supported only in Sway.""".-spec get_seats(server_ref(), [message_opt()]) -> message_ret([map()]).get_seats(Server, Opts) -> i3_client_connection:call( Server, {?get_seats, ?nopayload}, proplists:get_value(timeout, Opts, ?timeout) ).-doc """Equivalent to [`subscribe(Server, Events, [])`](`subscribe/3`).""".-spec subscribe(server_ref(), [binary()]) -> message_ret().subscribe(Server, [_ | _] = Events) -> subscribe(Server, Events, []).-doc """Subscribes this client connection to a list of asynchronous events.A message `{i3_event, Conn :: pid(), Ref :: reference(), Type ::binary(), Payload :: binary()}` will be sent to the calling processwhen an event is received.Returns `{ok, Ref :: reference()}` when the subscription is successfulor `{eventually, Ref :: reference()}` if the client process is notcurrently connected. The latter might be the case if the process wasstarted with `sync_connect` set to false or `auto_reconnect` set totrue. The function might also return `{error, {badmsg, map()}}` if thesubscription is rejected by the server.## Options * `timeout` - Call timeout (default: 5000)""".-spec subscribe(server_ref(), [binary()], [message_opt()]) -> message_ret(reference()).subscribe(Server, [_ | _] = Events, Opts) when is_list(Events) -> i3_client_connection:call( Server, {?subscribe, Events}, proplists:get_value(timeout, Opts, ?timeout) ).-doc false.init(_Args) -> {ok, #{ from => undefined, ref => undefined, connected => false, auto_reconnect => false, subscriber_events => #{} }}.-doc false.publish(Type, Payload, State) -> Subscribers = maps:get(Type, maps:get(subscriber_events, State), #{}), maps:foreach( fun(Ref, Pid) -> Pid ! {i3_event, self(), Ref, Type, Payload} end, Subscribers ), ok.-doc false.handle_connect(#{subscriber_events := SubscriberEvents} = State0) -> State1 = State0#{connected := true}, case maps:keys(SubscriberEvents) of [] -> {noreply, State1}; Events -> {send, ?subscribe, Events, State1} end.-doc false.handle_disconnect(State0) -> State1 = State0#{connected := false}, State2 = case State1 of #{auto_reconnect := true, from := {_, _} = From, ref := Ref} when is_reference(Ref) -> i3_client_connection:reply(From, {eventually, Ref}), State1#{from := undefined, ref := undefined}; _ -> State1 end, {noreply, State2}.-doc false.handle_call({?subscribe, Events}, {Pid, _} = From, State0) -> {Ref, NewEventsLazy, State1} = handle_subscribe(State0, Pid, Events), case State1 of #{connected := false} -> i3_client_connection:reply(From, {eventually, Ref}), {noreply, State1}; _ -> case NewEventsLazy() of [] -> i3_client_connection:reply(From, {ok, Ref}), {noreply, State1}; Events -> {send, ?subscribe, Events, State1#{from := From, ref := Ref}} end end;handle_call({Type, Payload}, From, State) -> {send, Type, Payload, State#{from := From}}.-doc false.handle_info({'DOWN', Ref, process, _, _}, State) -> {noreply, handle_unsubscribe(State, Ref)}.-doc false.handle_result(_, #{from := undefined, ref := undefined} = State) -> {noreply, State};handle_result({_, Response}, #{from := From, ref := undefined} = State) -> i3_client_connection:reply(From, Response), {noreply, State#{from := undefined}};% If there's a ref in the state it means we're expecting a result from% a subscription request.handle_result({?subscribe, {ok, _}}, #{from := From, ref := Ref} = State) -> i3_client_connection:reply(From, {ok, Ref}), {noreply, State#{from := undefined, ref := undefined}};handle_result({?subscribe, {error, _} = Error}, #{from := From, ref := Ref} = State0) -> i3_client_connection:reply(From, Error), State1 = handle_unsubscribe(State0, Ref), {noreply, State1#{from := undefined, ref := undefined}}.handle_subscribe(State, Pid, Events) -> Ref = erlang:monitor(process, Pid), Fun = fun(M) -> maps:put(Ref, Pid, M) end, SubscriberEvents1 = lists:foldl( fun(Event, Acc) -> maps:update_with(Event, Fun, #{Ref => Pid}, Acc) end, maps:get(subscriber_events, State), Events ), % It's not cheap and it actually might not be needed so it makes % sense to be lazy. NewEventsThunk = fun() -> maps:fold( fun(Event, RefPidMap, Acc) -> case maps:remove(Ref, RefPidMap) of Map when map_size(Map) =:= 0 -> [Event | Acc]; _ -> Acc end end, [], SubscriberEvents1 ) end, {Ref, NewEventsThunk, State#{subscriber_events := SubscriberEvents1}}.handle_unsubscribe(State, Ref) -> erlang:demonitor(Ref, [flush]), Fun = fun(_, RefPidMap) -> case maps:remove(Ref, RefPidMap) of Map when map_size(Map) =:= 0 -> false; Map -> {true, Map} end end, SubscriberEvents = maps:filtermap(Fun, maps:get(subscriber_events, State)), State#{subscriber_events := SubscriberEvents}.