Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

exclusive groups: reloading and floating weirdness #4139

Open
1 task done
PhoenixClank opened this issue Jan 27, 2023 · 16 comments
Open
1 task done

exclusive groups: reloading and floating weirdness #4139

PhoenixClank opened this issue Jan 27, 2023 · 16 comments
Assignees

Comments

@PhoenixClank
Copy link

The issue:

qtile version: 0.22.1

I have an exclusive group with some matches. When I cause a non-matching window to spawn in that group, qtile automatically creates a new group and moves the window there.
However, I can now freely move that window back into the exclusive group where it doesn't belong, and that works without errors.

Also, my config only has one group (that being the exclusive one), and when reloading, all non-persistent groups get removed and their windows get moved into the exclusive group where they don't belong. If the above is considered a feature, at least this right here should be a bug.

Required:

  • I have searched past issues to see if this bug has already been reported.
@elParaguayo
Copy link
Member

Thanks for posting this. Can you share your config?

@PhoenixClank
Copy link
Author

Sure.

import subprocess
import time

from dbus_next import Variant

from libqtile import qtile, hook, layout
from libqtile.config import Key, Click, Drag, Match, Group
from libqtile.lazy import lazy
from libqtile.backend.wayland import InputConfig

import mydbus



################################################################################
#                               config variables                               #
################################################################################

dgroups_key_binder = None
dgroups_app_rules = []  # type: list
follow_mouse_focus = True
bring_front_click = False
cursor_warp = False
auto_fullscreen = True
focus_on_window_activation = 'focus'
reconfigure_screens = True
wmname = 'LG3D'



################################################################################
#                               startupp-y stuff                               #
################################################################################

@hook.subscribe.startup
def startup(*args):
	subprocess.call(['~/.config/qtile/autostart'], shell=True)

#@hook.subscribe.resume
#def resume(*args):
#	subprocess.call(['swaylock'])



################################################################################
#                                    inputs                                    #
################################################################################

wl_input_rules = {
	'type:keyboard': InputConfig(kb_layout='de', kb_variant='deadgraveacute', kb_repeat_delay=250, kb_repeat_rate=60),
	'type:touchpad': InputConfig(natural_scroll=True, tap=True),
}


bright_min = 0
bright_max = 255
bright_notif_id = 0

# TODO: make this async (because of notifs)
@lazy.function
def brightness(qtile, *, up):
	global bright_notif_id
	with open('/sys/class/backlight/amdgpu_bl0/brightness', 'r t') as file:
		bright_curr = int(file.read())

		if up: bright_next = min(bright_max, int((bright_curr   1) * 2 - 1))
		else: bright_next = max(bright_min, int((bright_curr   1) / 2 - 1))

		file.truncate(0)
		print(int(bright_next), file=file)

		bright_notif_id = mydbus.notify("backlight brightness", hints={'value': Variant('u', int(100 * (bright_next - bright_min) / (bright_max - bright_min)))}, expire_timeout=1500, replaces_id=bright_notif_id)

@lazy.function
def show_window_info(qtile):
	mydbus.notify("window info", "name: {}\nWM class: {}".format(qtile.current_window.name, qtile.current_window.get_wm_class()))


wm = 'mod1' # control qtile with Alt X
launch = 'mod4' # quick-launch stuff with Win Y

keys = [
	Key([launch], 't', lazy.spawn('kitty'), desc="[T]erminal emulator"),
	Key([launch], 'f', lazy.spawn('pcmanfm-qt'), desc="[F]ile browser"),
	Key([launch], 'e', lazy.spawn('featherpad'), desc="text [E]ditor"),
	Key([launch], 'a', lazy.spawn('pavucontrol-qt'), desc="Pulse[A]udio settings"),
	Key([launch], 'q', lazy.spawn('kitty bpytop'), desc="task manager (not [Q]ps)"),
	Key([launch], 'b', lazy.spawn('firefox'), desc="web [B]rowser"),
	Key([launch], 'p', lazy.spawn('/home/felix/tor-browser-prompt'), desc="[P]rivacy-centric browser (Tor Browser)"),
	Key([launch], 'k', lazy.spawn('keepassxc'), desc="password manager ([K]eePassXC)"),

	Key([], 'XF86MonBrightnessUp', brightness(up=True)),
	Key([], 'XF86MonBrightnessDown', brightness(up=False)),
	# TODO: volume
	# TODO: MPRIS

	Key([wm], 'w', lazy.layout.up(), desc="move focus up"),
	Key([wm], 'a', lazy.layout.left(), desc="move focus left"),
	Key([wm], 's', lazy.layout.down(), desc="move focus down"),
	Key([wm], 'd', lazy.layout.right(), desc="move focus right"),
	Key([wm], 'Tab', lazy.layout.next(), desc="move focus to next window"),
	Key([wm], 'space', lazy.next_layout(), desc="(un-)maximize"),
	Key([wm], 'x', lazy.window.toggle_floating(), desc="(un-)float window"),
	Key([wm, 'shift'], 'w', lazy.layout.grow_up(), desc="grow window up"),
	Key([wm, 'shift'], 'a', lazy.layout.grow_left(), desc="grow window left"),
	Key([wm, 'shift'], 's', lazy.layout.grow_down(), desc="grow window down"),
	Key([wm, 'shift'], 'd', lazy.layout.grow_right(), desc="grow window right"),
	Key([wm], 'F4', lazy.window.kill(), desc="close window"),

	Key([wm, 'control'], 'r', lazy.restart() if qtile.core.name == 'x11' else lazy.reload_config(), desc="reload qtile"),
	Key([wm, 'control'], 'q', lazy.shutdown(), desc="exit qtile"),
]
# TODO find alternatives for these
if qtile.core.name == 'wayland': keys  = [
	Key([launch], 'r', lazy.spawn('bemenu-run'), desc="[R]un"),
	Key([launch], 'l', lazy.spawn('swaylock'), desc="[L]ock screen"),
	Key([launch], 'Print', lazy.spawn('grim'), desc="take screenshot"),
]


mouse = [
	Drag([wm], 'Button1', lazy.window.set_position_floating(), start=lazy.window.get_position()),
	Drag([wm], 'Button3', lazy.window.set_size_floating(), start=lazy.window.get_size()),
	Click([wm], 'Button2', lazy.window.kill()),
	Click([wm, 'shift'], 'Button2', show_window_info()),
]



################################################################################
#                                   windows                                    #
################################################################################

layouts = [
	layout.Bsp(border_width=0),
	layout.Max(),
	# Try more layouts by unleashing below layouts.
	#layout.Stack(num_stacks=2),
	#layout.Columns(),
	#layout.Matrix(),
	#layout.MonadTall(),
	#layout.MonadWide(),
	#layout.RatioTile(),
	#layout.Tile(),
	#layout.TreeTab(),
	#layout.VerticalTile(),
	#layout.Zoomy(),
]

floating_layout = layout.Floating(float_rules=[
	*layout.Floating.default_float_rules,
	Match(func=lambda win: bool(win.is_transient_for())),
	Match(wm_class='zenity'),
	Match(wm_class='firefox', title="Firefox — Sharing Indicator"),
], border_width=2, border_focus='#ff7f00', border_normal='#bf1f00')


from libqtile.backend.base import Internal
from libqtile.backend.wayland.layer import LayerStatic
from libqtile.widget.base import _Widget

@hook.subscribe.focus_change
def focus_change():
	for other in qtile.windows_map.values():
		if other is qtile.current_window or isinstance(other, (Internal, LayerStatic, _Widget)):
			other.opacity = 1
		else:
			other.opacity = .75


groups = [
	Group("comms", exclusive=True, matches=[
		Match(wm_class='thunderbird'),
		Match(wm_class='signal'),
		Match(wm_class='signal-desktop'),
		Match(func=lambda win: win.floating),
	]),
]

# windows that should take up the entire screen
#_GREEDY_WINDOWS = (
#	Match(func=lambda win: win.fullscreen),
#	Match(wm_class='texstudio'),
#	Match(wm_class='org.leocad.leocad'),
#)

#@hook.subscribe.client_new
#def client_new(win):
#	# float new windows in groups with greedy windows
#	if any(win.match(m) for m in _GREEDY_WINDOWS):
#		name = str(len(qtile.groups))
#		qtile.add_group(name)
#		win.group = qtile.groups_map[name]
#	elif any(any(other.match(m) for m in _GREEDY_WINDOWS) for other in qtile.current_group.windows): win.floating = True

@hook.subscribe.client_managed
def client_managed(win):
	if not isinstance(win, LayerStatic):
		win.group.cmd_toscreen()

@lazy.function
def move_window_to_group(qtile, *, next, switch_group=False, toggle=False):
	i = qtile.groups.index(qtile.current_group)
	if i == 0 and not next: return
	elif i == len(qtile.groups) - 1 and next:
		name = 'autogen-{}'.format(time.monotonic_ns())
		qtile.add_group(name)
	else: name = qtile.groups[i 1 if next else i-1].name
	qtile.current_window.togroup(name, switch_group=switch_group, toggle=toggle)

keys  = [
	Key([wm], 'i', lazy.screen.prev_group(), desc="display previous group"),
	Key([wm], 'k', lazy.screen.next_group(), desc="display next group"),
	Key([wm, 'shift'], 'i', move_window_to_group(next=False, switch_group=True), desc="move window to previous group"),
	Key([wm, 'shift'], 'k', move_window_to_group(next=True, switch_group=True), desc="move window to next group"),
]

#groups  = [Group(i) for i in "123456789"]

#for group in groups:
#	keys  = [
#		Key([wm], group.name, lazy.group[group.name].toscreen(), desc="switch to group {}".format(group.name)),
#		Key([wm, 'shift'], group.name, lazy.window.togroup(group.name, switch_group=True), desc="move window to group {}".format(group.name)),
#	]



################################################################################
#                                     bar                                      #
################################################################################

widget_defaults = dict(
    font='Noto Sans',
    fontsize=10,
    foreground='#bfbfbf',
    padding=1,
)
extension_defaults = widget_defaults.copy()


import psutil
import mywidgets

class MultiCpuGraph(mywidgets._MultiGraph):
	def __init__(self, length, colors, **config):
		mywidgets._MultiGraph.__init__(self, length, colors, min_value=0, max_value=100, log_scale=False, **config)

	def get_values(self):
		return psutil.cpu_percent(percpu=True)


class MultiNetGraph(mywidgets._MultiGraph):
	def __init__(self, length, col_dn, col_up, **config):
		super().__init__(length, [col_dn, col_up], min_value=0, **config)
		self.dn = 0
		self.up = 0

	def get_values(self):
		stat = psutil.net_io_counters(pernic=True)
		dDn = stat['eno1'].bytes_recv   stat['wlan0'].bytes_recv - self.dn
		dUp = stat['eno1'].bytes_sent   stat['wlan0'].bytes_sent - self.up
		self.dn  = dDn
		self.up  = dUp
		return dDn, dUp

	def draw(self):
		if any(any(hist) for hist in self.hists): return super().draw()

		self.drawer.clear(self.background or self.bar.background)

		self.drawer.set_source_rgb(self.foreground)
		size = min(self.width, self.height)
		self.drawer.ctx.move_to(.5*self.width, .5*self.height - .25*size)
		self.drawer.ctx.line_to(.5*self.width   .25*size, .5*self.height - .5*size)
		self.drawer.ctx.line_to(.5*self.width   .5*size, .5*self.height - .25*size)
		self.drawer.ctx.line_to(.5*self.width   .25*size, .5*self.height)
		self.drawer.ctx.line_to(.5*self.width   .5*size, .5*self.height   .25*size)
		self.drawer.ctx.line_to(.5*self.width   .25*size, .5*self.height   .5*size)
		self.drawer.ctx.line_to(.5*self.width, .5*self.height   .25*size)
		self.drawer.ctx.line_to(.5*self.width - .25*size, .5*self.height   .5*size)
		self.drawer.ctx.line_to(.5*self.width - .5*size, .5*self.height   .25*size)
		self.drawer.ctx.line_to(.5*self.width - .25*size, .5*self.height)
		self.drawer.ctx.line_to(.5*self.width - .5*size, .5*self.height - .25*size)
		self.drawer.ctx.line_to(.5*self.width - .25*size, .5*self.height - .5*size)
		self.drawer.ctx.fill()

		self.drawer.draw(offsetx=self.offsetx, offsety=self.offsety, width=self.width, height=self.height)


import random
from libqtile import widget
from libqtile.bar import Bar
from libqtile.config import Screen

screens = [
	Screen(
		left=Bar([
			#mywidgets.UnsavedChanges(mouse_callbacks={'Button1': lazy.spawn('bemenu-run')}),
			mywidgets.UnsavedChanges(),
			mywidgets.vertical_short(widget.Clock, update_interval=15, format="%a\n%d\n%b"),
			mywidgets.AnalogClock(padding=2),
			mywidgets.VGroupBox(bg_overrides={"comms": ('#3fff00', '#00bf3f')}),
			MultiNetGraph(length=16, col_dn='#007fff', col_up='#ff7f00'),
			MultiCpuGraph(length=16, colors=['#ff0000', '#ff7f00', '#ffff00', '#7fff00', '#00ff00', '#00ff7f', '#00ffff', '#007fff', '#0000ff', '#7f00ff', '#ff00ff', '#ff007f']),
			mywidgets.vertical_short(mywidgets.BetterBattery),
			# TODO: MPRIS all-players controller
			mywidgets.MprisAllPlayersController(notify=False),
			widget.StatusNotifier(), # TODO: why does Signal not show up?
			mywidgets.vertical_stacking(widget.CurrentLayout),
		], 24, background='#7f005f' if qtile.core.name == 'x11' else '#000000bf'),
		wallpaper='~/Pictures/Games/Pokémon/fanart/Lenov/Linna & Midori gigantamax (3840 × 2160).png' if qtile.core.name == 'x11' else '~/Pictures/Games/FAST Racing NEO/'   random.choice([
			"Avalanche Valley.jpg",
			"Caldera Post.jpg",
			"Cameron Crest.jpg",
			"Daitoshi Station.jpg",
			"Iceland.png",
			"Kenshu Jungle.jpg",
			"keyshot (NEO).jpg",
			"Kuiper Belt 1.png",
			"Kuiper Belt 2.png",
			"Mori Park.png",
			"Mueller Pacific.jpg",
			"New Zendling.png",
			"Pyramid Valley.png",
			"Scorpio Circuit.jpg",
			"Sendai Outpost.jpg",
			"Sunahara Plains.jpg",
			"Tepaneca Vale.jpg",
			"Zvil Raceway.jpg",
		]),
		wallpaper_mode='fill',
	),
	#Screen(
	#	right=Bar([
	#		mywidgets.UnsavedChanges(mouse_callbacks={'Button1': lazy.spawn('bemenu-run')}),
	#		mywidgets.VGroupBox(),
	#	], 24, background='#000000bf'),
	#	wallpaper='~/Pictures/fanart/Ravioli/Arch-tan 2.png',
	#	wallpaper_mode='fill',
	#),
]

@elParaguayo
Copy link
Member

There's definitely something odd with windows moving groups when configs are reloaded.

I'm also pretty sure we don't check the exclusivity when moving windows between groups. To be honest, I've never really looked at the exclusive behaviour much at all. Will take a closer look when I can.

@elParaguayo
Copy link
Member

I think the exclusive flag is only meant to stop windows being spawned in that group. It doesn't stop you moving them there because that's your choice to do so. So, I'm not sure that part is a bug, more of a feature.

Reloading groups, on the other hand, probably does need fixing.

@PhoenixClank
Copy link
Author

Upon looking at the documentation again, it's pretty clear about the exclusive flag being only for spawning and not for moving windows. I got that wrong, sorry.

Another thing that just came to mind: as you can see, my exclusive group permits all floating windows. However, the exclusivity check is done before the floating rules are applied, so if I spawn e.g. an instance of zenity, it still gets moved to another group.
Shall I make a new issue for that?

@elParaguayo
Copy link
Member

Upon looking at the documentation again, it's pretty clear about the exclusive flag being only for spawning and not for moving windows. I got that wrong, sorry.

No apology needed. If it's not what you expected then it's worth asking.

Another thing that just came to mind: as you can see, my exclusive group permits all floating windows. However, the exclusivity check is done before the floating rules are applied, so if I spawn e.g. an instance of zenity, it still gets moved to another group. Shall I make a new issue for that?

Just change the subject of this one and we can keep going here.

@PhoenixClank PhoenixClank changed the title windows can be moved into exclusive groups where they don't belong exclusive groups: reloading and floating weirdness Jan 27, 2023
@PhoenixClank
Copy link
Author

I thought this was for the reloading behavior, but alright, I'll give it a more general title.

@elParaguayo elParaguayo self-assigned this Apr 26, 2023
@elParaguayo
Copy link
Member

Just assigning this one to me. However, it will need to wait until after the next release.

@github-actions
Copy link

This issue is stale because it has been open 90 days with no activity. Remove the status: stale label or comment, or this will be closed in 30 days.

@PhoenixClank
Copy link
Author

No, dear bot, this still exists. (But it's not, like, super important or anything)

Copy link

github-actions bot commented Jan 9, 2024

This issue is stale because it has been open 90 days with no activity. Remove the status: stale label or comment, or this will be closed in 30 days.

@PhoenixClank
Copy link
Author

bumping this again, I guess

Copy link

github-actions bot commented Apr 9, 2024

This issue is stale because it has been open 90 days with no activity. Remove the status: stale label or comment, or this will be closed in 30 days.

@PhoenixClank
Copy link
Author

The latest release changed some things about how non-permanent groups work, but the behavior reported in this issue still exists.

Copy link

This issue is stale because it has been open 90 days with no activity. Remove the status: stale label or comment, or this will be closed in 30 days.

@PhoenixClank
Copy link
Author

still happening

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants