aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorThomas Letan <lthms@soap.coffee>2022-08-20 00:01:18 +0200
committerThomas Letan <lthms@soap.coffee>2022-08-21 13:45:33 +0200
commit7f8430cb34007d71da9b2718027c9e2758ac59ad (patch)
tree921995e14d5572a4a422e60754164bfc604bf5cb
Initial commit
-rw-r--r--.gitignore2
-rw-r--r--.ocamlformat0
-rw-r--r--README.md47
-rw-r--r--bin/spatial/dune3
-rw-r--r--bin/spatial/ipc.ml27
-rw-r--r--bin/spatial/main.ml82
-rw-r--r--bin/spatial/pp_helpers.ml7
-rw-r--r--bin/spatial/ribbon.ml320
-rw-r--r--bin/spatial/state.ml171
-rw-r--r--bin/spatial/windows_registry.ml19
-rw-r--r--bin/spatial/workspaces_registry.ml31
-rw-r--r--bin/spatialmsg/dune3
-rw-r--r--bin/spatialmsg/main.ml31
-rw-r--r--dune-project1
-rw-r--r--json-decoder.opam0
-rw-r--r--lib/json_decoder/dune4
-rw-r--r--lib/json_decoder/json_decoder.ml48
-rw-r--r--lib/spatial_sway_ipc/dune3
-rw-r--r--lib/spatial_sway_ipc/spatial_sway_ipc.ml34
-rw-r--r--lib/sway_ipc/dune4
-rw-r--r--lib/sway_ipc/socket.ml46
-rw-r--r--lib/sway_ipc/sway_ipc.ml48
-rw-r--r--lib/sway_ipc_types/command.ml40
-rw-r--r--lib/sway_ipc_types/dune4
-rw-r--r--lib/sway_ipc_types/event.ml163
-rw-r--r--lib/sway_ipc_types/input_device.ml59
-rw-r--r--lib/sway_ipc_types/libinput.ml159
-rw-r--r--lib/sway_ipc_types/message.ml56
-rw-r--r--lib/sway_ipc_types/node.ml231
-rw-r--r--lib/sway_ipc_types/output.ml101
-rw-r--r--lib/sway_ipc_types/raw_message.ml18
-rw-r--r--lib/sway_ipc_types/rect.ml11
-rw-r--r--lib/sway_ipc_types/seat.ml17
-rw-r--r--lib/sway_ipc_types/sway_version.ml19
-rw-r--r--lib/sway_ipc_types/workspace.ml23
-rw-r--r--lib/sway_ipc_types/workspace_id.ml8
-rw-r--r--spatial-sway.opam0
-rw-r--r--sway-ipc.opam0
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