ch11ng / exwm

Emacs X Window Manager
2.85k stars 136 forks source link

[RFC] improving exwm-randr #202

Open fjl opened 7 years ago

fjl commented 7 years ago

I'd like to improve exwm-randr so it fits my workflow better. At this time, the randr module allows one basic mode of operation where workspaces can be assigned to (named) outputs statically. This kind of works but I don't find it convenient. It would be nice if exwm-randr could just distribute workspaces on available outputs without having to configure anything.

When a new output comes up, it would just pick a workspace that is currently not displayed and show it there. The existing config variable could be used as a preference list, indicating which workspace should be chosen.

A new set of functions for managing outputs could be added:

exwm-randr-workspace-switch would select a different workspace on the current output. If the workspace is currently displayed somewhere else, this command would also move the current workspace to there (basically swapping them).

exwm-randr-disable-output would ensure that no workspace will be displayed on the given output. This is useful for cases where laptop lid is closed. It would be super nice to prompt for outputs interactively here.

exwm-randr-enable-output would be the opposite of the above command.

ch11ng commented 7 years ago

I think it's a good opportunity to improve the RandR module by removing its dependency on xrandr. The current implementation is damn simple and consists of only around 100 LOC, so I think it'd be easy to do this. We should keep it backward compatible by checking if exwm-randr-workspace-output-plist is set.

We need to work out a scheme of how to configure RandR outputs (including their geometries and whether some should be disabled) and where the workspaces should be put. Any ideas?

medranocalvo commented 7 years ago

Hello. I don't use multiple monitors at the moment. The behaviour you propose sounds similar to how XMonad used to manage workspaces in multiple displays. I found it quite sensible.

zabbal commented 7 years ago

I think RandR outputs could be configured automatically by smth like kscreen2 (in case of kde) or alike. I also find this proposal very similar to xmonad and hence a very good idea.

bendlas commented 7 years ago

I'd really like to see multi-monitor support improved. Are there any work-in-progress branches for this?

ch11ng commented 7 years ago

@bendlas I don't have any yet. Could you elaborate on the improvements you want?

bendlas commented 7 years ago

I need a way to move workspaces between outputs dynamically. In its simplest form that could just be the possibility to modify exwm-randr-workspace-output-plist at runtime to make it possible for the user to handle it anyway they want.

The look and feel I'm aiming for:

I'm willing to help make this happen, but I have no experience with Xlib programming, let alone programming a WM.

ch11ng commented 7 years ago

@bendlas Thank you for the proposal! It seems what you only want is to make the currently static configuration method dynamic. It's of course viable but what I want is to also make other features like configuring RandR outputs possible. I'm a bit reluctant to implement the dynamic configuration feature first TBH.

bendlas commented 7 years ago

I was trying to thing of a minimal thing that I could use to experiment with and implement what I want, while leaving it on equal footing with alternative behavior.

Of course, from a designer's POV, that's pretty much a cop-out. OTOH, even for supporting a more declarative approach, the underlying Workspace to Output mapping would have to be dynamized, no?

Btw, there is, in fact, a completely alternative approach that crossed my mind and since you seem to be looking for such things, let me share it:

What if we have workspaces be separate entities from frames and map workspaces into frames? Exwm could then just keep a fixed frame per output and the user could display their workspaces however they like, subject only to the restriction of X buffers only being displayed in their most recently focussed containing frame.

Such a workspace module would have the advantage of being useful even outside of exwm (in fact, I have wanted such a thing even before becoming an exwm user), thus receiving wider attention and support.

ch11ng commented 7 years ago

@bendlas The concern is the potential change of public interfaces (something like exwm-randr-workspace-output-plist) which can be both confusing and annoying. So before implementing the dynamic mapping feature other features should at least be planned in advanced.

The "alternative approach" you proposed requires some fundamental changes in EXWM. I don't know if this is superior but it will unlikely happen in the near future.

bendlas commented 7 years ago

@ch11ng OK, it's good to know now, what you're aiming for. I do still think that a dynamic output-plist would be sufficiently data-driven to implement any other behavior on top, without requiring breakage, but yours is probably the better judgement.

FWIW, since the mapping is completely static right now, the dynamic mapping could start out as a completely separate module, that would just initialize from exwm-randr-workspace-output-plist, and thus retain complete compatibility per definition. The static plist mapping could then later be deprecated, if at all.

The "alternative approach" of workspaces being separated from frames just represents what would have been the least suprise to me. As far as I can tell, it should be functionally equivalent to dynamic output mapping anyway.

The only real alternative that comes to my mind, is to expand workspaces across multiple frames and thus switch all outputs at once, when switching workspaces. That's the way stumpwm does it but I do like the workspace-per-output approach of exwm much better. Especially since with dynamic mapping, stumpwm's behavior could be mimicked, but the reverse would be quite awkward.

bendlas commented 7 years ago

ad the original thread on how to configure the outputs in the first place: I don't think that exwm should inherently deal with that (any more than the current hook to call xrandr or similar). That's the job of the display-manager, isn't it? I'm comforable with using xrandr directly (e.g. when attaching a beamer) and people who aren't can use gnome-control-center or any of the other graphical tools.

In my view, exwm should only read the randr state and use that as an input to it's workspace-output mapping. Semi-automatically discovering and configuring outputs, while retaining manual configurability seems like a real piece of work to me and it should not hold up our ability to use already configured outputs well.

Of course, if workspace-output mapping was moved into core, exwm-randr could be used to develop a configuration system for the outputs themselves, but entangling output mapping and output configuration feels like a layering violation to me.

bvk commented 7 years ago

@bendlas

I have the following dynamic exwm-xrandr configuration which is working (somewhat) reasonably for me. I primarily use external monitor at work and home. But at work I connect the monitor using a HDMI cable where as at home I connect the monitor using DisplayPort cable. So, I need to be able to switch displays dynamically after I resume my laptop from sleep. Here is my configuration:

;; Enable two xrandr outputs one named 'default' and another named 'other'.
(defun my-exwm-xrandr-enable (default other)
  (start-process-shell-command
   "xrandr" nil (concat "xrandr --output " other " --right-of " default " --auto")))

;; Enable only one xrandr output named 'default' by disabling others.
(defun my-exwm-xrandr-disable (default)
  (start-process-shell-command
   "xrandr" nil (concat "xrandr --output " default " --auto")))

;; Update exwm-randr-workspace-output-plist with two outputs named
;; 'default' and 'other'.  If the 'other' output is same as 'default'
;; then all workspaces will be redirected to the 'default' output.
(defun my-exwm-xrandr-config (default other)
  (setq exwm-randr-workspace-output-plist
    (progn
      (setq result (list 0 default))
      (setq index 1)
      (while (< index exwm-workspace-number)
        (setq result (append result (list index other)))
        (setq index (1+ index)))
      result)))

;; Dynamically find the active xrandr outputs and update exwm
;; workspace configuration and enable xrandr outputs appropriately.
(defun my-exwm-xrandr-hook (default)
  (let* ((connected-cmd "xrandr -q|awk '/ connected/ {print $1}'")
     (connected (process-lines "bash" "-lc" connected-cmd)))
    (cond ((member "DP1" connected)
       (progn (my-exwm-xrandr-config default "DP1")
          (my-exwm-xrandr-enable default "DP1")))
      ((member "DP2" connected)
       (progn (my-exwm-xrandr-config default "DP2")
          (my-exwm-xrandr-enable default "DP2")))
      ((member "HDMI1" connected)
       (progn (my-exwm-xrandr-config default "HDMI1")
          (my-exwm-xrandr-enable default "HDMI1")))
      ((member "HDMI2" connected)
       (progn (my-exwm-xrandr-config default "HDMI2")
          (my-exwm-xrandr-enable default "HDMI2")))
      (t (progn (my-exwm-xrandr-config default default)
            (my-exwm-xrandr-disable default))))))

(setq exwm-randr-screen-change-hook
      (lambda () (my-exwm-xrandr-hook "eDP1")))

Note that "eDP1" display refers to the Laptop display output. My laptop has two DisplayPort outputs (DP1 and DP2) and two HDMI outputs (HDMI1 and HDMI2). When external monitor is connected above configures workspace-0 to laptop display and all other workspaces to the external monitor; otherwise configures all workspaces to the laptop display, i.e., when no monitor is connected.

I still face some problems -- like external monitor is not detected -- frequently, in which case I use kill-emacs to restart the X session.

@ch11ng

Is there a way I can dynamically send ScreenChangeNotify event to exwm, perhaps using a key-binding?

ch11ng commented 7 years ago

@bvk Do not use start-process-shell-command as the commands can be executed out-of-order. Use something like shell-command instead.

Is there a way I can dynamically send ScreenChangeNotify event to exwm, perhaps using a key-binding?

The event is generated by X server when it detects something changes. Why would you need that?

bvk commented 7 years ago

Thanks for the shell-command suggestion. I didn't know about that.

I noticed that exwm doesn't call exwm-randr-screen-change-hook when I disconnect the HDMI cable (but DisplayPort cable works just fine). I was hoping if I can generate ScreenChangeNotify event manually, exwm will invoke exwm-randr-screen-change-hook which will update xrandr outputs as necessary.

Currently I run xrandr -q command (through a keybinding) every time after I disconnect the HDMI cable and it seems to trigger the exwm screen change hook.

ch11ng commented 7 years ago

Only X server is responsible for generating that event. I have no HDMI port to produce the problem but could you tell if the hook is run but failed to configure the screen when you disconnect the HDMI cable?

bvk commented 7 years ago

I think the hook is not invoked, but I can try again and confirm later.

I usually run 'xrandr -q' after disconnecting the HDMI output and it some how triggers the hook which fixes the outputs.

bvk commented 7 years ago

I can confirm that hook is not invoked when I disconnect the HDMI output.

ch11ng commented 7 years ago

Then you have to use your trick to do that, though I'm still wondering why querying RANDR is even relevant.

QiangF commented 6 years ago

@bvk I guess xrandr always output the primary display in front of other displays, perhaps it is not necessary to set the default specifically.

(defun my-exwm-xrandr-hook ()
(interactive)
(let* ((connected-cmd "xrandr -q|awk '/ connected/ {print $1}'")
    (connected (process-lines "bash" "-lc" connected-cmd))
    (primary (nth 0 connected))
    (other (nth 1 connected))
    (previous (delete-dups (seq-remove
                'integerp
                exwm-randr-workspace-output-plist))))
    (progn 
    (cond (other
    (progn (my-exwm-xrandr-config primary other)
        (my-exwm-xrandr-two-outputs primary other)))
    (t (progn (my-exwm-xrandr-config primary primary)
            (mapcar 'my-exwm-xrandr-off
                (delete primary previous)))))
    (exwm-randr--refresh)
    (exwm--log "Display: %s refreshed." connected))))

(setq exwm-randr-screen-change-hook
    (lambda () (my-exwm-xrandr-hook)))

And I just found a handy python script which can remember profiles of screen arrangement, it also use udev rules to detect monitor change. https://github.com/phillipberndt/autorandr

ch11ng commented 6 years ago

@bvk I guess xrandr always output the primary display in front of other displays,

+1. From the spec:

RRSetOutputPrimary marks 'output' as the primary output for the screen with the same root window as 'window'. This output's CRTC will be sorted to the front of the list in Xinerama and RANDR geometry requests for the benefit of older applications.

lsp-ableton commented 4 years ago

I've done some work to get my setup to work how I wanted it to which might help inform decisions about how to move forward with this.

First, I configure my displays using xrandr in the exwm-randr-screen-change-hook. This took more effort than I'd like, so it might be nice to try to abstract some of this away for exwm, but I don't know if it really makes sense. It seems like a common use case is switching which displays are active based on what displays are connected. For example, I want to deactivate my built-in display when my external monitors are connected.

Second, I wanted a way to always open a certain workspace in the monitor that is not currently focused. My setup is that I have one workspace for code, one for communications, one for web search / reading documents, and one for org-mode. I wanted my org-mode workspace to always open in the other monitor, because I use it to take notes on what I'm reading or to look at tasks.

To preface this, I also assigned names to my workspaces to help me remember which one is which. This could be left out, but I preferred to work with the workspace names in my display configuration.

(setq exwm-workspace-name-alist '((0 . "Code")
                                         (1 . "Comms")
                                         (2 . "Info")
                                         (3 . "Org")
                                         (4 . "Output")))

Then I defined my displays and also a data structure to store which workspace should be displayed in which monitor, along with a helper function that converts the alist to exwm-workspace-monitor-plist:

    (setq exwm-monitor-list '("DP1-1" "DP1-2"))

    (setq exwm-workspace-monitor-alist '(("Comms" . "DP1-1")
                                         ("Info" . "DP1-1")
                                         ("Output" . "DP1-1")
                                         ("Org" . "DP1-2")
                                         ("Code" . "DP1-2")))

    (defun update-exwm-randr-workspace-monitor-plist ()
      "Update exwm-randr-workspace-monitor-plist based on the current
       value of exwm-workspace-monitor-alist"
      (setq exwm-randr-workspace-monitor-plist (mapcan (lambda (workspace->monitor)
                                                         (let ((workspace-number (car (rassoc (car workspace->monitor)
                                                                                               exwm-workspace-name-alist)))
                                                                (monitor (cdr workspace->monitor)))
                                                           (list workspace-number monitor)))
                                                       exwm-workspace-monitor-alist)))
    (update-exwm-randr-workspace-monitor-plist)

Finally, I define a function to switch to a target workspace on the other monitor. It uses exwm-workspace-monitor-alist to determine which monitor the current workspace is displayed on, then uses exwm-monitor-list to get the other monitor. It then updates exwm-workspace-monitor-alist so that the target display will be displayed on the other monitor, and finally updates exwm-workspace-monitor-plist and calls exwm-randr-refresh before making the workspace switch.

(defun exwm-workspace-switch-create-other-monitor (i)
      (let ((target-workspace (alist-get i exwm-workspace-name-alist))
            (current-workspace-position (exwm-workspace--position exwm-workspace--current)))
        (when (not (= i current-workspace-position))
          (let* ((current-workspace (alist-get current-workspace-position exwm-workspace-name-alist))
                 (current-monitor (cdr (assoc current-workspace exwm-workspace-monitor-alist)))
                 (other-monitor (car (seq-filter (lambda (x)
                                                   (not (string= x current-monitor)))
                                                 exwm-monitor-list))))
            (when (and other-monitor
                       (not (string= (cdr (assoc target-workspace exwm-workspace-monitor-alist))
                                     other-monitor)))
              (setf (cdr (assoc target-workspace exwm-workspace-monitor-alist)) other-monitor)
              (update-exwm-randr-workspace-monitor-plist)
              (exwm-randr-refresh))
            (exwm-workspace-switch-create i)))))

For now, this is all I really need in terms of multi-monitor support. If there's any interest in officially adding something like this to exwm, I'm happy to do it, but for now I'm assuming this is a pretty specific to my configuration.

ch11ng commented 4 years ago

[...] so it might be nice to try to abstract some of this away for exwm [...]

That's what we are looking for, a simple but extensible abstraction of multi-monitor setups, and a set of operations we can perform on it.

lsp-ableton commented 4 years ago

[...] so it might be nice to try to abstract some of this away for exwm [...]

That's what we are looking for, a simple but extensible abstraction of multi-monitor setups, and a set of operations we can perform on it.

Maybe we should discuss in more detail? I think what this abstraction should do is allow you to easily express things like:

I could see things like this being defined in a declarative config where the order that the displays are defined in determines their index from exwm's point of view. The first monitor listed in the config that's currently connected is monitor 0, the next monitor 1, etc. For example, let's say that displays X and Y belong to one workstation, and W and Z belong to a different workstation. If we define the monitors in the order X, Y, W, Z, then the mapping will be (X 0 Y 1) at the first workstation and (W 0 Z 1) at the second workstation, because even though X and W are physically different, they have the same role.

That would be the declarative configuration part, but I also have a few interactive command ideas. I think you want to be able to open workspaces in a specific monitor and also have the option of switching between monitors rather than workspaces.

I'm fairly sure this can all be done in a backwards compatible way by modifying the monitor plist and then refreshing. If someone has a config that doesn't make use of the new options, then we just don't modify that list and it works the same way as before.

Also, I'm not really understanding the difference between this issue and https://github.com/ch11ng/exwm/issues/527. Should one be closed, or am I missing the distinction?

ch11ng commented 4 years ago

Not sure if there is any scenario left out in your design. My idea was once reworking workspaces as frame configurations and making each monitor a frame so users can fallback to native (and possibly familiar) Emacs commands and configurations. But there are some restrictions I'm not quite clear how to deal with yet, such as how to apply a frame configuration to another frame.

Also, I'm not really understanding the difference between this issue and #527.

Not exactly the same. That issue mainly focus on replacing xrandr with an XELB implementation which AFAIK is usually not part of a window manager but a separate tool. Of course such a tool could be included in EXWM if it proved useful.

lsp-ableton commented 4 years ago

Not sure if there is any scenario left out in your design.

There may be, but I have trouble thinking of one. I would tend towards making an abstraction that covers the majority of use cases, and then let users with highly irregular setups fall back to modifying exwm-randr-workspace-monitor-plist directly. Still, if we make sure that the configuration can be different based on the current combination of connected displays, then I don't see why anyone would need to do this.

My idea was once reworking workspaces as frame configurations and making each monitor a frame so users can fallback to native (and possibly familiar) Emacs commands and configurations.

I see how it makes sense to reuse existing features to handle this (i.e. display-monitor-attributes-list), but even if we went that route, it would still be nice to have an abstraction on top of this to automatically handle changes to the connected monitor list, and if we're falling back to native commands/configurations then it seems like this would fall outside of the scope of exwm. Maybe if we have an option to let exwm manage display-monitor-attributes-list, then that covers the case where someone has existing frame configs that they want to reuse for exwm while also making it easy for users that are unfamiliar with that part of emacs to easily set up multiple monitors. I've never configured frames in emacs before, so I'm not really sure how it's being used currently. I wonder if it would really make sense to reuse these configs in the context of exwm.

But there are some restrictions I'm not quite clear how to deal with yet, such as how to apply a frame configuration to another frame.

Could you go into more detail about this if you want this change to be made?

ch11ng commented 4 years ago

Actually, instead of working out a new way to configure exwm-randr-workspace-monitor-plist, we can utilize xrandr --setmonitor and xrandr --delmonitor to configure monitors in a quite flexible way, as monitor names and geometries are decoupled from physical displays.

Could you go into more detail about this if you want this change to be made?

That is, if we move a workspace to another monitor (either initiated by the user, or due to disconnection of the previous monitor) we'll have to apply the frame configuration to another one.