aboutsummaryrefslogtreecommitdiffhomepage
path: root/freedowm.py
blob: e268f4400157b0ad844d8da2e3bea99faa3fa06c (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
#!/usr/bin/env python3.7

import configparser
import os

from Xlib import X, XK
from Xlib.display import Display
from Xlib.ext import randr


class FreedoWM(object):
    def __init__(self):
        self.config = configparser.ConfigParser()
        self.config.read(os.environ['HOME'] + "/.config/freedowm.ini")
        self.general = self.config["GENERAL"]
        self.keys = self.config["KEYMAP"]
        self.colors = self.config["COLORS"]
        self.programs = self.config["PROGRAMS"]
        self.mod = X.Mod1Mask if self.keys["MOD"] == "alt" else X.Mod4Mask

        self.display = Display()
        self.screen = self.display.screen()
        self.root = self.screen.root
        self.event = self.display.next_event()
        self.colormap = self.screen.default_colormap
        self.currently_focused = None
        self.tiling_state = False
        self.tiling_windows = []
        self.start = None
        self.ignore_actions = False
        self.program_stack = []
        self.program_stack_index = -1
        self.monitors = []
        self.monitor_id, self.zero_coordinate = 0, 0

        self.NET_WM_NAME = self.display.intern_atom('_NET_WM_NAME')
        self.NET_ACTIVE_WINDOW = self.display.intern_atom('_NET_ACTIVE_WINDOW')

        self.get_monitors()

    def set_listeners(self):
        # Listen for window changes
        self.root.change_attributes(
            event_mask=X.PropertyChangeMask | X.FocusChangeMask | X.StructureNotifyMask | X.SubstructureNotifyMask | X.PointerMotionMask
        )

        # Keyboard listener
        self.root.grab_key(X.AnyKey, self.mod, 1, X.GrabModeAsync, X.GrabModeAsync)

        # Button (Mouse) listeners
        self.root.grab_button(X.AnyButton, self.mod, 1,
                              X.ButtonPressMask | X.ButtonReleaseMask | X.PointerMotionMask,
                              X.GrabModeAsync, X.GrabModeAsync, X.NONE, X.NONE)

    def log(self, message):
        if self.general["DEBUG"] != "0":
            print(message)

    def get_monitors(self):
        window = self.root.create_window(0, 0, 1, 1, 1, self.screen.root_depth)
        res = randr.get_screen_resources(window).outputs

        for i in range(self.display.screen_count() + 1):
            info = randr.get_output_info(window, res[i], 0)
            crtc_info = randr.get_crtc_info(window, info.crtc, 0)
            self.monitors.append({"width": crtc_info.width, "height": crtc_info.height})

        self.log(self.monitors)
        window.destroy()

    def is_key(self, key_name):
        return self.event.type == X.KeyPress \
               and self.event.detail == self.display.keysym_to_keycode(XK.string_to_keysym(key_name))

    def window_focused(self):
        return hasattr(self.event, "child") and self.event.child != X.NONE or self.root.query_pointer().child != 0

    def set_border(self, child, color):
        if child is not None and child is not X.NONE:
            border_color = self.colormap.alloc_named_color(color).pixel
            child.configure(border_width=int(self.general["BORDER"]))
            child.change_attributes(None, border_pixel=border_color)

    def update_tiling(self):
        self.log("UPDATE TILING")
        monitor = self.monitors[self.monitor_id]
        tiling_num = len(self.tiling_windows[self.monitor_id]) + 1
        width = round(monitor["width"] / tiling_num) - 2 * int(self.general["BORDER"])
        for i, child in enumerate(self.root.query_tree().children):
            child.configure(
                stack_mode=X.Above,
                width=width,
                height=monitor["height"] - 2 * int(self.general["BORDER"]),
                x=self.zero_coordinate + width * i,
                y=0,
            )
            if child not in self.tiling_windows[self.monitor_id]:
                self.tiling_windows[self.monitor_id].append(child)

    def update_windows(self):
        new_focus = False

        # Configure new window
        if self.event.type == X.CreateNotify:
            if not self.ignore_actions:
                self.log("NEW WINDOW")
                window = self.event.window
                if self.root.query_pointer().root_x > self.monitors[0]["width"]:
                    self.monitor_id = 1
                    self.zero_coordinate = self.monitors[0]["width"]
                    x_center = round(self.monitors[1]["width"] / 2 + self.monitors[0]["width"])
                    y_center = round(self.monitors[1]["height"] / 2)
                else:
                    x_center = round(self.monitors[0]["width"] / 2)
                    y_center = round(self.monitors[0]["height"] / 2)
                self.program_stack.append(window)
                self.program_stack_index = len(self.program_stack) - 1
                if self.tiling_state:
                    self.update_tiling()
                else:
                    window.configure(
                        stack_mode=X.Above,
                        x=x_center - round(window.get_geometry().width / 2),
                        y=y_center - round(window.get_geometry().height / 2),
                    )
                self.root.warp_pointer(x_center, y_center)
            else:
                self.ignore_actions = False

        # Remove closed window from stack
        if self.event.type == X.DestroyNotify:
            self.program_stack.remove(self.event.window)
            if self.tiling_state:
                self.tiling_windows[self.monitor_id].remove(self.event.window)
                self.update_tiling()

        # Set focused window "in focus"
        if self.window_focused() and not self.ignore_actions:
            if hasattr(self.event, "child") and self.event.child != X.NONE \
                    and self.event.child != self.currently_focused:
                new_focus = True
                self.currently_focused = self.event.child
                self.currently_focused.configure(stack_mode=X.Above)
                self.program_stack_index = self.program_stack.index(self.currently_focused)
            elif self.root.query_pointer().child != self.currently_focused:
                new_focus = True
                self.currently_focused = self.root.query_pointer().child
                self.currently_focused.configure(stack_mode=X.Above)
                self.program_stack_index = self.program_stack.index(self.currently_focused)

        # Set all windows to un-focused borders
        if self.event.type == X.FocusOut and not self.ignore_actions or new_focus:
            for child in self.root.query_tree().children:
                self.log("RESET BORDERS")
                self.set_border(child, self.colors["INACTIVE_BORDER"])

        # Set focused window border
        if self.event.type == X.FocusIn and not self.ignore_actions or new_focus:
            child = self.root.query_pointer().child
            self.currently_focused = child
            if child != 0:
                self.log("FOCUS")
                child.configure(stack_mode=X.Above)
                self.set_border(child, self.colors["ACTIVE_BORDER"])

        self.display.sync()

    # Check for actions until exit
    def main_loop(self):
        self.set_listeners()
        while 1:
            self.event = self.display.next_event()
            self.update_windows()

            # Move window (MOD + left click)
            if self.event.type == X.ButtonPress and self.event.child != X.NONE:
                attribute = self.event.child.get_geometry()
                self.start = self.event

            # Resize window (MOD + right click)
            elif self.event.type == X.MotionNotify and self.start:
                x_diff = self.event.root_x - self.start.root_x
                y_diff = self.event.root_y - self.start.root_y
                self.start.child.configure(
                    x=attribute.x + (self.start.detail == 1 and x_diff or 0),
                    y=attribute.y + (self.start.detail == 1 and y_diff or 0),
                    width=max(1, attribute.width + (self.start.detail == 3 and x_diff or 0)),
                    height=max(1, attribute.height + (self.start.detail == 3 and y_diff or 0))
                )

            # Cycle between windows (MOD + Tab) // X11's "tab" keysym is 0, but it's 23
            if self.event.type == X.KeyPress and self.event.detail == int(self.keys["CYCLE"]) \
                    and len(self.program_stack) > 0:
                if self.program_stack_index + 1 >= len(self.program_stack):
                    self.program_stack_index = 0
                else:
                    self.program_stack_index += 1
                active_window = self.program_stack[self.program_stack_index]
                active_window.configure(stack_mode=X.Above)
                self.root.warp_pointer(
                    round(active_window.get_geometry().x + active_window.get_geometry().width / 2),
                    round(active_window.get_geometry().y + active_window.get_geometry().height / 2)
                )

            # Toggle tiling state (MOD + t)
            elif self.is_key(self.keys["TILE"]):
                if not self.tiling_state:
                    for i in range(self.display.screen_count() + 1):
                        self.tiling_windows.append([])
                    self.update_tiling()
                    self.tiling_state = True
                else:
                    self.tiling_windows = []
                    self.tiling_state = False

            # Close window (MOD + Q)
            elif self.is_key(self.keys["CLOSE"]) and self.window_focused():
                self.event.child.destroy()

            # Open terminal (MOD + Enter) // X11's "enter" keysym is 0, but it's 36
            elif self.event.type == X.KeyPress and self.event.detail == int(self.keys["TERMINAL"]):
                os.system(self.programs["TERMINAL"] + " &")

            # Open dmenu (MOD + D)
            elif self.is_key(self.keys["MENU"]):
                self.ignore_actions = True
                os.system(self.programs["MENU"] + " &")

            # Exit window manager (MOD + C)
            elif self.is_key(self.keys["QUIT"]):
                self.display.close()

            elif self.event.type == X.ButtonRelease:
                self.start = None


FreedoWM = FreedoWM()
FreedoWM.main_loop()