diff options
-rw-r--r-- | .gitignore | 110 | ||||
-rw-r--r-- | README.md | 21 | ||||
-rwxr-xr-x | move-to-next-monitor | 200 |
3 files changed, 247 insertions, 84 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b3f845d --- /dev/null +++ b/.gitignore @@ -0,0 +1,110 @@ +.idea +.ipynb_checkpoints + +# Created by .ignore support plugin (hsz.mobi) +### Python template +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +.static_storage/ +.media/ +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ + @@ -1,16 +1,22 @@ # move-to-next-monitor -Script to move windows from one monitor to the next in Xubuntu +Originally, this is fork of [jc00ke](https://github.com/jc00ke/move-to-next-monitor)'s `move-to-next-monitor`. -Even though I don't use Xubuntu anymore, people are still interested -in this script. Sweet! Glad it can help others. +The logic has been rewritten to Python and changed slightly. Now, it cycles window through the monitors, which don't +need to be same sized to work properly. The order of cycling could be controlled with `--horizontal-first` +and `--reverse` parameters. -I mapped `ctrl+alt+n` to exectute this script. +Script requires to work properly: + * `xrandr` + * `xdotool` + * `wmctrl` + * `xprop` + * `python` ## Usage ``` -wget https://raw.githubusercontent.com/jc00ke/move-to-next-monitor/master/move-to-next-monitor +wget https://raw.githubusercontent.com/vanaoff/move-to-next-monitor/master/move-to-next-monitor chmod +x move-to-next-monitor mv move-to-next-monitor /somewhere/in/your/$PATH ``` @@ -19,11 +25,8 @@ mv move-to-next-monitor /somewhere/in/your/$PATH [We have one](code_of_conduct.md), and you're expected to follow it. -## Support - -Since I don't use Xubuntu and therefore don't have a way to test changes, I'm going to rely on others to QA and give me feedback. - ## Thanks * [icyrock](http://icyrock.com/blog/2012/05/xubuntu-moving-windows-between-monitors/) post for initial development * [@jordansissel](https://github.com/jordansissel) for his excellent [xdotool](https://github.com/jordansissel/xdotool) +* [jc00ke](https://github.com/jc00ke/move-to-next-monitor) diff --git a/move-to-next-monitor b/move-to-next-monitor index 07a6ff0..33affce 100755 --- a/move-to-next-monitor +++ b/move-to-next-monitor @@ -1,75 +1,125 @@ -#!/bin/sh -# -# Move the current window to the next monitor. -# -# Also works only on one X screen (which is the most common case). -# -# Props to -# http://icyrock.com/blog/2012/05/xubuntu-moving-windows-between-monitors/ -# -# Unfortunately, both "xdotool getwindowgeometry --shell $window_id" and -# checking "-geometry" of "xwininfo -id $window_id" are not sufficient, as -# the first command does not respect panel/decoration offsets and the second -# will sometimes give a "-0-0" geometry. This is why we resort to "xwininfo". - -screen_width=$(xdpyinfo | awk '/dimensions:/ { print $2; exit }' | cut -d"x" -f1) -screen_height=$(xdpyinfo | awk '/dimensions:/ { print $2; exit }' | cut -d"x" -f2) -display_width=$(xdotool getdisplaygeometry | cut -d" " -f1) -display_height=$(xdotool getdisplaygeometry | cut -d" " -f2) -window_id=$(xdotool getactivewindow) - -# Remember if it was maximized. -window_horz_maxed=$(xprop -id "$window_id" _NET_WM_STATE | grep '_NET_WM_STATE_MAXIMIZED_HORZ') -window_vert_maxed=$(xprop -id "$window_id" _NET_WM_STATE | grep '_NET_WM_STATE_MAXIMIZED_VERT') - -# Un-maximize current window so that we can move it -wmctrl -ir "$window_id" -b remove,maximized_vert,maximized_horz - -# Read window position -x=$(xwininfo -id "$window_id" | awk '/Absolute upper-left X:/ { print $4 }') -y=$(xwininfo -id "$window_id" | awk '/Absolute upper-left Y:/ { print $4 }') - -# Subtract any offsets caused by panels or window decorations -x_offset=$(xwininfo -id "$window_id" | awk '/Relative upper-left X:/ { print $4 }') -y_offset=$(xwininfo -id "$window_id" | awk '/Relative upper-left Y:/ { print $4 }') -x=$(( x - x_offset)) -y=$(( y - y_offset)) - -# Compute new X position -new_x=$((x + display_width)) -# Compute new Y position -new_y=$((y + display_height)) - -# If we would move off the right-most monitor, we set it to the left one. -# We also respect the window's width here: moving a window off more than half its width won't happen. -width=$(xdotool getwindowgeometry "$window_id" | awk '/Geometry:/ { print $2 }'|cut -d"x" -f1) -if [ "$(( new_x + width / 2))" -gt "$screen_width" ]; then - new_x=$((new_x - screen_width)) -fi - -height=$(xdotool getwindowgeometry "$window_id" | awk '/Geometry:/ { print $2 }'|cut -d"x" -f2) -if [ "$((new_y + height / 2))" -gt "$screen_height" ]; then - new_y=$((new_y - screen_height)) -fi - -# Don't move off the left side. -if [ "$new_x" -lt 0 ]; then - new_x=0 -fi - -# Don't move off the bottom -if [ "$new_y" -lt 0 ]; then - new_y=0 -fi - -# Move the window -xdotool windowmove "$window_id" "$new_x" "$new_y" - -# Maximize window again, if it was before -if [ -n "${window_horz_maxed}" ] && [ -n "${window_vert_maxed}" ]; then - wmctrl -ir "$window_id" -b add,maximized_vert,maximized_horz -elif [ -n "${window_horz_maxed}" ]; then - wmctrl -ir "$window_id" -b add,maximized_horz -elif [ -n "${window_vert_maxed}" ]; then - wmctrl -ir "$window_id" -b add,maximized_vert -fi +#!/bin/env python + +import subprocess +import re + +import sys + + +def execute(command): + return subprocess.run(command.split(), stdout=subprocess.PIPE).stdout.decode('utf8').strip().split('\n') + + +HORIZONTAL_FIRST = (2, 3) +VERTICAL_FIRST = (3, 2) +display_coordinates_split = re.compile('[x+]') + + +def get_display_positions(): + for x in execute('xrandr'): + split = x.split(' ') + if 'connected' in split: + position = [display_coordinates_split.split(t) for t in split if t != 'primary'][2] + yield [int(x) for x in position] + + +def get_active_window_id(): + return int(execute('xdotool getactivewindow')[0]) + + +def get_window_state(window_id): + window_state = execute('xprop -id %s _NET_WM_STATE' % window_id)[0] + return dict(hmaximized='_NET_WM_STATE_MAXIMIZED_HORZ' in window_state, + vmaximized='_NET_WM_STATE_MAXIMIZED_VERT' in window_state, + focused='_NET_WM_STATE_FOCUSED' in window_state) + + +def set_window_state(window_id, state): + if state['hmaximized'] or state['vmaximized']: + props = ['add'] + if state['hmaximized']: + props.append('maximized_horz') + if state['vmaximized']: + props.append('maximized_vert') + execute('wmctrl -ir %s -b %s' % (window_id, ','.join(props))) + + +def unmaximize(window_id): + execute('wmctrl -ir %s -b remove,maximized_vert,maximized_horz' % window_id) + + +def get_window_position(window_id): + abs_x, abs_y, rel_x, rel_y, width, height = [0] * 6 + for row in execute('xwininfo -id %s' % window_id): + if 'Absolute upper-left X:' in row: + abs_x = int(row.split()[-1]) + elif 'Absolute upper-left Y:' in row: + abs_y = int(row.split()[-1]) + elif 'Relative upper-left X:' in row: + rel_x = int(row.split()[-1]) + elif 'Relative upper-left Y:' in row: + rel_y = int(row.split()[-1]) + elif 'Width:' in row: + width = int(row.split()[-1]) + elif 'Height:' in row: + height = int(row.split()[-1]) + return abs_x - rel_x, abs_y - rel_y, width + rel_x, height + rel_y + + +def point_seq(window_id): + x, y, x_size, y_size = get_window_position(window_id) + for y in range(y, y_size + 1, int(y_size / 2)): + yield x + int(x_size / 2), y + yield x, y + yield x + x_size, y + + +def get_display_index(point, displays): + x, y = point + for i, display in enumerate(displays): + width, height, woffset, hoffset = display + if woffset <= x < woffset + width and hoffset <= y < hoffset + height: + return i + + +def get_display_index_of_window(window_id, displays): + for point in point_seq(window_id): + return get_display_index(point, displays) + + +def next_display_index(index, displays, step=1): + return (index + step) % len(displays) + + +def new_window_position(window_id, displays, step=1): + window_pos = wx, wy = get_window_position(window_id) + display_index = get_display_index(window_pos, displays) + owidth, oheight, owoffset, ohoffset = displays[display_index] + xrel = (wx - owoffset) / owidth + yrel = (wy - ohoffset) / oheight + nwidth, nheight, nwoffset, nhoffset = displays[next_display_index(display_index, displays, step)] + return xrel * nwidth + nwoffset, yrel * nheight + nhoffset + + +def move_to_next_monitor(window_id, displays, step=1): + state = get_window_state(window_id) + unmaximize(window_id) + xnew, ynew = new_window_position(window_id, displays, step) + execute('xdotool windowmove %s %s %s' % (window_id, xnew, ynew)) + set_window_state(window_id, state) + + +if __name__ == '__main__': + if '--horizontal-first' in sys.argv: + sorting = HORIZONTAL_FIRST + else: + sorting = VERTICAL_FIRST + + displays = sorted(get_display_positions(), key=lambda x: [x[i] for i in sorting]) + + if len(displays) > 1: + if '--reverse' in sys.argv: + step = -1 + else: + step = 1 + move_to_next_monitor(get_active_window_id(), displays, step) |