Linux window auto-organizer (across multiple screens)

6 months ago

One of the things that always makes me crazy is window organization on the desktop. OSX has pretty good apps to manage windows, but i always find Linux a bit lacking so i decided to roll out my own simple solution.

It's written in Elixir because i like it, but can easily be adapter for other languages since it uses wmctrl to get information about windows and define their positions. The rest is just find the windows roughly by name and assigning the positions and dimensions i want. And it works across multiple screens because apparently X11 reports positions as one single virtual screen if you are not mirroring your screens, of course.

Just put the script in a directory that your PATH contains, and don't forget to change the first line to point to the correct elixir binary path. Then you can just execute whenever you want and all your windows will pop into their positions automagically.

Happy windowing...

EDIT: Updated script to not blow up when a particular window is not open.

#!/home/void/.asdf/shims/elixir

require Logger

windows = %{
  opera: %{x: 3408, y: 0, w: 1712, h: 1403},
  terminal: %{x: 0, y: 0, w: 1584, h: 1371},
  thunderbird: %{x: 6045, y: 200, w: 995, h: 1051},
  jetbrains: %{x: 2560, y: 0, w: 2060, h: 1373},
  slack: %{x: 5120, y: 200, w: 1184, h: 1050},
  spotify: %{x: 1592, y: 0, w: 968, h: 1373}
}

defmodule WindowGatherer do
  def run(windows) do
    IO.inspect("Gathering windows...")

    {window_lines, 0} = System.cmd("wmctrl", ["-l", "-p", "-G", "-x"])
    window_lines = window_lines
    |> String.split("\n")
    |> Enum.reject(&(String.split(&1) == []))
    |> Enum.map(fn line ->
      [id, _, _, _, _, _, _ | rest] = String.split(line)
      %{
        id: id,
        names: rest |> Enum.join(" ") |> String.downcase()
      }
    end)

    fill_ids(windows, window_lines)
  end

  defp fill_ids(windows, window_lines) do
    windows
    |> Enum.map(fn {window_key, window_data} ->
      window_attributes = window_lines
        |> Enum.filter(&(&1.names =~ Atom.to_string(window_key)))
        |> Enum.reject(&is_nil/1)
        |> Enum.at(0)
      case window_attributes do
        map when is_map(window_attributes) ->
          {window_key, Map.put(window_data, :id, Map.fetch!(map, :id))}
        _ -> nil
      end
    end)
    |> Enum.reject(&is_nil/1)
    |> Map.new()
  end
end

defmodule WindowOrganizer do
  def run(windows) do
    IO.inspect("Organizing windows...")

    windows
    |> Enum.each(fn {key, win} ->
      IO.inspect("Moving #{key}...")
      System.cmd("wmctrl", [
        "-ir",
        win.id,
        "-e",
        "0,#{win.x},#{win.y},#{win.w},#{win.h}"
      ])
    end)
  end
end

windows
|> WindowGatherer.run()
|> WindowOrganizer.run()