├── .gitignore ├── Makefile ├── dune-project ├── i3_workspaces.opam ├── layout.gif ├── readme.md └── src ├── args.ml ├── common ├── actions.ml ├── actions.mli ├── common.ml ├── dune ├── option.ml └── tree.ml ├── configuration ├── configuration.ml ├── configuration.mli └── dune ├── dune ├── handlers ├── binaryLayoutHandler.ml ├── defaultHandler.ml ├── defaultHandler.mli ├── dune ├── execHandler.ml ├── handlers.ml ├── handlers.mli ├── loggerHandler.ml └── simpleLayoutHandler.ml ├── i3_workspaces.ml └── i3dot.ml /.gitignore: -------------------------------------------------------------------------------- 1 | _build 2 | .merlin 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | dune build @install 3 | 4 | clean: 5 | dune clean 6 | 7 | docs: 8 | dune build @doc 9 | 10 | install: 11 | dune install --prefix "/usr" -p i3_workspaces 12 | 13 | deps: 14 | opam install . --deps-only 15 | 16 | .PHONY: all clean 17 | -------------------------------------------------------------------------------- /dune-project: -------------------------------------------------------------------------------- 1 | (lang dune 1.11) 2 | (name i3_workspaces) 3 | (version 1.0) 4 | -------------------------------------------------------------------------------- /i3_workspaces.opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | version: "0.2" 3 | synopsis: "Workspace manager for i3 WM" 4 | name: "i3_workspaces" 5 | authors: ["Sébastien Dailly"] 6 | maintainer: ["Sébastien Dailly"] 7 | license: "WTFPL" 8 | depends: [ 9 | "dune" 10 | "ocaml" 11 | "lwt" 12 | "i3ipc" {>= "0.3"} 13 | "lwt_ppx" 14 | "ocaml-inifiles" 15 | ] 16 | dev-repo: "git+https://github.com/chimrod/i3_workspaces.git" 17 | build: [ 18 | ["dune" "build" "-p" name "-j" jobs] 19 | ] 20 | -------------------------------------------------------------------------------- /layout.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chimrod/i3_workspaces/ce1dceb8633b427b315c1a06d86c5698a23f62eb/layout.gif -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # A workspace manager for I3 wm 2 | 3 | ## Goals 4 | 5 | `i3_workspaces` allow you to configure your workspace easily. I use it for 6 | setting a wallpaper to each workspace, or launch application on some 7 | workspaces. 8 | 9 | ### Problem with `assign` 10 | 11 | The [assign](https://i3wm.org/docs/userguide.html#assign_workspace) keyword in 12 | the i3 configuration allow you to set a dedicated workspace for an application. 13 | This solution as flaws, like moving ALL the windows on this workspace, even if 14 | the parent has been moved to another one workspace. 15 | 16 | We are answering the problem in a different way : instead of creating the 17 | window, then moving it to a workspace, you run the application when the 18 | workspace is created. You can then forget all the placement rules, and you can 19 | move the application anywhere, without being annoyed by pop-up window created 20 | on another workspace. 21 | 22 | ## Configuration 23 | 24 | Create a configuration in `${XDG_CONFIG_HOME}/i3_workspaces/config` with : 25 | 26 | ```ini 27 | [global] 28 | image=~/wallpaper/default.jpg 29 | on_focus=feh --bg-scale ${image} 30 | 31 | [1] 32 | image=~/wallpaper/1.jpg 33 | 34 | [2] 35 | image=~/wallpaper/2.jpg 36 | on_init_swallow_class=URxvt 37 | 38 | [mail] 39 | on_init=thunderbird 40 | 41 | [web] 42 | image=~/wallpaper/web.jpg 43 | on_init=firefox 44 | on_init_swallow_class=Firefox 45 | 46 | [music] 47 | on_init=gmpc 48 | ``` 49 | 50 | - the `on_focus` command will be launched on workspace change. 51 | - the `on_init` will be launched on workspace creation. 52 | 53 | Keys defined in `global` section will apply on any workspace, and can be 54 | overriden in a dedicated workspace section. 55 | 56 | The key `on_init_swallow_class` tells i3 that the window with the given class 57 | shall be placed on the workspace. It create a container in the workspace, 58 | and i3 will not destroy it if you leave the workspace right after creating it : 59 | this prevent `on_init` event to be run a second time when the window is created. 60 | 61 | ### Layout 62 | 63 | You can also let the application manage for you the window placement. 64 | i3_workspaces provide a binary layout which automaticaly divide each container 65 | following a binary space partionning : 66 | 67 | ![Layout example](layout.gif) 68 | 69 | ```ini 70 | [global] 71 | layout=binary 72 | ``` 73 | 74 | The three layout are managed : 75 | 76 | - Vertical 77 | - Horizontal 78 | - Binary 79 | 80 | ## Compilation 81 | 82 | The application is coded in OCaml, a functionnal language, and uses 83 | [i3ipc](https://github.com/Armael/ocaml-i3ipc/) to communicate with the i3. 84 | 85 | Require [opam](http://opam.ocaml.org/) 86 | 87 | Download the project and compile it with `opam pin add https://github.com/Chimrod/i3_workspaces.git` 88 | 89 | Install with `sudo make install` 90 | -------------------------------------------------------------------------------- /src/args.ml: -------------------------------------------------------------------------------- 1 | type t = { 2 | config : string 3 | (** Specify the path to the configuration file. 4 | 5 | By default, search for ${XDG_CONFIG_HOME}/i3_workspaces/config*) 6 | } 7 | 8 | let usage = 9 | "i3_workspaces" 10 | 11 | let check_file env path = begin 12 | try 13 | let env_value = Unix.getenv env in 14 | let file = env_value ^ "/" ^ path ^ "config" in 15 | if Sys.file_exists file then 16 | Some file 17 | else 18 | None 19 | with Not_found -> None 20 | end 21 | 22 | (** Try to look for the default environment *) 23 | let default_conf () = begin 24 | let configuration = ref "" in 25 | begin match check_file "XDG_CONFIG_HOME" "i3_workspaces/" with 26 | | Some f -> configuration := f 27 | | None -> 28 | match check_file "HOME" ".config/i3_workspaces/" with 29 | | Some f -> configuration := f 30 | | None -> () 31 | end; 32 | 33 | let speclist = 34 | [ ("-c", Arg.Set_string configuration, "Configuration file") ] in 35 | Arg.parse speclist (fun _ -> ()) usage; 36 | {config = !configuration } 37 | 38 | end 39 | -------------------------------------------------------------------------------- /src/common/actions.ml: -------------------------------------------------------------------------------- 1 | type node = I3ipc.Reply.node 2 | 3 | type actions = 4 | | W: (unit -> (string * (unit -> unit Lwt.t)) Lwt.t) -> actions 5 | | C: string -> actions 6 | 7 | type t = actions list 8 | 9 | type answer = I3ipc.Reply.command_outcome list 10 | 11 | let create = [] 12 | 13 | (** Create a container which swallow the given class *) 14 | let swallow class_name t = begin 15 | 16 | let f () = begin 17 | 18 | let%lwt (file, channel) = Lwt_io.open_temp_file ~prefix:"i3_workspaces" () in 19 | let%lwt () = Lwt_io.fprintf channel {|{"swallows": [{"class": "%s"}]}|} class_name 20 | in 21 | 22 | let command = "append_layout " ^ file 23 | and after () = Lwt_io.close channel 24 | in Lwt.return (command, after) 25 | end in 26 | 27 | W f::t 28 | end 29 | 30 | (** Launch an application *) 31 | let launch exec t command = begin 32 | C ( 33 | begin match exec with 34 | | `NoStartupId -> ("exec --no-startup-id \"" ^ command ^ "\"") 35 | | _ -> ("exec \"" ^ command ^ "\"") 36 | end 37 | )::t 38 | end 39 | 40 | let _focus (container:I3ipc.Reply.node) = begin 41 | "[con_id=" ^ (container.I3ipc.Reply.id) ^ "] " 42 | end 43 | 44 | let split (container:node) new_layout t = begin 45 | let open I3ipc.Reply in 46 | let con_id = _focus container in 47 | C ( 48 | begin match new_layout with 49 | | SplitV -> (con_id ^ "split vertical") 50 | | SplitH -> (con_id ^ "split horizontal") 51 | | _ -> "nop" 52 | end 53 | )::t 54 | end 55 | 56 | let layout (container:node) new_layout t = begin 57 | let open I3ipc.Reply in 58 | let con_id = _focus container in 59 | C ( 60 | begin match new_layout with 61 | | SplitV -> (con_id ^ "layout splitv") 62 | | SplitH -> (con_id ^ "layout splith") 63 | | _ -> "nop" 64 | end 65 | )::t 66 | end 67 | 68 | let exec ~focus message t = 69 | let focus = _focus focus in 70 | C (focus ^ message)::t 71 | 72 | let empty = [] 73 | 74 | let apply conn t = begin 75 | 76 | let b = Buffer.create 16 in 77 | let add_elem b elem = begin 78 | Buffer.add_string b elem; 79 | Buffer.add_string b ";"; 80 | b 81 | end in 82 | 83 | let f (buffer, posts) = begin function 84 | | C c -> Lwt.return (add_elem buffer c, posts) 85 | | W f -> let%lwt c, p = f () in 86 | Lwt.return (add_elem buffer c, p::posts) 87 | end in 88 | 89 | let%lwt command, posts = Lwt_list.fold_left_s f (b, []) t in 90 | let command' = Buffer.contents command in 91 | print_endline command'; 92 | let%lwt result = I3ipc.command conn command' in 93 | let%lwt _posts = Lwt_list.iter_p (fun f -> f ()) posts in 94 | Lwt.return result 95 | end 96 | -------------------------------------------------------------------------------- /src/common/actions.mli: -------------------------------------------------------------------------------- 1 | type t 2 | 3 | val create: t 4 | 5 | type answer 6 | 7 | (** Create a container which swallow the given class *) 8 | val swallow : string -> t -> t 9 | 10 | (** Launch an application with i3 exec command *) 11 | val launch : [`NoStartupId | `StartupId ] -> t -> string -> t 12 | 13 | val split: I3ipc.Reply.node -> I3ipc.Reply.node_layout -> t -> t 14 | 15 | val layout: I3ipc.Reply.node -> I3ipc.Reply.node_layout -> t -> t 16 | 17 | val exec: focus:I3ipc.Reply.node -> string -> t -> t 18 | 19 | val empty: answer 20 | 21 | (** Execute all the commands *) 22 | val apply: I3ipc.connection -> t -> answer Lwt.t 23 | -------------------------------------------------------------------------------- /src/common/common.ml: -------------------------------------------------------------------------------- 1 | module Tree = Tree 2 | module Option = Option 3 | module Actions = Actions 4 | -------------------------------------------------------------------------------- /src/common/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name common) 3 | (libraries 4 | configuration 5 | i3ipc 6 | lwt 7 | ) 8 | (preprocess (pps lwt_ppx ppx_deriving.enum)) 9 | 10 | ) 11 | -------------------------------------------------------------------------------- /src/common/option.ml: -------------------------------------------------------------------------------- 1 | let map ~f = function 2 | | None -> None 3 | | Some x -> Some (f x) 4 | 5 | let bind ~f = function 6 | | None -> None 7 | | Some x -> f x 8 | 9 | 10 | let default ~v = function 11 | | None -> v 12 | | Some x -> x 13 | 14 | let (or) = function 15 | | None -> (fun opt2 -> opt2) 16 | | Some _ as opt1 -> (fun _ -> opt1) 17 | -------------------------------------------------------------------------------- /src/common/tree.ml: -------------------------------------------------------------------------------- 1 | type t = I3ipc.Reply.node 2 | 3 | (** General traverse tree function which return the workspace which contains a 4 | window match a given predicate. 5 | *) 6 | let traverse f (root:t) = begin 7 | 8 | let rec match_container wks (nodes: t list) = begin match nodes with 9 | | [] -> false 10 | | hd::tl -> 11 | if f hd then 12 | true 13 | else 14 | let nodes' = List.rev_append hd.I3ipc.Reply.nodes tl in 15 | (match_container [@tailcall]) wks nodes' 16 | end in 17 | 18 | let rec traverse (nodes:t list) = begin 19 | begin match nodes with 20 | | [] -> None 21 | | hd::tl -> 22 | 23 | begin match hd.nodetype with 24 | | Workspace -> 25 | begin match match_container hd (hd::hd.nodes) with 26 | | true -> Some hd 27 | | false -> (traverse [@tailcall]) tl 28 | end 29 | | Floating_con | Dockarea -> (traverse[@tailcall]) tl 30 | | _ -> (traverse[@tailcall]) (List.rev_append hd.nodes tl) 31 | end 32 | end 33 | end in 34 | traverse [root] 35 | end 36 | 37 | let get_workspace (t:t) (container:t) = begin 38 | traverse (fun c -> c.I3ipc.Reply.id = container.I3ipc.Reply.id) t 39 | end 40 | 41 | let get_focused_workspace (t:t) = begin 42 | traverse (fun c -> c.I3ipc.Reply.focused) t 43 | end 44 | 45 | -------------------------------------------------------------------------------- /src/configuration/configuration.ml: -------------------------------------------------------------------------------- 1 | type t = Inifiles.inifile 2 | 3 | 4 | (** Regex for extracting the variables parameters in configuration file *) 5 | let param = Str.regexp {|\${.+}|} 6 | 7 | let load_or_global conf section field = begin 8 | let local_value = try (conf#getaval section field) with 9 | | Inifiles.Invalid_element _ 10 | | Inifiles.Invalid_section _ -> [] 11 | and global_value = try (conf#getaval "global" field) with 12 | | Inifiles.Invalid_element _ 13 | | Inifiles.Invalid_section _ -> [] in 14 | List.append global_value local_value 15 | end 16 | 17 | exception Too_many_recursion of string 18 | 19 | let rec last_element = function 20 | | [] -> None 21 | | elem::[] -> Some elem 22 | | _::tl -> (last_element[@tailcal]) tl 23 | 24 | 25 | (** Replace the given string with the values associated with key in the ini 26 | file *) 27 | let rec get_params t level section str = begin 28 | 29 | (** Prevent infinite looping *) 30 | begin if level > 10 then 31 | raise (Too_many_recursion section) 32 | end; 33 | 34 | match Str.search_forward param str 0 with 35 | | exception Not_found -> str 36 | | _ -> 37 | let matched = Str.matched_string str in 38 | let len = String.length matched in 39 | (* Remove the ${ } around the text *) 40 | let argument = String.sub matched 2 (len - 3) in 41 | begin match last_element (load_or_global t section argument) with 42 | | None -> 43 | (* This should occur only in startup *) 44 | Printf.eprintf "Key %s not found in section [%s] nor [global]\n%!" argument section; 45 | raise Not_found 46 | 47 | | Some x -> 48 | (get_params[@tailcall]) t (level + 1) section (Str.replace_first param x str) 49 | end; 50 | 51 | end 52 | 53 | (** Load the value from the Configuration 54 | If the key does not exist, look for the global section 55 | *) 56 | let load_values conf section field = begin 57 | load_or_global conf section field 58 | |> List.rev_map (get_params conf 0 section) 59 | end 60 | 61 | let load_value conf section field = begin 62 | let values = load_or_global conf section field 63 | |> List.rev_map (get_params conf 0 section) in 64 | match values with 65 | | [] -> None 66 | | hd::_ -> Some hd 67 | end 68 | 69 | 70 | (** Check the whole configuration to ensure that each variable use in a 71 | section is defined somewhere *) 72 | let check_section conf section = begin 73 | let f _key value = ignore (get_params conf 0 section value) 74 | in 75 | conf#iter f section 76 | end 77 | 78 | let load f = begin 79 | let conf = new Inifiles.inifile f in 80 | let sections = conf#sects in 81 | (* Ensure we have a global section *) 82 | match List.exists ((=) "global") sections with 83 | | false -> 84 | Printf.eprintf "No [global] section found in %s\nExciting\n%!" f; 85 | None 86 | | true -> 87 | (* First check all the values in the config file *) 88 | match List.iter (check_section conf) sections with 89 | | exception Not_found -> None 90 | | exception Too_many_recursion section -> 91 | Printf.eprintf "Too many recursion in section %s\n%!" section; 92 | None 93 | | _ -> Some conf 94 | end 95 | -------------------------------------------------------------------------------- /src/configuration/configuration.mli: -------------------------------------------------------------------------------- 1 | type t 2 | 3 | (** Load the configuration from the given file name *) 4 | val load : string -> t option 5 | 6 | (** Load the values from the section and given key *) 7 | val load_values: t -> string -> string -> string list 8 | 9 | val load_value: t -> string -> string -> string option 10 | -------------------------------------------------------------------------------- /src/configuration/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name configuration) 3 | (libraries 4 | inifiles 5 | str 6 | ) 7 | (preprocess (pps lwt_ppx ppx_deriving.enum)) 8 | 9 | ) 10 | -------------------------------------------------------------------------------- /src/dune: -------------------------------------------------------------------------------- 1 | (env 2 | (dev 3 | (flags (:standard -warn-error -A -cclib -lxcb)) 4 | ) 5 | (release 6 | (ocamlopt_flags (-O3))) 7 | ) 8 | 9 | (executables 10 | (names i3_workspaces i3dot) 11 | (libraries 12 | configuration 13 | common 14 | handlers 15 | lwt 16 | ) 17 | (preprocess (pps lwt_ppx)) 18 | (public_names i3_workspaces i3dot) 19 | ) 20 | -------------------------------------------------------------------------------- /src/handlers/binaryLayoutHandler.ml: -------------------------------------------------------------------------------- 1 | open Common 2 | open I3ipc.Reply 3 | 4 | include DefaultHandler.M 5 | 6 | type traversal = Actions.t -> node -> node -> (Actions.t * node list) 7 | 8 | let switch_layout = begin function 9 | | SplitH -> Some SplitV 10 | | SplitV -> Some SplitH 11 | | _ -> None 12 | end 13 | 14 | let rec get_singles acc t = begin match t.nodes with 15 | | single::[] -> (get_singles[@tailcall]) (single::acc) single 16 | | _ -> acc 17 | end 18 | 19 | let rec reduce container state = begin function 20 | | [] -> state, container 21 | | hd::tl -> 22 | begin match hd.layout with 23 | | SplitV -> 24 | let state = Actions.exec ~focus:container "move left" state in 25 | (reduce [@tailcall]) container state tl 26 | | SplitH -> 27 | let state = Actions.exec ~focus:container "move up" state in 28 | reduce container state tl 29 | | _ -> state, container 30 | end 31 | end 32 | 33 | (** Simplify the tree by removing all the containers which contains a single 34 | child *) 35 | let reduce_tree state _ container = begin 36 | begin match get_singles [] container with 37 | | leaf::tl -> 38 | let state, child = reduce leaf state tl in 39 | (* If the container is a workspace, restore its original layout by 40 | splitting hist last child, it has been destroyed when we move the last 41 | child out of his parent *) 42 | let state' = begin match container.nodetype with 43 | | Workspace -> Actions.split child (child.layout) state 44 | | _ -> state 45 | end in 46 | (state', [child]) 47 | | [] -> 48 | (state, container.nodes) 49 | end 50 | 51 | end 52 | 53 | let check_split container state orientation = begin 54 | List.fold_left (fun (state, acc) n -> 55 | begin match n.layout == orientation, n.nodes with 56 | | _, [] -> 57 | (* This is the last container, and it is directly under a node with 58 | many childs, we split *) 59 | Actions.split n orientation state, acc 60 | | false, _ -> 61 | (* The node is not final, but the layout is not the good one. We change 62 | it *) 63 | let n' = {n with layout = orientation} in 64 | Actions.layout n orientation state, n'::acc 65 | | true, _ -> 66 | (* The node already has the right orientation, and is not a final 67 | container : we keep it unchanged *) 68 | state, n::acc 69 | end 70 | ) (state, []) container.nodes 71 | end 72 | 73 | let binary_tree state parent container = begin 74 | begin match container.nodes, switch_layout container.layout with 75 | (* If the node has no childs, ignore it *) 76 | | [], _ 77 | | _, None -> state, [] 78 | (* If the node has only one child, just change the layout *) 79 | | hd::[], Some new_layout -> 80 | if parent.layout == container.layout then ( 81 | Actions.layout container new_layout state, [{hd with layout = new_layout}] 82 | ) else ( 83 | state, [hd] 84 | ) 85 | (* If there is more than one child, split all the descendors *) 86 | | _, Some new_layout -> 87 | check_split container state new_layout 88 | end 89 | end 90 | 91 | let rec traverse (f:traversal) nodes state: Actions.t = begin match nodes with 92 | | [] -> (* no nodes, just return *) state 93 | | ((parent:node), (hd:node))::tl -> 94 | let (state, childs) = f state parent hd in 95 | (* Add the parent information to all childs *) 96 | let childs' = List.map (fun t -> hd, t) childs in 97 | (traverse[@tailcall]) f (List.append tl childs') state 98 | end 99 | 100 | let (|>>=?) opt f = Option.bind ~f opt 101 | 102 | (** Check if we should manage this workspace *) 103 | let handlers ini workspace = workspace.I3ipc.Reply.name 104 | |>>=? fun name -> Configuration.load_value ini name "layout" 105 | |>>=? fun layout -> match layout with 106 | | "binary" -> Some true 107 | | _ -> None 108 | 109 | let window_create ini node state = begin 110 | match handlers ini node with 111 | | None -> state 112 | | Some _ -> 113 | begin match switch_layout node.I3ipc.Reply.layout with 114 | | Some layout' -> 115 | let fake_root = I3ipc.Reply.{node with layout = layout'} in 116 | traverse binary_tree [fake_root, node] state 117 | | None -> 118 | (* The workspace layout is not splith nor splitv, ignoring *) 119 | state 120 | end 121 | end 122 | 123 | let window_close ini node state = begin 124 | match handlers ini node with 125 | | None -> state 126 | | Some _ -> 127 | begin match switch_layout node.I3ipc.Reply.layout with 128 | | Some layout' -> 129 | let fake_root = I3ipc.Reply.{node with layout = layout'} in 130 | traverse reduce_tree [fake_root, node] state 131 | | None -> 132 | (* The workspace layout is not splith nor splitv, ignoring *) 133 | state 134 | end 135 | end 136 | -------------------------------------------------------------------------------- /src/handlers/defaultHandler.ml: -------------------------------------------------------------------------------- 1 | module type HANDLER = sig 2 | 3 | (** Function to call on workspace change *) 4 | val workspace_focus: Configuration.t -> I3ipc.Reply.node -> string -> Common.Actions.t -> Common.Actions.t 5 | 6 | val workspace_init: Configuration.t -> I3ipc.Reply.node -> string -> Common.Actions.t -> Common.Actions.t 7 | 8 | val window_create: Configuration.t -> I3ipc.Reply.node -> Common.Actions.t -> Common.Actions.t 9 | 10 | val window_close: Configuration.t -> I3ipc.Reply.node -> Common.Actions.t -> Common.Actions.t 11 | end 12 | 13 | 14 | module M = struct 15 | let workspace_focus _ini _event _name state = 16 | state 17 | 18 | let workspace_init _ini _event _name state = 19 | state 20 | 21 | let window_create _ini _node state = 22 | state 23 | 24 | let window_close _ini _node state = 25 | state 26 | end 27 | -------------------------------------------------------------------------------- /src/handlers/defaultHandler.mli: -------------------------------------------------------------------------------- 1 | module type HANDLER = sig 2 | 3 | (** Function to call on workspace change 4 | The function is called with the given parameters : 5 | - Configuration is the application config 6 | - Workspace 7 | - the workspace name 8 | - The pointer the I3 actions to execute 9 | *) 10 | val workspace_focus: Configuration.t -> I3ipc.Reply.node -> string -> Common.Actions.t -> Common.Actions.t 11 | 12 | (** Function to call on workspace creation *) 13 | val workspace_init: Configuration.t -> I3ipc.Reply.node -> string -> Common.Actions.t -> Common.Actions.t 14 | 15 | (** Function to call on window creation. 16 | The given node is the window workspace *) 17 | val window_create: Configuration.t -> I3ipc.Reply.node -> Common.Actions.t -> Common.Actions.t 18 | 19 | (** Function to call on window closing 20 | The given node is the focused workspace *) 21 | val window_close: Configuration.t -> I3ipc.Reply.node -> Common.Actions.t -> Common.Actions.t 22 | end 23 | 24 | module M:HANDLER 25 | -------------------------------------------------------------------------------- /src/handlers/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name handlers) 3 | (libraries 4 | configuration 5 | common 6 | i3ipc 7 | lwt 8 | ) 9 | (preprocess (pps lwt_ppx ppx_deriving.enum)) 10 | 11 | ) 12 | -------------------------------------------------------------------------------- /src/handlers/execHandler.ml: -------------------------------------------------------------------------------- 1 | include Common 2 | include DefaultHandler.M 3 | 4 | let workspace_focus ini _event name state = begin 5 | Configuration.load_values ini name "on_focus" 6 | |> List.fold_left (Actions.launch `NoStartupId) state 7 | end 8 | 9 | let workspace_init ini _event name state = begin 10 | (* If there is a swallow option, we run it in first *) 11 | let state = Configuration.load_values ini name "on_init_swallow_class" 12 | |> List.fold_left (fun a b -> Actions.swallow b a) state 13 | in 14 | (* Do not run no-startup-id : we want the application to be launched on 15 | this workspace *) 16 | Configuration.load_values ini name "on_init" 17 | |> List.fold_left (Actions.launch `StartupId) state 18 | end 19 | -------------------------------------------------------------------------------- /src/handlers/handlers.ml: -------------------------------------------------------------------------------- 1 | open Common 2 | 3 | let modules = ref [] 4 | 5 | let register_handler (m : (module DefaultHandler.HANDLER)) = begin 6 | modules := m :: (!modules) 7 | end 8 | let () = register_handler (module BinaryLayoutHandler) 9 | let () = register_handler (module LoggerHandler) 10 | let () = register_handler (module ExecHandler) 11 | let () = register_handler (module SimpleLayoutHandler) 12 | 13 | let get_handlers () = ! modules 14 | 15 | let workspace_event conn {I3ipc.Event.change; I3ipc.Event.current; _} ini = begin 16 | 17 | let call_workspace_focus workspace name state (module H:DefaultHandler.HANDLER) = begin 18 | H.workspace_focus ini workspace name state 19 | end 20 | 21 | and call_workspace_init workspace name state (module H:DefaultHandler.HANDLER) = begin 22 | H.workspace_init ini workspace name state 23 | end 24 | 25 | in 26 | 27 | begin match current with 28 | | None -> Lwt.return Actions.empty 29 | | Some workspace -> 30 | begin match workspace.I3ipc.Reply.name with 31 | | None -> Lwt.return Actions.empty 32 | (* Ensure we have a workspace and a name *) 33 | | Some name -> 34 | begin match change with 35 | | Focus -> 36 | get_handlers () 37 | |> List.fold_left (call_workspace_focus workspace name) Actions.create 38 | |> Actions.apply conn 39 | | Init 40 | | Rename -> 41 | get_handlers () 42 | |> List.fold_left (call_workspace_init workspace name) Actions.create 43 | |> Actions.apply conn 44 | | _ -> Lwt.return Actions.empty 45 | end 46 | end 47 | end 48 | end 49 | 50 | let window_event conn {I3ipc.Event.change; I3ipc.Event.container} ini = begin 51 | 52 | let%lwt tree = I3ipc.get_tree conn in 53 | 54 | 55 | let call_window_close workspace state (module H:DefaultHandler.HANDLER) = begin 56 | H.window_close ini workspace state 57 | end 58 | 59 | and call_window_create workspace state (module H:DefaultHandler.HANDLER) = begin 60 | H.window_create ini workspace state 61 | end in 62 | 63 | begin match change with 64 | | I3ipc.Event.Close -> 65 | (* On close event, we try to find the focused workspace, as the container given 66 | by i3 does not exists anymore in the tree *) 67 | let focused_workspace = Common.Tree.get_focused_workspace tree in 68 | begin match focused_workspace with 69 | | None -> Lwt.return Actions.empty 70 | | Some workspace -> 71 | get_handlers () 72 | |> List.fold_left (call_window_close workspace) Actions.create 73 | |> Actions.apply conn 74 | end 75 | | I3ipc.Event.New -> 76 | let workspace = Common.Tree.get_workspace tree container in 77 | begin match workspace with 78 | | None -> Lwt.return Actions.empty 79 | | Some workspace -> 80 | get_handlers () 81 | |> List.fold_left (call_window_create workspace) Actions.create 82 | |> Actions.apply conn 83 | end 84 | | I3ipc.Event.Move -> 85 | (* Move is like a Close event followed by a New one *) 86 | 87 | let handlers = get_handlers () in 88 | 89 | let focused_workspace = Common.Tree.get_focused_workspace tree in 90 | let state = begin match focused_workspace with 91 | | None -> Actions.create 92 | | Some workspace -> 93 | List.fold_left (call_window_close workspace) Actions.create handlers 94 | end in 95 | let current_workspace = Common.Tree.get_workspace tree container in 96 | let state' = begin match current_workspace with 97 | | None -> state 98 | | Some workspace -> 99 | List.fold_left (call_window_create workspace) state handlers 100 | end in 101 | Actions.apply conn state' 102 | | _ -> Lwt.return Actions.empty 103 | end 104 | 105 | end 106 | -------------------------------------------------------------------------------- /src/handlers/handlers.mli: -------------------------------------------------------------------------------- 1 | (** Register a new handler *) 2 | val register_handler: (module DefaultHandler.HANDLER) -> unit 3 | 4 | (** Mananeg an I3ipc workspace event *) 5 | val workspace_event: I3ipc.connection -> I3ipc.Event.workspace_event_info -> Configuration.t -> Common.Actions.answer Lwt.t 6 | 7 | (** Mananeg an I3ipc window event *) 8 | val window_event: I3ipc.connection -> I3ipc.Event.window_event_info -> Configuration.t -> Common.Actions.answer Lwt.t 9 | 10 | -------------------------------------------------------------------------------- /src/handlers/loggerHandler.ml: -------------------------------------------------------------------------------- 1 | include DefaultHandler.M 2 | 3 | let workspace_focus _ _ name state = begin 4 | Printf.printf "Receive workspace event Focus on %s\n" 5 | name; 6 | state 7 | end 8 | 9 | let workspace_init _ _ name state = begin 10 | Printf.printf "Receive workspace event Init on %s\n" 11 | name; 12 | state 13 | end 14 | -------------------------------------------------------------------------------- /src/handlers/simpleLayoutHandler.ml: -------------------------------------------------------------------------------- 1 | include Common 2 | include DefaultHandler.M 3 | 4 | let (|>>=?) opt f = Option.bind ~f opt 5 | 6 | (** Check if we should manage this workspace *) 7 | let handlers ini name = 8 | Configuration.load_value ini name "layout" 9 | |>>=? fun layout -> match layout with 10 | | "horizontal" -> Some I3ipc.Reply.SplitH 11 | | "vertical" -> Some I3ipc.Reply.SplitV 12 | | _ -> None 13 | 14 | let workspace_init ini node name state = 15 | (* Set the workspace in vertical mode *) 16 | match handlers ini name with 17 | | None -> state 18 | | Some t -> Actions.split node t state 19 | -------------------------------------------------------------------------------- /src/i3_workspaces.ml: -------------------------------------------------------------------------------- 1 | 2 | 3 | let pp_error format = begin function 4 | | I3ipc.No_IPC_socket -> Format.fprintf format "No_IPC_socket" 5 | | I3ipc.Bad_magic_string str -> Format.fprintf format "Bad_magic_string %s" str 6 | | I3ipc.Unexpected_eof -> Format.fprintf format "Unexpected_eof" 7 | | I3ipc.Unknown_type _t -> Format.fprintf format "Unknown_type" 8 | | I3ipc.Bad_reply str -> Format.fprintf format "Bad_reply %s" str 9 | end 10 | 11 | let rec event_loop configuration conn = begin 12 | 13 | begin match%lwt I3ipc.next_event conn with 14 | | exception I3ipc.Protocol_error err -> 15 | (* On error, log to stderr then loop *) 16 | Format.eprintf "%a\n%!" pp_error err; 17 | (event_loop[@tailcall]) configuration conn 18 | | I3ipc.Event.Workspace wks -> 19 | let%lwt _result = Handlers.workspace_event conn wks configuration in 20 | (event_loop[@tailcall]) configuration conn 21 | | I3ipc.Event.Window w -> 22 | let%lwt _result = Handlers.window_event conn w configuration in 23 | (event_loop[@tailcall]) configuration conn 24 | | _ -> 25 | (* This should not happen, we did not subscribe to other events *) 26 | (event_loop[@tailcall]) configuration conn 27 | end 28 | 29 | end 30 | 31 | let main = 32 | 33 | let cfg = Args.default_conf () in 34 | 35 | if not (Sys.file_exists cfg.config) then ( 36 | Printf.eprintf "Configuration file %s not found\nExciting\n%!" cfg.config; 37 | exit 1 38 | ); 39 | (* Load the ini file *) 40 | begin match Configuration.load cfg.config with 41 | | None -> exit 1 42 | | Some config -> 43 | let%lwt conn = I3ipc.connect () in 44 | 45 | let%lwt reply = I3ipc.subscribe conn [Workspace ; Window] in 46 | if reply.success then 47 | event_loop config conn 48 | else 49 | Lwt.return_unit 50 | end 51 | 52 | let () = Lwt_main.run main 53 | -------------------------------------------------------------------------------- /src/i3dot.ml: -------------------------------------------------------------------------------- 1 | open Common 2 | 3 | type t = I3ipc.Reply.node 4 | 5 | let (|>>=?) opt f = Option.bind ~f opt 6 | 7 | let opt_name = function 8 | | Some name -> name 9 | | None -> "?" 10 | 11 | (** Show all the links from one node to his childs *) 12 | let show_links formatter (t:t) = begin 13 | 14 | Format.pp_print_list ~pp_sep:(fun _ _ -> ()) (fun f (subnode:t) -> 15 | Format.fprintf f "\n%s -> %s" 16 | t.id 17 | subnode.id 18 | ) formatter t.nodes 19 | end 20 | 21 | let rec pp_print_node formatter (t:t) = begin 22 | 23 | (* The window name *) 24 | let w_name : string = 25 | t.window_properties 26 | |>>=? (fun f -> f.title) 27 | |> opt_name 28 | in 29 | 30 | let print_attrs f (t:t) = begin match t.nodetype with 31 | | Root -> 32 | Format.fprintf f "label=root style=filled fillcolor=\"/accent6/1\"" 33 | | Output -> 34 | Format.fprintf f "style=filled fillcolor=\"/accent6/2\"" 35 | | Con -> 36 | Format.fprintf f "label=\"%s - %a\r%s\" style=filled fillcolor=\"/accent6/3\" " 37 | w_name 38 | I3ipc.Reply.pp_node_layout t.layout 39 | t.id 40 | | Floating_con -> 41 | Format.fprintf f "style=filled fillcolor=\"/accent6/4\"" 42 | | Workspace -> 43 | Format.fprintf f "label=\"%s - %a - %s\" style=filled fillcolor=\"/accent6/5\"" 44 | (opt_name t.name) 45 | I3ipc.Reply.pp_node_layout t.layout 46 | t.id 47 | | Dockarea -> 48 | Format.fprintf f "style=filled fillcolor=\"/accent6/6\"" 49 | end in 50 | 51 | Format.fprintf formatter "\n%s [%a]%a%a" 52 | t.id 53 | print_attrs t 54 | show_links t 55 | (Format.pp_print_list ~pp_sep:(fun _ _ -> ()) pp_print_node) t.nodes 56 | 57 | end 58 | 59 | let to_dot formatter t = begin 60 | Format.fprintf formatter "digraph G {\ 61 | node [shape=record];\ 62 | %a\ 63 | }" 64 | pp_print_node t 65 | end 66 | 67 | let to_dot tree name = begin 68 | let channel = Stdlib.open_out name in 69 | let formatter = Format.formatter_of_out_channel channel in 70 | to_dot formatter tree; 71 | Stdlib.close_out channel 72 | end 73 | 74 | let main = 75 | 76 | let%lwt conn = I3ipc.connect () in 77 | let%lwt tree = I3ipc.get_tree conn in 78 | begin match Tree.get_focused_workspace tree with 79 | | None -> 80 | Printf.eprintf "No workspace found\n%!"; 81 | exit 1 82 | | Some workspace -> 83 | let time = string_of_float @@ ceil @@ Unix.time () in 84 | let name = workspace.I3ipc.Reply.id ^ "_" ^ time ^ "gv" in 85 | to_dot workspace name; 86 | let command = Lwt_process.shell ("dot -Tpng -O " ^ name) in 87 | let%lwt _status = Lwt_process.exec command in 88 | Lwt.return @@ Unix.unlink name 89 | end 90 | 91 | let () = Lwt_main.run main 92 | --------------------------------------------------------------------------------