diff options
author | Thomas Letan <lthms@soap.coffee> | 2022-08-20 00:01:18 +0200 |
---|---|---|
committer | Thomas Letan <lthms@soap.coffee> | 2022-08-21 13:45:33 +0200 |
commit | 7f8430cb34007d71da9b2718027c9e2758ac59ad (patch) | |
tree | 921995e14d5572a4a422e60754164bfc604bf5cb |
Initial commit
38 files changed, 1840 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d6133e2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +_build/ +_opam/
\ No newline at end of file diff --git a/.ocamlformat b/.ocamlformat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/.ocamlformat diff --git a/README.md b/README.md new file mode 100644 index 0000000..3a1d512 --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +# spatial-sway + +`spatial-sway` implements a spatial model inspired by Material Shell +and Paper WM, for Sway. More precisely, it organizes the windows in +your workspaces as if they are on a loop, showing only a fixed number +at a time. + +It is implemented as a daemon, communicating with Sway using your +favorite tiling manager’s IPC protocol (if you are curious, have a +look at `man sway-ipc`!). + +It is missing some features, but `spatial-sway` can already by used +today. Here is an example of a configuration that works. + +``` +set $spatial "/usr/local/bin/spatial" +set $spatialmsg "/usr/local/bin/spatialmsg" + +# Start the daemon when sway is started. +exec $spatialmsg + +# Focus the next window on the left, if the focus is on the last window +# on the left of the visible area, windows will shift right to make room +# for the next candidate on the loop, and the window on the far right +# will disappear. +bindsym $mod+t exec $spatialmsg move_left + +# Same thing, for the right. +bindsym $mod+n exec $spatialmsg move_right + +# Move the focused window on the left, shift the loop if necessary. +bindsym $mod+Shift+t exec $spatialmsg move_window_left + +# Move the focused window on the right, shift the loop if necessary. +bindsym $mod+Shift+n exec $spatialmsg move_window_right + +# Toggle between a mode where only one window is visible, or a fixed +# numbers. spatial-sway will remember how may windows you want visible +# when not in full view mode. +bindsym $mod+space exec $spatialmsg toggle_full_view + +# Decrease the number of windows to display when not in full view mode. +bindsym $mod+g exec $spatialmsg decr_maximum_visible_size + +# Increase the number of windows to display when not in full view mode. +bindsym $mod+h exec $spatialmsg incr_maximum_visible_size +``` diff --git a/bin/spatial/dune b/bin/spatial/dune new file mode 100644 index 0000000..3682cc9 --- /dev/null +++ b/bin/spatial/dune @@ -0,0 +1,3 @@ +(executable + (name main) + (libraries spatial-sway.ipc sway-ipc lwt lwt.unix)) diff --git a/bin/spatial/ipc.ml b/bin/spatial/ipc.ml new file mode 100644 index 0000000..ec00322 --- /dev/null +++ b/bin/spatial/ipc.ml @@ -0,0 +1,27 @@ +open Spatial_sway_ipc + +let rec socket_handler server () = + let open Lwt.Syntax in + let* socket, _ = Lwt_unix.accept server in + let buffer = Bytes.create 4 in + try + let* read_bytes = Lwt_unix.read socket buffer 0 4 in + assert (read_bytes = 4); + let code = Bytes.get_int32_ne buffer 0 in + let* () = Lwt_unix.close socket in + Lwt.return @@ Some (of_int32_exn code) + with _ -> + let* () = Lwt_unix.close socket in + socket_handler server () + +let create_server () = + let open Lwt.Syntax in + let socket = Lwt_unix.socket PF_UNIX SOCK_STREAM 0 in + let* socket_exists = Lwt_unix.file_exists socket_path in + let* () = + if socket_exists then Lwt_unix.unlink socket_path else Lwt.return () + in + let sockaddr = Lwt_unix.ADDR_UNIX socket_path in + let+ () = Lwt_unix.bind socket sockaddr in + let () = Lwt_unix.listen socket 100 in + Lwt_stream.from (socket_handler socket) diff --git a/bin/spatial/main.ml b/bin/spatial/main.ml new file mode 100644 index 0000000..da99f2b --- /dev/null +++ b/bin/spatial/main.ml @@ -0,0 +1,82 @@ +open Sway_ipc_types + +type input = From_sway of Event.t | From_client of Spatial_sway_ipc.t + +let workspace_handle (ev : Event.workspace_event) state = + match ev.change with + | Focus -> + let state = + match ev.current.name with + | Some workspace -> State.set_current_workspace workspace state + | None -> state + in + Lwt.return (state, true) + | Init | Empty | Move | Rename | Urgent | Reload -> Lwt.return (state, false) + +let window_handle (ev : Event.window_event) state = + match ev.change with + | Event.New -> + let state = + State.register_window false 2 state.State.current_workspace state + ev.container + in + Lwt.return (state, true) + | Event.Close -> + let state = State.unregister_window state ev.container.id in + Lwt.return (state, true) + | Event.Focus | Event.Title | Event.Fullscreen_mode | Event.Move | Event.Mark + | Event.Urgent -> + Lwt.return (state, false) + | Event.Floating -> + (* TODO: disable spatial-sway for the concerned workspace *) + Lwt.return (state, false) + +let event_handle ev state = + let open Lwt.Syntax in + Lwt.try_bind + (fun () -> + let* state, arrange = + match ev with + | From_sway (Event.Workspace ev) -> workspace_handle ev state + | From_sway (Window ev) -> window_handle ev state + | From_client ev -> State.client_handle ev state + | _ -> assert false + in + let+ () = + if arrange then State.arrange_current_workspace state else Lwt.return () + in + state) + Lwt.return + (fun _exn -> + let+ _ = Lwt_io.printf "something went wrong with an event\n" in + state) + +let merge_streams l = + let open Lwt.Syntax in + Lwt_stream.from (fun () -> + Lwt.choose + (List.map + (fun x -> + let+ x = Lwt_stream.next x in + Some x) + l)) + +let main () = + let open Lwt.Syntax in + let* stream_sway = Sway_ipc.subscribe [ Window; Workspace ] in + let* stream_client = Ipc.create_server () in + let stream = + merge_streams + [ + Lwt_stream.map (fun x -> From_sway x) stream_sway; + Lwt_stream.map (fun x -> From_client x) stream_client; + ] + in + let* state = State.init false 2 in + let* () = State.arrange_current_workspace state in + let string = Format.asprintf "%a" State.pp state in + let* () = Lwt_io.printf "%s\n" string in + let* _ = Lwt_stream.fold_s event_handle stream state in + Lwt.return () + +let () = Lwt_main.run @@ main () diff --git a/bin/spatial/pp_helpers.ml b/bin/spatial/pp_helpers.ml new file mode 100644 index 0000000..6104c29 --- /dev/null +++ b/bin/spatial/pp_helpers.ml @@ -0,0 +1,7 @@ +let pp_windows_seq fmt l = + Format.( + fprintf fmt "%a" + (pp_print_list + ~pp_sep:(fun fmt () -> pp_print_string fmt " ") + pp_print_int) + (List.map Int64.to_int l)) diff --git a/bin/spatial/ribbon.ml b/bin/spatial/ribbon.ml new file mode 100644 index 0000000..02cf889 --- /dev/null +++ b/bin/spatial/ribbon.ml @@ -0,0 +1,320 @@ +open Sway_ipc_types + +type t = { + full_view : bool; + maximum_visible_size : int; + visible : (int * int64 list) option; + hidden : int64 list; +} + +let empty full_view maximum_visible_size = + assert (0 < maximum_visible_size); + { full_view; maximum_visible_size; visible = None; hidden = [] } + +let visible_windows_count = function + | { visible = None; _ } -> 0 + | { visible = Some (f, l); _ } -> + assert (0 <= f && f < List.length l); + List.length l + +let rec insert_at n a = function + | l when n = 0 -> a :: l + | x :: rst when 0 < n -> x :: insert_at (n - 1) a rst + | _ -> raise (Invalid_argument "insert_at") + +let rec remove_at n = function + | _ :: rst when n = 0 -> rst + | x :: rst when 0 < n -> x :: remove_at (n - 1) rst + | _ -> raise (Invalid_argument "remove_at") + +let push_front x l = x :: l +let pop_front = function x :: l -> Some (x, l) | [] -> None + +let pop_front_exn l = + match pop_front l with + | Some res -> res + | None -> raise (Invalid_argument "pop_front_exn") + +let push_back x l = List.rev l |> push_front x |> List.rev + +let pop_back l = + List.rev l |> pop_front |> Option.map (fun (x, l) -> (x, List.rev l)) + +let pop_back_exn l = + match pop_back l with + | Some res -> res + | None -> raise (Invalid_argument "pop_back_exn") + +let shrink_left ribbon = + match ribbon.visible with + | Some (0, _) when ribbon.maximum_visible_size < visible_windows_count ribbon + -> + raise (Invalid_argument "shrink_left") + | Some (f, l) when ribbon.maximum_visible_size < visible_windows_count ribbon + -> + let w, l = pop_front_exn l in + { + ribbon with + visible = Some (f - 1, l); + hidden = push_back w ribbon.hidden; + } + | _ -> ribbon + +let shrink_right ribbon = + match ribbon.visible with + | Some (f, l) + when ribbon.maximum_visible_size < visible_windows_count ribbon + && f + 1 = List.length l -> + raise (Invalid_argument "shrink_right") + | Some (f, l) when ribbon.maximum_visible_size < visible_windows_count ribbon + -> + let w, l = pop_back_exn l in + { ribbon with visible = Some (f, l); hidden = push_front w ribbon.hidden } + | _ -> ribbon + +let shrink ribbon = + match ribbon.visible with + | Some (f, _) when 0 < f -> shrink_left ribbon + | Some (f, l) when f < List.length l - 1 -> shrink_right ribbon + | _ -> ribbon + +let insert_window window ribbon = + match ribbon.visible with + | None -> { ribbon with visible = Some (0, [ window ]) } + | Some (f, l) -> + let f = f + 1 in + let ribbon = { ribbon with visible = Some (f, insert_at f window l) } in + if f < ribbon.maximum_visible_size then shrink_right ribbon + else shrink_left ribbon + +let remove window = + let rec remove idx acc = function + | x :: rst when window = x -> Some (idx, List.rev acc @ rst) + | x :: rst -> remove (idx + 1) (x :: acc) rst + | [] -> None + in + remove 0 [] + +let remove_if_present window = + let rec remove acc = function + | x :: rst when window = x -> List.rev acc @ rst + | x :: rst -> remove (x :: acc) rst + | [] -> List.rev acc + in + remove [] + +let fill_space ribbon = + if visible_windows_count ribbon < ribbon.maximum_visible_size then + match (pop_back ribbon.hidden, ribbon.visible) with + | Some (x, hidden), Some (f, l) -> + { ribbon with visible = Some (f + 1, push_front x l); hidden } + | Some (x, hidden), None -> + { ribbon with visible = Some (0, [ x ]); hidden } + | None, _ -> ribbon + else ribbon + +let incr_maximum_visible ribbon = + fill_space + { ribbon with maximum_visible_size = ribbon.maximum_visible_size + 1 } + +let decr_maximum_visible ribbon = + if 2 < ribbon.maximum_visible_size then + shrink + { ribbon with maximum_visible_size = ribbon.maximum_visible_size - 1 } + else ribbon + +let remove_window window ribbon = + fill_space + @@ + match ribbon.visible with + | Some (f, l) -> ( + match remove window l with + | Some (idx, l) -> + let f' = + if f < idx then f + else if f = idx && f < List.length l then f + else f - 1 + in + { ribbon with visible = Some (f', l) } + | None -> { ribbon with hidden = remove_if_present window ribbon.hidden }) + | None -> { ribbon with hidden = remove_if_present window ribbon.hidden } + +let toggle_full_view ribbon = { ribbon with full_view = not ribbon.full_view } + +let move_focus_left ribbon = + match ribbon.visible with + | None -> ribbon + | Some (0, l) -> ( + match pop_back ribbon.hidden with + | Some (x, hidden) -> + shrink_right + { ribbon with visible = Some (0, push_front x l); hidden } + | None -> + let x, l = pop_back_exn l in + { ribbon with visible = Some (0, push_front x l) }) + | Some (f, l) -> { ribbon with visible = Some (f - 1, l) } + +let move_focus_right ribbon = + match ribbon.visible with + | None -> ribbon + | Some (f, l) when f < List.length l - 1 -> + { ribbon with visible = Some (f + 1, l) } + | Some (f, l) -> ( + match pop_front ribbon.hidden with + | Some (x, hidden) -> + shrink_left + { ribbon with visible = Some (f + 1, push_back x l); hidden } + | None -> + let x, l = pop_front_exn l in + { ribbon with visible = Some (f, push_back x l) }) + +let split_visible ribbon = + let rec split_visible acc f = function + | x :: rst when f = 0 -> Some (List.rev acc, x, rst) + | x :: rst -> split_visible (x :: acc) (f - 1) rst + | [] -> None + in + match ribbon.visible with None -> None | Some (f, l) -> split_visible [] f l + +let move_window_left ribbon = + match split_visible ribbon with + | Some (left, focus, right) -> ( + match pop_back left with + | Some (x, left) -> + (* Case: [|a b {f} ..| ..] + Result: [|a {f} b ..| ..] *) + { + ribbon with + visible = + Some (List.length left, left @ [ focus ] @ push_front x right); + } + | None -> ( + (* Case: [|{f} ..| ..] *) + match pop_back ribbon.hidden with + | Some (x, hidden) -> + (* Case: [|{f} ..| a b] *) + (* Result: [|{f} b ..| a] *) + { + ribbon with + visible = Some (0, focus :: push_front x right); + hidden; + } + |> shrink_right + | None -> ( + (* Case: [|{f} ..|] *) + match pop_back right with + | Some (x, right) -> + (* Case: [|{f} a b c|] + Result: [|{f} c a b|] *) + { + ribbon with + visible = Some (0, focus :: push_front x right); + } + | None -> + (* Case: [|{f}|] + Result: [|{f}|] *) + ribbon))) + (* Case: [||] + Result: [||] *) + | None -> ribbon + +let move_window_right ribbon = + match split_visible ribbon with + | Some (left, focus, right) -> ( + let f = List.length left + 1 in + match pop_front right with + | Some (x, right) -> + (* Case: [|.. {f} a b| ..] + Result: [|.. a {f} b| ..] *) + { + ribbon with + visible = Some (f, push_back x left @ [ focus ] @ right); + } + | None -> ( + (* Case: [|.. {f}| ..] *) + match pop_front ribbon.hidden with + | Some (x, hidden) -> + (* Case: [|.. {f}| a b] *) + (* Result: [|.. a {f}| b] *) + { + ribbon with + visible = Some (f, push_back x left @ [ focus ] @ right); + hidden; + } + |> shrink_left + | None -> ( + (* Case: [|.. {f}|] *) + match pop_front left with + | Some (x, left) -> + (* Case: [|a b {f}|] + Result: [|b a {f}|] *) + { + ribbon with + visible = Some (f, push_back x left @ [ focus ] @ right); + } + | None -> + (* Case: [|{f}|] + Result: [|{f}|] *) + ribbon))) + (* Case: [||] + Result: [||] *) + | None -> ribbon + +let hide_window_command window = + let target = Format.sprintf "*%Ld*" window in + Command.With_criteria (Con_id window, Move_container target) + +let show_window_command workspace window = + [ + Command.With_criteria (Con_id window, Move_container workspace); + Command.With_criteria (Con_id window, Focus); + ] + +let visible_windows ribbon = + match ribbon.visible with + | Some (f, l) when ribbon.full_view -> [ List.nth l f ] + | Some (_, l) -> l + | None -> [] + +let all_windows ribbon = + ribbon.hidden @ visible_windows { ribbon with full_view = false } + +let focused_window ribbon = + match ribbon.visible with Some (f, l) -> List.nth_opt l f | None -> None + +let hide_all_windows_commands ribbon = + List.map hide_window_command @@ all_windows ribbon + +let show_visible_windows_commands workspace ribbon = + match ribbon.visible with + | Some (f, l) when ribbon.full_view -> + show_window_command workspace (List.nth l f) + | Some (_, l) -> List.concat_map (show_window_command workspace) l + | None -> [] + +let focus_command ribbon = + List.concat_map + (fun x -> + [ + Command.With_criteria (Con_id x, Focus); + With_criteria (Con_id x, Opacity 1.0); + ]) + (Option.to_list @@ focused_window ribbon) + +let arrange_commands ~focus workspace ribbon = + let hide_commands = hide_all_windows_commands ribbon in + let show_commands = show_visible_windows_commands workspace ribbon in + let focus_command = + match ribbon.visible with + | Some (f, l) when focus -> + [ Command.With_criteria (Con_id (List.nth l f), Focus) ] + | _ -> [] + in + hide_commands @ show_commands @ focus_command + +let pp fmt ribbon = + match split_visible ribbon with + | None -> Format.fprintf fmt "[]" + | Some (left, x, right) -> + Format.fprintf fmt "[|%a{%Ld}%a|%a]" Pp_helpers.pp_windows_seq left x + Pp_helpers.pp_windows_seq right Pp_helpers.pp_windows_seq ribbon.hidden diff --git a/bin/spatial/state.ml b/bin/spatial/state.ml new file mode 100644 index 0000000..d967d16 --- /dev/null +++ b/bin/spatial/state.ml @@ -0,0 +1,171 @@ +open Sway_ipc_types + +type state = { + current_workspace : string; + windows : Windows_registry.t; + workspaces : Workspaces_registry.t; +} + +let empty current_workspace = + { + current_workspace; + windows = Windows_registry.empty; + workspaces = Workspaces_registry.empty; + } + +let set_current_workspace current_workspace state = + { state with current_workspace } + +let insert_window default_full_view default_maximum_visible workspace window + app_id state = + { + state with + workspaces = + Workspaces_registry.register_window default_full_view + default_maximum_visible workspace window state.workspaces; + windows = + Windows_registry.register window { app_id; workspace } state.windows; + } + +let toggle_full_view workspace state = + { + state with + workspaces = + Workspaces_registry.update workspace + (function + | Some ribbon -> Some (Ribbon.toggle_full_view ribbon) | None -> None) + state.workspaces; + } + +let move_window_right workspace state = + { + state with + workspaces = + Workspaces_registry.update workspace + (function + | Some ribbon -> Some (Ribbon.move_window_right ribbon) | None -> None) + state.workspaces; + } + +let move_window_left workspace state = + { + state with + workspaces = + Workspaces_registry.update workspace + (function + | Some ribbon -> Some (Ribbon.move_window_left ribbon) | None -> None) + state.workspaces; + } + +let incr_maximum_visible_size workspace state = + { + state with + workspaces = + Workspaces_registry.update workspace + (function + | Some ribbon -> Some (Ribbon.incr_maximum_visible ribbon) + | None -> None) + state.workspaces; + } + +let decr_maximum_visible_size workspace state = + { + state with + workspaces = + Workspaces_registry.update workspace + (function + | Some ribbon -> Some (Ribbon.decr_maximum_visible ribbon) + | None -> None) + state.workspaces; + } + +let arrange_workspace_commands ~focus workspace state = + match Workspaces_registry.find_opt workspace state.workspaces with + | Some ribbon -> Ribbon.arrange_commands ~focus workspace ribbon + | None -> [] + +let arrange_workspace ~focus ~socket workspace state = + let open Lwt.Syntax in + let cmds = arrange_workspace_commands ~focus workspace state in + let* _replies = Sway_ipc.send_command ~socket (Run_command cmds) in + Lwt.return () + +let arrange_current_workspace state = + Sway_ipc.wtih_socket (fun socket -> + arrange_workspace ~focus:true ~socket state.current_workspace state) + +let register_window default_full_view default_maximum_visible workspace state + (tree : Node.t) = + match tree.app_id with + | Some app_id -> + insert_window default_full_view default_maximum_visible workspace tree.id + app_id state + | _ -> state + +let unregister_window state window = + let info = Windows_registry.find window state.windows in + let windows = Windows_registry.unregister window state.windows in + let workspaces = + Workspaces_registry.update info.workspace + (function + | Some ribbon -> Some (Ribbon.remove_window window ribbon) + | None -> None) + state.workspaces + in + { state with windows; workspaces } + +let init default_full_view default_maximum_visible = + let open Lwt.Syntax in + let* cw = Sway_ipc.get_current_workspace () in + let+ tree = Sway_ipc.get_tree () in + let workspaces = Node.filter (fun x -> x.node_type = Workspace) tree in + List.fold_left + (fun state workspace -> + match workspace.Node.name with + | Some workspace_name -> + Node.fold state + (register_window default_full_view default_maximum_visible + workspace_name) + workspace + | None -> state) + (empty cw.name) workspaces + +let client_handle ev state = + Lwt.return + @@ + match ev with + | Spatial_sway_ipc.Move_left -> + ( { + state with + workspaces = + Workspaces_registry.update state.current_workspace + (function + | Some ribbon -> Some (Ribbon.move_focus_left ribbon) + | None -> None) + state.workspaces; + }, + true ) + | Move_right -> + ( { + state with + workspaces = + Workspaces_registry.update state.current_workspace + (function + | Some ribbon -> Some (Ribbon.move_focus_right ribbon) + | None -> None) + state.workspaces; + }, + true ) + | Move_window_left -> (move_window_left state.current_workspace state, true) + | Move_window_right -> (move_window_right state.current_workspace state, true) + | Toggle_full_view -> (toggle_full_view state.current_workspace state, true) + | Incr_maximum_visible_space -> + (incr_maximum_visible_size state.current_workspace state, true) + | Decr_maximum_visible_space -> + (decr_maximum_visible_size state.current_workspace state, true) + +let pp fmt state = + Format.( + fprintf fmt "current_workspace: %s@ windows: %a@ workspaces: %a" + state.current_workspace Windows_registry.pp state.windows + Workspaces_registry.pp state.workspaces) diff --git a/bin/spatial/windows_registry.ml b/bin/spatial/windows_registry.ml new file mode 100644 index 0000000..55dddb3 --- /dev/null +++ b/bin/spatial/windows_registry.ml @@ -0,0 +1,19 @@ +module Map = Map.Make (Int64) + +type info = { workspace : string; app_id : string } +type t = info Map.t + +let empty : t = Map.empty +let register : int64 -> info -> t -> t = Map.add +let unregister = Map.remove +let find = Map.find + +let pp_window fmt (id, { app_id; workspace }) = + Format.fprintf fmt "{ id = %Ld; app_id = %s; workspace = %s }" id app_id + workspace + +let pp fmt windows = + let open Format in + fprintf fmt "%a" + (pp_print_list ~pp_sep:(fun fmt () -> pp_print_string fmt ", ") pp_window) + (Map.to_seq windows |> List.of_seq) diff --git a/bin/spatial/workspaces_registry.ml b/bin/spatial/workspaces_registry.ml new file mode 100644 index 0000000..5e8c94d --- /dev/null +++ b/bin/spatial/workspaces_registry.ml @@ -0,0 +1,31 @@ +module Map = Map.Make (String) + +type t = Ribbon.t Map.t + +let empty : t = Map.empty + +let register_window default_full_view default_maximum_visible workspace window + reg = + Map.update workspace + (fun workspace -> + let ribbon = + Option.value + ~default:(Ribbon.empty default_full_view default_maximum_visible) + workspace + in + Some (Ribbon.insert_window window ribbon)) + reg + +let unregister = Map.remove +let find = Map.find +let find_opt = Map.find_opt +let update = Map.update +let get_list reg = reg |> Map.to_seq |> Seq.map fst |> List.of_seq + +let pp fmt workspaces = + let open Format in + fprintf fmt "%a" + (pp_print_list + ~pp_sep:(fun fmt () -> pp_print_string fmt ", ") + (fun fmt (x, r) -> fprintf fmt "%s: %a" x Ribbon.pp r)) + (Map.to_seq workspaces |> List.of_seq) diff --git a/bin/spatialmsg/dune b/bin/spatialmsg/dune new file mode 100644 index 0000000..334a1c0 --- /dev/null +++ b/bin/spatialmsg/dune @@ -0,0 +1,3 @@ +(executable + (name main) + (libraries spatial-sway.ipc lwt lwt.unix)) diff --git a/bin/spatialmsg/main.ml b/bin/spatialmsg/main.ml new file mode 100644 index 0000000..a1611c4 --- /dev/null +++ b/bin/spatialmsg/main.ml @@ -0,0 +1,31 @@ +let connect () = + let open Lwt.Syntax in + let socket = Lwt_unix.socket PF_UNIX SOCK_STREAM 0 in + let+ () = Lwt_unix.connect socket (ADDR_UNIX Spatial_sway_ipc.socket_path) in + socket + +let send_command socket cmd = + let open Lwt.Syntax in + let buffer = Bytes.create 4 in + Bytes.set_int32_ne buffer 0 (Spatial_sway_ipc.to_int32 cmd); + let* _ = Lwt_unix.write socket buffer 0 4 in + Lwt.return () + +let main () = + let open Lwt.Syntax in + let cmd = + match Sys.argv.(1) with + | "move_left" -> Spatial_sway_ipc.Move_left + | "move_right" -> Move_right + | "move_window_left" -> Move_window_left + | "move_window_right" -> Move_window_right + | "toggle_full_view" -> Toggle_full_view + | "incr_maximum_visible_size" -> Incr_maximum_visible_space + | "decr_maximum_visible_size" -> Decr_maximum_visible_space + | _ -> raise (Invalid_argument "bad command") + in + let* socket = connect () in + let* () = send_command socket cmd in + Lwt_unix.close socket + +let () = Lwt_main.run @@ main () diff --git a/dune-project b/dune-project new file mode 100644 index 0000000..ef5a428 --- /dev/null +++ b/dune-project @@ -0,0 +1 @@ +(lang dune 3.0)
\ No newline at end of file diff --git a/json-decoder.opam b/json-decoder.opam new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/json-decoder.opam diff --git a/lib/json_decoder/dune b/lib/json_decoder/dune new file mode 100644 index 0000000..8aa9198 --- /dev/null +++ b/lib/json_decoder/dune @@ -0,0 +1,4 @@ +(library + (name json_decoder) + (public_name json-decoder) + (libraries ezjsonm)) diff --git a/lib/json_decoder/json_decoder.ml b/lib/json_decoder/json_decoder.ml new file mode 100644 index 0000000..b7b9c57 --- /dev/null +++ b/lib/json_decoder/json_decoder.ml @@ -0,0 +1,48 @@ +type 'a t = Ezjsonm.value -> 'a + +module Syntax = struct + let ( let+ ) dec f value = f (dec value) + + let ( and+ ) x y value = + let x = x value in + let y = y value in + (x, y) + + let ( let* ) dec k value = + let x = dec value in + k x + + let return x _value = x +end + +let field str enc value = + try enc (Ezjsonm.find value [ str ]) with + | Not_found -> failwith (str ^ " not found") + | exn -> + Format.printf "%s\n" str; + raise exn + +let field_opt str enc value = + try + match Ezjsonm.find_opt value [ str ] with + | Some x -> Some (enc x) + | None -> None + with _exn -> None + +let list enc = Ezjsonm.get_list enc +let string = Ezjsonm.get_string +let int64 = Ezjsonm.get_int64 + +let string_enum l = + let open Syntax in + let+ constant = string in + try List.assoc constant l + with Not_found -> failwith (constant ^ " not a correct value") + +let bool = Ezjsonm.get_bool +let float = Ezjsonm.get_float + +let rec mu : ('a t -> 'a t) -> 'a t = + fun f_enc value -> (f_enc (mu f_enc)) value + +let of_string dec str = Ezjsonm.value_from_string str |> dec diff --git a/lib/spatial_sway_ipc/dune b/lib/spatial_sway_ipc/dune new file mode 100644 index 0000000..4a95cc7 --- /dev/null +++ b/lib/spatial_sway_ipc/dune @@ -0,0 +1,3 @@ +(library + (name spatial_sway_ipc) + (public_name spatial-sway.ipc)) diff --git a/lib/spatial_sway_ipc/spatial_sway_ipc.ml b/lib/spatial_sway_ipc/spatial_sway_ipc.ml new file mode 100644 index 0000000..07303f3 --- /dev/null +++ b/lib/spatial_sway_ipc/spatial_sway_ipc.ml @@ -0,0 +1,34 @@ +let socket_path = "/tmp/spatial-sway.socket" + +type t = + | Move_left + | Move_right + | Move_window_left + | Move_window_right + | Toggle_full_view + | Incr_maximum_visible_space + | Decr_maximum_visible_space + +let to_int32 = function + | Move_left -> 0l + | Move_right -> 1l + | Move_window_left -> 2l + | Move_window_right -> 3l + | Toggle_full_view -> 4l + | Incr_maximum_visible_space -> 5l + | Decr_maximum_visible_space -> 6l + +let of_int32 = function + | 0l -> Some Move_left + | 1l -> Some Move_right + | 2l -> Some Move_window_left + | 3l -> Some Move_window_right + | 4l -> Some Toggle_full_view + | 5l -> Some Incr_maximum_visible_space + | 6l -> Some Decr_maximum_visible_space + | _ -> None + +let of_int32_exn i = + match of_int32 i with + | Some res -> res + | None -> raise (Invalid_argument "Spatial_sway_ipc.of_int32_exn") diff --git a/lib/sway_ipc/dune b/lib/sway_ipc/dune new file mode 100644 index 0000000..77ad160 --- /dev/null +++ b/lib/sway_ipc/dune @@ -0,0 +1,4 @@ +(library + (name sway_ipc) + (public_name sway-ipc) + (libraries json-decoder sway-ipc.types lwt lwt.unix)) diff --git a/lib/sway_ipc/socket.ml b/lib/sway_ipc/socket.ml new file mode 100644 index 0000000..bca1b05 --- /dev/null +++ b/lib/sway_ipc/socket.ml @@ -0,0 +1,46 @@ +open Sway_ipc_types + +type socket = Lwt_io.input_channel * Lwt_io.output_channel + +let sway_sock_path () = + match Sys.getenv_opt "SWAYSOCK" with + | Some path -> path + | None -> failwith "SWAYSOCK environment variable is missing" + +let read_magic_string (socket, _, _) = + let open Lwt.Syntax in + let magic = Raw_message.magic_string in + let* msg = Lwt_io.read ~count:(String.length magic) socket in + assert (msg = magic); + Lwt.return () + +let write_raw_message (_, socket, _) raw = + let msg = Raw_message.to_string raw in + Lwt_io.write socket msg + +let rec read_all ~count ((socket, _, _) as s) = + let open Lwt.Syntax in + let* payload = Lwt_io.read ~count socket in + if String.length payload = count then Lwt.return payload + else + let+ rest = read_all ~count:(count - String.length payload) s in + payload ^ rest + +let read_raw_message ((socket, _, _) as s) = + let open Lwt.Syntax in + let* () = read_magic_string s in + let* msg = Lwt_io.read ~count:4 socket in + let size = Raw_message.string_to_int32 msg in + let* msg = Lwt_io.read ~count:4 socket in + let msg_type = Raw_message.string_to_int32 msg in + let* payload = read_all ~count:(Int32.to_int size) s in + Lwt.return (msg_type, payload) + +let rec read_next_event s evs = + let open Lwt.Syntax in + let* ((opc, _) as raw) = read_raw_message s in + if List.exists (fun x -> opc = Sway_ipc_types.Event.event_type_code x) evs + then Lwt.return (Event.event_of_raw_message raw) + else read_next_event s evs + +let close (_, _, s) = Lwt_unix.close s diff --git a/lib/sway_ipc/sway_ipc.ml b/lib/sway_ipc/sway_ipc.ml new file mode 100644 index 0000000..ccf0956 --- /dev/null +++ b/lib/sway_ipc/sway_ipc.ml @@ -0,0 +1,48 @@ +open Sway_ipc_types + +let connect () = + let open Lwt.Syntax in + let socket = Lwt_unix.socket PF_UNIX SOCK_STREAM 0 in + let+ () = Lwt_unix.connect socket (ADDR_UNIX (Socket.sway_sock_path ())) in + let socket_in = Lwt_io.of_fd ~mode:Input socket in + let socket_out = Lwt_io.of_fd ~mode:Output socket in + (socket_in, socket_out, socket) + +let close socket = Socket.close socket + +let wtih_socket f = + let open Lwt.Syntax in + let* socket = connect () in + let* res = f socket in + let+ () = Socket.close socket in + res + +let socket_from_option = function + | Some socket -> Lwt.return socket + | None -> connect () + +let send_command ?socket cmd = + let open Lwt.Syntax in + let* socket = socket_from_option socket in + let ((op, _) as raw) = Message.to_raw_message cmd in + let* () = Socket.write_raw_message socket raw in + let* op', payload = Socket.read_raw_message socket in + assert (op = op'); + Lwt.return @@ Json_decoder.of_string (Message.reply_decoder cmd) payload + +let subscribe ?socket events = + let open Lwt.Syntax in + let* socket = socket_from_option socket in + let+ { success } = send_command ~socket (Subscribe events) in + if success then + Lwt_stream.from (fun () -> + let+ ev = Socket.read_next_event socket events in + Some ev) + else failwith "Something went wrong" + +let get_tree ?socket () = send_command ?socket Get_tree + +let get_current_workspace ?socket () = + let open Lwt.Syntax in + let+ workspaces = send_command ?socket Get_workspaces in + List.find (fun w -> w.Workspace.focused) workspaces diff --git a/lib/sway_ipc_types/command.ml b/lib/sway_ipc_types/command.ml new file mode 100644 index 0000000..5c02d63 --- /dev/null +++ b/lib/sway_ipc_types/command.ml @@ -0,0 +1,40 @@ +open Format + +type direction = Up | Right | Down | Left + +let pp_direction fmt direction = + pp_print_string fmt + @@ + match direction with + | Up -> "up" + | Right -> "right" + | Down -> "down" + | Left -> "left" + +type with_criteria = Move_container of string | Focus | Opacity of float +type criteria = Con_id of int64 | Focused + +type t = + | With_criteria of criteria * with_criteria + | Focus of direction + | Focus_output of direction + | Focus_output_by_name of string + | Workspace of string + +let pp_criteria fmt = function + | Con_id i -> fprintf fmt "[con_id=%Ld]" i + | Focused -> () + +let pp_with_criteria fmt = function + | Move_container string -> fprintf fmt "move container to workspace %s" string + | Focus -> fprintf fmt "focus" + | Opacity value -> fprintf fmt "opacity set %f" value + +let pp fmt = function + | With_criteria (crit, cmd) -> + fprintf fmt "%a %a" pp_criteria crit pp_with_criteria cmd + | Focus direction -> fprintf fmt "focus %a" pp_direction direction + | Focus_output direction -> + fprintf fmt "focus output %a" pp_direction direction + | Focus_output_by_name name -> fprintf fmt "focus output %s" name + | Workspace name -> fprintf fmt "workspace %s" name diff --git a/lib/sway_ipc_types/dune b/lib/sway_ipc_types/dune new file mode 100644 index 0000000..5e162b7 --- /dev/null +++ b/lib/sway_ipc_types/dune @@ -0,0 +1,4 @@ +(library + (name sway_ipc_types) + (public_name sway-ipc.types) + (libraries json-decoder)) diff --git a/lib/sway_ipc_types/event.ml b/lib/sway_ipc_types/event.ml new file mode 100644 index 0000000..1176a29 --- /dev/null +++ b/lib/sway_ipc_types/event.ml @@ -0,0 +1,163 @@ +type event_type = + | Workspace + | Mode + | Window + | Barconfig_update + | Binding + | Shutdown + | Tick + | Bar_state_update + | Input + +let event_type_code = function + | Workspace -> 0x80000000l + | Mode -> 0x80000002l + | Window -> 0x80000003l + | Barconfig_update -> 0x80000004l + | Binding -> 0x80000005l + | Shutdown -> 0x80000006l + | Tick -> 0x80000007l + | Bar_state_update -> 0x80000014l + | Input -> 0x80000015l + +let event_type_of_code = function + | 0x80000000l -> Workspace + | 0x80000002l -> Mode + | 0x80000003l -> Window + | 0x80000004l -> Barconfig_update + | 0x80000005l -> Binding + | 0x80000006l -> Shutdown + | 0x80000007l -> Tick + | 0x80000014l -> Bar_state_update + | 0x80000015l -> Input + | _ -> raise (Invalid_argument "event_type_of_code") + +let event_type_decoder = + Json_decoder.string_enum + [ + ("workspace", Workspace); + ("mode", Mode); + ("window", Window); + ("barconfig_update", Barconfig_update); + ("binding", Binding); + ("shutdown", Shutdown); + ("tick", Tick); + ("bar_state_update", Bar_state_update); + ("input", Input); + ] + +let event_type_string = function + | Workspace -> "workspace" + | Mode -> "mode" + | Window -> "window" + | Barconfig_update -> "barconfig_update" + | Binding -> "binding" + | Shutdown -> "shutdown" + | Tick -> "tick" + | Bar_state_update -> "bar_state_update" + | Input -> "input" + +type workspace_change = Init | Empty | Focus | Move | Rename | Urgent | Reload + +let workspace_change_decoder = + Json_decoder.string_enum + [ + ("init", Init); + ("empty", Empty); + ("focus", Focus); + ("move", Move); + ("rename", Rename); + ("urgent", Urgent); + ("reload", Reload); + ] + +type workspace_event = { + change : workspace_change; + current : Node.t; + old : Node.t option; +} + +let workspace_event_decoder = + let open Json_decoder in + let open Syntax in + let+ change = field "change" workspace_change_decoder + and+ current = field "current" Node.decoder + and+ old = field_opt "old" Node.decoder in + { change; current; old } + +type mode_event = { change : string; pango_markup : bool } + +let mode_event_decoder = + let open Json_decoder in + let open Syntax in + let+ change = field "change" string + and+ pango_markup = field "pango_markup" bool in + { change; pango_markup } + +type window_change = + | New + | Close + | Focus + | Title + | Fullscreen_mode + | Move + | Floating + | Urgent + | Mark + +let window_change_decoder = + Json_decoder.string_enum + [ + ("new", New); + ("close", Close); + ("focus", Focus); + ("title", Title); + ("fullscreen_mode", Fullscreen_mode); + ("move", Move); + ("floating", Floating); + ("urgent", Urgent); + ("mark", Mark); + ] + +type window_event = { change : window_change; container : Node.t } + +let window_event_decoder = + let open Json_decoder in + let open Syntax in + let+ change = field "change" window_change_decoder + and+ container = field "container" Node.decoder in + { change; container } + +type tick_event = { first : bool; payload : string } + +let tick_event_decoder = + let open Json_decoder in + let open Syntax in + let+ first = field "first" bool and+ payload = field "payload" string in + { first; payload } + +type t = + | Workspace of workspace_event + | Mode of mode_event + | Window of window_event + +type event = t + +let decoder (code : event_type) = + let open Json_decoder in + let open Syntax in + match code with + | Workspace -> + let+ ev = workspace_event_decoder in + Workspace ev + | Mode -> + let+ ev = mode_event_decoder in + Mode ev + | Window -> + let+ ev = window_event_decoder in + Window ev + | _ -> assert false + +let event_of_raw_message (opc, payload) = + let ev = event_type_of_code opc in + Json_decoder.of_string (decoder ev) payload diff --git a/lib/sway_ipc_types/input_device.ml b/lib/sway_ipc_types/input_device.ml new file mode 100644 index 0000000..b76e6fe --- /dev/null +++ b/lib/sway_ipc_types/input_device.ml @@ -0,0 +1,59 @@ +type input_type = + | Keyboard + | Pointer + | Touch + | Tablet_tool + | Tablet_pad + | Switch + +let input_type_decoder = + Json_decoder.string_enum + [ + ("keyboard", Keyboard); + ("pointer", Pointer); + ("touch", Touch); + ("tablet_tool", Tablet_tool); + ("tablet_pad", Tablet_pad); + ("switch", Switch); + ] + +type t = { + identifier : string; + name : string; + vendor : int64; + product : int64; + input_type : input_type; + xkb_active_layout_name : string option; + xkb_layout_names : string list option; + xkb_active_layout_index : int64 option; + scroll_factor : float option; + libinput : Libinput.t option; +} + +let decoder = + let open Json_decoder in + let open Syntax in + let+ identifier = field "identifier" string + and+ name = field "name" string + and+ vendor = field "vendor" int64 + and+ product = field "product" int64 + and+ input_type = field "input_type" input_type_decoder + and+ xkb_active_layout_name = field_opt "xkb_active_layout_name" string + and+ xkb_layout_names = field_opt "xkb_active_layout_name" (list string) + and+ xkb_active_layout_index = field_opt "xkb_active_layout_index" int64 + and+ scroll_factor = field_opt "scroll_factor" float + and+ libinput = field_opt "libinput" Libinput.decoder in + { + identifier; + name; + vendor; + product; + input_type; + xkb_active_layout_name; + xkb_layout_names; + xkb_active_layout_index; + scroll_factor; + libinput; + } + +type input_device = t diff --git a/lib/sway_ipc_types/libinput.ml b/lib/sway_ipc_types/libinput.ml new file mode 100644 index 0000000..35d702d --- /dev/null +++ b/lib/sway_ipc_types/libinput.ml @@ -0,0 +1,159 @@ +type send_events = Enabled | Disabled | Disabled_on_external_mouse | Unknown + +let send_events_decoder = + Json_decoder.string_enum + [ + ("enabled", Enabled); + ("disabled", Disabled); + ("disabled_on_external_mouse", Disabled_on_external_mouse); + ("unknown", Unknown); + ] + +type tap = Enabled | Disabled | Unknown + +let tap_decoder = + Json_decoder.string_enum + [ ("enabled", Enabled); ("disabled", Disabled); ("unknown", Unknown) ] + +type tap_button_map = Lmr | Lrm | Unknown + +let tap_button_map_decoder = + Json_decoder.string_enum [ ("lmr", Lmr); ("lrm", Lrm); ("unknown", Unknown) ] + +type tap_drag = Enabled | Disabled | Unknown + +let tap_drag_decoder = + Json_decoder.string_enum + [ ("enabled", Enabled); ("disabled", Disabled); ("unknown", Unknown) ] + +type tap_drag_lock = Enabled | Disabled | Unknown + +let tap_drag_lock_decoder = + Json_decoder.string_enum + [ ("enabled", Enabled); ("disabled", Disabled); ("unknown", Unknown) ] + +type accel_profile = None | Flat | Adaptive | Unknown + +let accel_profile_decoder = + Json_decoder.string_enum + [ + ("none", None); + ("flat", Flat); + ("adaptive", Adaptive); + ("unknown", Unknown); + ] + +type natural_scroll = Enabled | Disabled | Unknown + +let natural_scroll_decoder = + Json_decoder.string_enum + [ ("enabled", Enabled); ("disabled", Disabled); ("unknown", Unknown) ] + +type left_handed = Enabled | Disabled | Unknown + +let left_handed_decoder = + Json_decoder.string_enum + [ ("enabled", Enabled); ("disabled", Disabled); ("unknown", Unknown) ] + +type click_method = None | Button_areas | Clickfinger | Unknown + +let click_method_decoder = + Json_decoder.string_enum + [ + ("none", None); + ("button_areas", Button_areas); + ("clickfinger", Clickfinger); + ("unknown", Unknown); + ] + +type middle_emulation = Enabled | Disabled | Unknown + +let middle_emulation_decoder = + Json_decoder.string_enum + [ ("enabled", Enabled); ("disabled", Disabled); ("unknown", Unknown) ] + +type scroll_method = None | Two_fingers | Edge | On_button_down | Unknown + +let scroll_method_decoder = + Json_decoder.string_enum + [ + ("none", None); + ("two_fingers", Two_fingers); + ("edge", Edge); + ("on_button_down", On_button_down); + ("unknown", Unknown); + ] + +type dwt = Enabled | Disabled | Unknown + +let dwt_decoder = + Json_decoder.string_enum + [ ("enabled", Enabled); ("disabled", Disabled); ("unknown", Unknown) ] + +type calibration_matrix = float * float * float * float * float * float + +let calibration_matrix_decoder = + let open Json_decoder in + let open Syntax in + let+ l = list float in + match l with + | [ a; b; c; d; e; f ] -> (a, b, c, d, e, f) + | _ -> raise (Invalid_argument "calibration_matrix_decoder") + +type t = { + send_events : send_events option; + tap : tap option; + tap_button_map : tap_button_map option; + tap_drag : tap_drag option; + tap_drag_lock : tap_drag_lock option; + accel_speed : float option; + accel_profile : accel_profile option; + natural_scroll : natural_scroll option; + left_handed : left_handed option; + click_method : click_method option; + middle_emulation : middle_emulation option; + scroll_method : scroll_method option; + scroll_button : int64 option; + dwt : dwt option; + calibration_matrix : calibration_matrix option; +} + +type libinput = t + +let decoder = + let open Json_decoder in + let open Syntax in + let+ send_events = field_opt "send_events" send_events_decoder + and+ tap = field_opt "tap" tap_decoder + and+ tap_button_map = field_opt "tap_button_map" tap_button_map_decoder + and+ tap_drag = field_opt "tap_drag" tap_drag_decoder + and+ tap_drag_lock = field_opt "tap_drag_lock" tap_drag_lock_decoder + and+ accel_speed = field_opt "accel_speed" float + and+ accel_profile = field_opt "accel_profile" accel_profile_decoder + and+ natural_scroll = field_opt "natural_scroll" natural_scroll_decoder + and+ left_handed = field_opt "left_handed" left_handed_decoder + and+ click_method = field_opt "click_method" click_method_decoder + and+ middle_emulation = field_opt "middle_emulation" middle_emulation_decoder + and+ scroll_method = field_opt "scroll_method" scroll_method_decoder + and+ scroll_button = field_opt "scroll_button" int64 + and+ dwt = field_opt "dwt" dwt_decoder + and+ calibration_matrix = + field_opt "calibration_matrix" calibration_matrix_decoder + in + { + send_events; + tap; + tap_button_map; + tap_drag; + tap_drag_lock; + accel_speed; + accel_profile; + natural_scroll; + left_handed; + click_method; + middle_emulation; + scroll_method; + scroll_button; + dwt; + calibration_matrix; + } diff --git a/lib/sway_ipc_types/message.ml b/lib/sway_ipc_types/message.ml new file mode 100644 index 0000000..90ca40f --- /dev/null +++ b/lib/sway_ipc_types/message.ml @@ -0,0 +1,56 @@ +type subscribe_reply = { success : bool } + +let subscribe_reply_decoder = + let open Json_decoder in + let open Syntax in + let+ success = field "success" bool in + { success } + +type run_command_reply = { + success : bool; + parse_error : bool option; + error : string option; +} + +let run_command_reply_decoder = + let open Json_decoder in + let open Syntax in + let+ success = field "success" bool + and+ parse_error = field_opt "parse_error" bool + and+ error = field_opt "error" string in + { success; parse_error; error } + +type _ t = + | Run_command : Command.t list -> run_command_reply list t + | Get_workspaces : Workspace.t list t + | Subscribe : Event.event_type list -> subscribe_reply t + | Get_tree : Node.t t + | Get_outputs : Output.t list t + +let to_raw_message : type reply. reply t -> Raw_message.t = function + | Run_command cmds -> + ( 0l, + Format.( + asprintf "%a" + (pp_print_list + ~pp_sep:(fun fmt () -> pp_print_string fmt "\n") + (fun fmt x -> fprintf fmt "%a" Command.pp x)) + cmds) ) + | Get_workspaces -> (1l, "") + | Subscribe evs -> + ( 2l, + Format.( + asprintf "[%a]" + (pp_print_list + ~pp_sep:(fun fmt () -> pp_print_string fmt ", ") + (fun fmt x -> fprintf fmt "\"%s\"" (Event.event_type_string x))) + evs) ) + | Get_outputs -> (3l, "") + | Get_tree -> (4l, "") + +let reply_decoder : type reply. reply t -> reply Json_decoder.t = function + | Run_command _ -> Json_decoder.list run_command_reply_decoder + | Get_workspaces -> Json_decoder.list Workspace.decoder + | Subscribe _ -> subscribe_reply_decoder + | Get_outputs -> Json_decoder.list Output.decoder + | Get_tree -> Node.decoder diff --git a/lib/sway_ipc_types/node.ml b/lib/sway_ipc_types/node.ml new file mode 100644 index 0000000..62272f0 --- /dev/null +++ b/lib/sway_ipc_types/node.ml @@ -0,0 +1,231 @@ +type fullscreen_mode = + | None + (* 0 *) + | Full_workspace + (* 1 *) + | Global_fullscreen (* 2 *) + +let fullscreen_mode_decoder = + let open Json_decoder in + let open Syntax in + let+ i = int64 in + match i with + | 0L -> None + | 1L -> Full_workspace + | 2L -> Global_fullscreen + | _ -> raise (Invalid_argument "fullscreen_mode_decoder") + +type node_type = Root | Output | Workspace | Con | Floating_con + +let node_type_decoder = + Json_decoder.string_enum + [ + ("root", Root); + ("output", Output); + ("workspace", Workspace); + ("con", Con); + ("floating_con", Floating_con); + ] + +type border = Normal | None | Pixel | Csd + +let border_decoder = + Json_decoder.string_enum + [ ("normal", Normal); ("none", None); ("pixel", Pixel); ("csd", Csd) ] + +type layout = + | Split_horizontal + | Split_vertical + | Stacked + | Tabbed + | Output + | None + +let layout_decoder = + Json_decoder.string_enum + [ + ("splith", Split_horizontal); + ("splitv", Split_vertical); + ("stacked", Stacked); + ("tabbed", Tabbed); + ("output", Output); + ("none", None); + ] + +type mark = string + +let mark_decoder = Json_decoder.string + +type application_state = Enabled | None + +let application_state_decoder = + Json_decoder.string_enum [ ("enabled", Enabled); ("none", None) ] + +type user_state = Focus | Fullscreen | Open | Visible | None + +let user_state_decoder = + Json_decoder.string_enum + [ + ("focus", Focus); + ("fullscreen", Fullscreen); + ("open", Open); + ("visible", Visible); + ("none", None); + ] + +type idle_inhibitors = { application : application_state; user : user_state } + +let idle_inhibitors_decoder = + let open Json_decoder in + let open Syntax in + let+ application = application_state_decoder and+ user = user_state_decoder in + { application; user } + +type window_properties = { + title : string; + window_class : string; + instance : string; + window_role : string option; + window_type : string option; + transient_for : string option; +} + +let window_properties_decoder = + let open Json_decoder in + let open Syntax in + let+ title = field "title" string + and+ window_class = field "window_class" string + and+ instance = field "instance" string + and+ window_role = field_opt "window_class" string + and+ window_type = field_opt "window_type" string + and+ transient_for = field_opt "transient_for" string in + { title; window_class; instance; window_role; window_type; transient_for } + +type t = { + id : int64; + name : string option; + node_type : node_type; + border : border; + current_border_width : int64; + layout : layout; + orientation : Output.orientation; + percent : float option; + rect : Rect.t; + window_rect : Rect.t; + deco_rect : Rect.t; + geometry : Rect.t; + urgent : bool; + sticky : bool; + marks : mark list; + focused : bool; + focus : int64 list; + nodes : t list; + floating_nodes : t list; + representation : string option; + fullscreen_mode : fullscreen_mode option; + app_id : string option; + pid : int64 option; + visible : bool option; + shell : string option; + inhibit_idle : bool option; + idle_inhibitors : idle_inhibitors option; + window : int64 option; + window_properties : window_properties option; +} + +type node = t + +let decoder = + let open Json_decoder in + let open Syntax in + mu (fun node_decoder -> + let+ id = field "id" int64 + and+ name = field_opt "name" string + and+ node_type = field "type" node_type_decoder + and+ border = field "border" border_decoder + and+ current_border_width = field "current_border_width" int64 + and+ layout = field "layout" layout_decoder + and+ orientation = field "orientation" Output.orientation_decoder + and+ percent = field_opt "percent" float + and+ rect = field "rect" Rect.decoder + and+ window_rect = field "window_rect" Rect.decoder + and+ deco_rect = field "deco_rect" Rect.decoder + and+ geometry = field "geometry" Rect.decoder + and+ urgent = field "urgent" bool + and+ sticky = field "sticky" bool + and+ marks = field "marks" @@ list mark_decoder + and+ focused = field "focused" bool + and+ focus = field "focus" @@ list int64 + and+ nodes = field "nodes" @@ list node_decoder + and+ floating_nodes = field "floating_nodes" @@ list node_decoder + and+ representation = field_opt "representation" string + and+ fullscreen_mode = field_opt "fullscreen_mode" fullscreen_mode_decoder + and+ app_id = field_opt "app_id" string + and+ pid = field_opt "pid" int64 + and+ visible = field_opt "visible" bool + and+ shell = field_opt "shell" string + and+ inhibit_idle = field_opt "inhibit_idle" bool + and+ idle_inhibitors = field_opt "idle_inhibitors" idle_inhibitors_decoder + and+ window = field_opt "window" int64 + and+ window_properties = + field_opt "window_properties" window_properties_decoder + in + { + id; + name; + node_type; + border; + current_border_width; + orientation; + layout; + percent; + rect; + window_rect; + deco_rect; + geometry; + urgent; + sticky; + marks; + focused; + nodes; + floating_nodes; + representation; + fullscreen_mode; + app_id; + pid; + visible; + shell; + inhibit_idle; + window; + window_properties; + focus; + idle_inhibitors; + }) + +let rec fold acc f node = + let acc = f acc node in + let acc = List.fold_left (fun acc node -> fold acc f node) acc node.nodes in + let acc = + List.fold_left (fun acc node -> fold acc f node) acc node.floating_nodes + in + acc + +let rec filter f node = + List.concat + [ + (if f node then [ node ] else []); + List.concat_map (fun x -> filter f x) node.nodes; + List.concat_map (fun x -> filter f x) node.floating_nodes; + ] + +let rec find f node = find_first f ((node :: node.nodes) @ node.floating_nodes) + +and find_first f = function + | x :: rst -> + if f x then Some x else find_first f (x.nodes @ x.floating_nodes @ rst) + | [] -> None + +let find_workspace_by_name name root = + find (fun x -> x.name = name && x.node_type == Root) root + +let is_window node = Option.is_some node.pid diff --git a/lib/sway_ipc_types/output.ml b/lib/sway_ipc_types/output.ml new file mode 100644 index 0000000..ec777fb --- /dev/null +++ b/lib/sway_ipc_types/output.ml @@ -0,0 +1,101 @@ +type subpixel_hinting = Rgb | Bgr | Vrgb | Vbgr | None | Unknown + +let subpixel_hinting_decoder = + Json_decoder.string_enum + [ + ("rgb", Rgb); + ("bgr", Bgr); + ("vrgb", Vrgb); + ("none", None); + ("unknown", Unknown); + ] + +type transform = + | Normal + | Ninety + | One_eighty + | Two_seventy + | Flipped_ninety + | Flipped_one_eighty + | Flipped_two_seventy + +let transform_decoder = + Json_decoder.string_enum + [ + ("normal", Normal); + ("90", Ninety); + ("180", One_eighty); + ("270", Two_seventy); + ("flipped-90", Flipped_ninety); + ("flipped-180", Flipped_one_eighty); + ("flipped-270", Flipped_two_seventy); + ] + +type mode = { width : int64; height : int64; refresh : int64 } + +let mode_decoder = + let open Json_decoder in + let open Syntax in + let+ width = field "width" int64 + and+ height = field "height" int64 + and+ refresh = field "refresh" int64 in + { width; height; refresh } + +type orientation = Vertical | Horizontal | None + +let orientation_decoder = + Json_decoder.string_enum + [ ("vertical", Vertical); ("horizontal", Horizontal); ("none", None) ] + +type t = { + name : string; + make : string; + model : string; + serial : string; + active : bool; + dpms : bool; + primary : bool; + scale : float option; + subpixel_hinting : subpixel_hinting option; + transform : transform option; + current_workspace : Workspace_id.t option; + modes : mode list; + current_mode : mode option; + rect : Rect.t; +} + +type output = t + +let decoder = + let open Json_decoder in + let open Syntax in + let+ name = field "name" string + and+ make = field "make" string + and+ model = field "model" string + and+ serial = field "serial" string + and+ active = field "active" bool + and+ dpms = field "dpms" bool + and+ primary = field "primary" bool + and+ scale = field_opt "scale" float + and+ subpixel_hinting = field_opt "subpixel_hinting" subpixel_hinting_decoder + and+ transform = field_opt "transform" transform_decoder + and+ current_workspace = field_opt "current_workspace" Workspace_id.decoder + and+ modes = field "modes" @@ list mode_decoder + and+ current_mode = field_opt "current_mode" mode_decoder + and+ rect = field "rect" Rect.decoder in + { + name; + make; + model; + serial; + active; + dpms; + primary; + scale; + subpixel_hinting; + transform; + current_workspace; + modes; + current_mode; + rect; + } diff --git a/lib/sway_ipc_types/raw_message.ml b/lib/sway_ipc_types/raw_message.ml new file mode 100644 index 0000000..16cdd9a --- /dev/null +++ b/lib/sway_ipc_types/raw_message.ml @@ -0,0 +1,18 @@ +type t = int32 * string +type raw_message = t + +let int32_to_string x = + let buffer = Bytes.create 4 in + Bytes.set_int32_ne buffer 0 x; + Bytes.to_string buffer + +let string_to_int32 x = + let buffer = Bytes.of_string x in + Bytes.get_int32_ne buffer 0 + +let magic_string = "i3-ipc" + +let to_string (code, payload) = + magic_string + ^ int32_to_string (String.length payload |> Int32.of_int) + ^ int32_to_string code ^ payload diff --git a/lib/sway_ipc_types/rect.ml b/lib/sway_ipc_types/rect.ml new file mode 100644 index 0000000..12b612f --- /dev/null +++ b/lib/sway_ipc_types/rect.ml @@ -0,0 +1,11 @@ +type t = { x : int64; y : int64; width : int64; height : int64 } +type rect = t + +let decoder = + let open Json_decoder.Syntax in + let open Json_decoder in + let+ x = field "x" int64 + and+ y = field "y" int64 + and+ width = field "width" int64 + and+ height = field "height" int64 in + { x; y; width; height } diff --git a/lib/sway_ipc_types/seat.ml b/lib/sway_ipc_types/seat.ml new file mode 100644 index 0000000..5292ea3 --- /dev/null +++ b/lib/sway_ipc_types/seat.ml @@ -0,0 +1,17 @@ +type t = { + name : string; + capabilities : int64; + focus : int64; + devices : Input_device.t list; +} + +type seat = t + +let decoder = + let open Json_decoder in + let open Syntax in + let+ name = field "name" string + and+ capabilities = field "capabilities" int64 + and+ focus = field "focus" int64 + and+ devices = field "devices" @@ list Input_device.decoder in + { name; capabilities; focus; devices } diff --git a/lib/sway_ipc_types/sway_version.ml b/lib/sway_ipc_types/sway_version.ml new file mode 100644 index 0000000..d49407c --- /dev/null +++ b/lib/sway_ipc_types/sway_version.ml @@ -0,0 +1,19 @@ +type t = { + major : int64; + minor : int64; + patch : int64; + human_readable : string; + loaded_config_file_name : string; +} + +type sway_version = t + +let decoder = + let open Json_decoder in + let open Syntax in + let+ major = field "major" int64 + and+ minor = field "minor" int64 + and+ patch = field "patch" int64 + and+ human_readable = field "human_readable" string + and+ loaded_config_file_name = field "loaded_config_file_name" string in + { major; minor; patch; human_readable; loaded_config_file_name } diff --git a/lib/sway_ipc_types/workspace.ml b/lib/sway_ipc_types/workspace.ml new file mode 100644 index 0000000..582e946 --- /dev/null +++ b/lib/sway_ipc_types/workspace.ml @@ -0,0 +1,23 @@ +type t = { + num : int64; + name : string; + visible : bool; + focused : bool; + urgent : bool; + rect : Rect.t; + output : string; +} + +type workspace = t + +let decoder = + let open Json_decoder in + let open Syntax in + let+ num = field "num" int64 + and+ name = field "name" string + and+ visible = field "visible" bool + and+ focused = field "focused" bool + and+ urgent = field "urgent" bool + and+ rect = field "rect" Rect.decoder + and+ output = field "output" string in + { num; name; visible; focused; urgent; rect; output } diff --git a/lib/sway_ipc_types/workspace_id.ml b/lib/sway_ipc_types/workspace_id.ml new file mode 100644 index 0000000..26082af --- /dev/null +++ b/lib/sway_ipc_types/workspace_id.ml @@ -0,0 +1,8 @@ +type t = Index of int | Name of string +type workspace_id = t + +let decoder = + let open Json_decoder in + let open Syntax in + let+ str = string in + match int_of_string_opt str with Some id -> Index id | None -> Name str diff --git a/spatial-sway.opam b/spatial-sway.opam new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/spatial-sway.opam diff --git a/sway-ipc.opam b/sway-ipc.opam new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/sway-ipc.opam |