Repository: qtile/qtile

Issue:
Logs filled wih "Key spec duplicated" errors, but config is only binding keys once (using simple_key_binder)
### Issue description

My QTile log is filled with "Key spec duplicated" errors, for all of the group-switching keys, multiple times.  I can't see anything in my config that would be trying to define the same key more than once.  I am using "simple_key_binder" from libqtile.dgroups to define those keybindings.  I thought I had read somewhere that this was not needed, and QTile makes a bunch of default bindings for group switching, but when I removed it, all of my mod+number keys stopped working, so, I put it back in.


### Version

0.23.0

### Backend

X11 (default)

### Config

```python
import os
import re
import socket
import subprocess
import psutil
import json
from libqtile import hook
from libqtile import qtile
from typing import List
from libqtile import bar, layout, widget
from libqtile.config import Click, Drag, Group, Key, Match, Screen, ScratchPad, DropDown, KeyChord
from libqtile.lazy import lazy
from libqtile.utils import guess_terminal
from libqtile.widget import Spacer, Backlight
from libqtile.widget.image import Image
from libqtile.dgroups import simple_key_binder
from pathlib import Path
from typing import Callable


# Get home path
home = str(Path.home())

# --------------------------------------------------------
# Define Status Bar
# --------------------------------------------------------
wm_bar = "qtile"

# --------------------------------------------------------
# Set default apps
# --------------------------------------------------------
terminal = "alacritty"

# --------------------------------------------------------
# Keybindings
# --------------------------------------------------------
mod = "mod4" # SUPER KEY

keys = [
    # Focus
    Key([mod], "Left", lazy.layout.left(), desc="Move focus to left"),
    Key([mod], "Right", lazy.layout.right(), desc="Move focus to right"),
    Key([mod], "Down", lazy.layout.down(), desc="Move focus down"),
    Key([mod], "Up", lazy.layout.up(), desc="Move focus up"),
    Key([mod], "space", lazy.layout.next(), desc="Move window focus to other window around"),

    # Move
    Key([mod, "shift"], "Left", lazy.layout.shuffle_left(), desc="Move window to the left"),
    Key([mod, "shift"], "Right", lazy.layout.shuffle_right(), desc="Move window to the right"),
    Key([mod, "shift"], "Down", lazy.layout.shuffle_down(), desc="Move window down"),
    Key([mod, "shift"], "Up", lazy.layout.shuffle_up(), desc="Move window up"),

    # Swap
    Key([mod, "shift"], "h", lazy.layout.swap_left()),
    Key([mod, "shift"], "l", lazy.layout.swap_right()),

    # Step through groups
    Key([mod, "control"], "Left", lazy.screen.prev_group(skip_managed=True), desc="Prev screen"),
    Key([mod, "control"], "Right", lazy.screen.next_group(skip_managed=True), desc="Next screen"),

    # Size
    Key([mod, "control"], "Down", lazy.layout.shrink(), desc="Grow window to the left"),
    Key([mod, "control"], "Up", lazy.layout.grow(), desc="Grow window to the right"),
    Key([mod], "n", lazy.layout.normalize(), desc="Reset all window sizes"),

    # Floating
    Key([mod], "t", lazy.window.toggle_floating(), desc='Toggle floating'),

    # Split
    Key([mod, "shift"], "Return", lazy.layout.toggle_split(), desc="Toggle between split and unsplit sides of stack"),

    # Toggle Layouts
    Key([mod], "Tab", lazy.next_layout(), desc="Toggle between layouts"),

    # Fullscreen
    Key([mod], "f", lazy.window.toggle_fullscreen()),

    #System
    Key([mod], "Escape", lazy.window.kill(), desc="Kill focused window"),
    Key([mod, "control"], "r", lazy.reload_config(), desc="Reload the config"),
    Key([mod, "control"], "Escape", lazy.spawn(home + "/.config/scripts/powermenu.sh"), desc="Open Powermenu"),

    # Audio level and video brightness
    Key([], "XF86AudioRaiseVolume",  lazy.widget['pulsevolume'].increase_vol(), desc="Raise volume"),
    Key([], "XF86AudioLowerVolume",  lazy.widget['pulsevolume'].decrease_vol(), desc="Lower volume"),
    Key([], "XF86AudioMute",         lazy.widget['pulsevolume'].mute(), desc="Mute/unmute"),
    Key([], "XF86AudioMicMute",      lazy.spawn(home + "/.config/scripts/micmute.sh"), desc="Toggle microphone mute"),
    Key([], "XF86MonBrightnessUp",   lazy.spawn(home + "/.config/scripts/backlight.sh +"), desc="Increase brightness"),
    Key([], "XF86MonBrightnessDown", lazy.spawn(home + "/.config/scripts/backlight.sh -"), desc="Decrease brightness"),

    # Apps
    Key([mod], "Return", lazy.spawn(terminal), desc="Launch terminal"),
    Key([mod, "control"], "Return", lazy.spawn(home + "/.config/scripts/applauncher.sh"), desc="Launch Rofi"),
    Key([mod], "b", lazy.spawn("/usr/bin/env MOZ_X11_EGL=1 /usr/bin/firefox"), desc="Launch Browser"),
    Key([mod, "shift"], "w", lazy.spawn(home + "/.config/scripts/updatewal.sh"), desc="Update Theme and Wallpaper"),
    Key([mod, "control"], "w", lazy.spawn(home + "/.config/scripts/wallpaper.sh"), desc="Select Theme and Wallpaper"),
]

# --------------------------------------------------------
# Groups
# --------------------------------------------------------

groups = [
    Group("1", layout='monadtall'),
    Group("2", layout='monadtall'),
    Group("3", layout='monadtall'),
    Group("4", layout='monadtall'),
    Group("5", layout='monadtall'),
    Group("6", layout='monadtall'),
]

dgroups_key_binder = simple_key_binder(mod)


# --------------------------------------------------------
# Scratchpads
# --------------------------------------------------------

groups.append(
    ScratchPad('scratchpad', [
      DropDown('htop', terminal + " -t htop -e htop", x=0.447, width=0.55, height=0.995, opacity=1),
      DropDown('khal', terminal + " -t ikhal -e ikhal", x=0.677, width=0.32, height=0.995, opacity=1),
]))


# --------------------------------------------------------
# Pywal Colors
# --------------------------------------------------------

colors = os.path.expanduser('~/.cache/wal/colors.json')
colordict = json.load(open(colors))
Color0=(colordict['colors']['color0'])
Color1=(colordict['colors']['color1'])
Color2=(colordict['colors']['color2'])
Color3=(colordict['colors']['color3'])
Color4=(colordict['colors']['color4'])
Color5=(colordict['colors']['color5'])
Color6=(colordict['colors']['color6'])
Color7=(colordict['colors']['color7'])
Color8=(colordict['colors']['color8'])
Color9=(colordict['colors']['color9'])
Color10=(colordict['colors']['color10'])
Color11=(colordict['colors']['color11'])
Color12=(colordict['colors']['color12'])
Color13=(colordict['colors']['color13'])
Color14=(colordict['colors']['color14'])
Color15=(colordict['colors']['color15'])
ColorFG=Color7
ColorBG=Color0
ColorPri=Color1

# --------------------------------------------------------
# Setup Layout Theme
# --------------------------------------------------------

layout_theme = {
    "border_width": 3,
    "margin": 8,
    "border_focus": Color3,
    "border_normal": "FFFFFF",
    "single_border_width": 3
}

# --------------------------------------------------------
# Layouts
# --------------------------------------------------------

layouts = [
    # layout.Columns(),
    layout.Max(**layout_theme),
    # Try more layouts by unleashing below layouts.
    # layout.Stack(num_stacks=2),
    # layout.Bsp(),
    # layout.Matrix(),
    layout.MonadTall(**layout_theme),
    layout.MonadWide(**layout_theme),
    layout.RatioTile(**layout_theme),
    # layout.Tile(),
    # layout.TreeTab(),
    # layout.VerticalTile(),
    # layout.Zoomy(),
    layout.Floating()
]

# --------------------------------------------------------
# Setup Widget Defaults
# --------------------------------------------------------

widget_defaults = dict(
    font="Fira Sans SemiBold",
    fontsize=14,
    padding=3
)
extension_defaults = widget_defaults.copy()

# --------------------------------------------------------
# Widgets
# --------------------------------------------------------

colorStr='<span foreground="{}">'.format(ColorPri)   # used for battery widget

widget_list = [
    widget.TextBox(
        text='',
        foreground=ColorPri,
        mouse_callbacks={"Button1": lazy.spawn(home + "/.config/scripts/applauncher.sh")},
        width=30
    ),
    widget.GroupBox(
        highlight_method='block',
        block_highlight_text_color='000000',
        foreground='ffffff',
        active=ColorFG,
        inactive=ColorPri,
        this_screen_border=Color2,
        this_current_screen_border=Color10,
        other_screen_border=Color6,
        other_current_screen_border=Color14,
        rounded=False
    ),
    widget.CurrentLayoutIcon(),
    widget.Spacer(
        length=10
    ),
    widget.TextBox(
        text='  win10',
        foreground=ColorPri,
        mouse_callbacks={"Button1": lazy.spawn("/usr/bin/virsh -c qemu:///system start win10 && virt-viewer -c qemu:///system win10", shell=True)},
        width=70
    ),
    widget.WindowName(
        max_chars=50,
        foreground=ColorFG
    ),
    widget.Systray(),
    widget.Wlan(
        format="{essid}",
        foreground=ColorFG,
        update_interval=5,
        width=140
    ),
    widget.Spacer(
        length=10
    ),

    widget.TextBox(
        text='',
        foreground=ColorPri
    ),
#    widget.PulseVolume(
#        fmt='{}',
#        foreground=ColorFG,
#        step=5,
#        width=45
#    ),

    widget.Battery(
        format=colorStr+'{char}</span> {percent:2.0%}',
        full_char='',
        unknown_char='',
        charge_char='',
        discharge_char='',
        empty_char='',
        foreground=ColorFG,
        low_foreground='ff00c0',
        low_percentage=0.25,
        update_interval=30,
        width=64
    ),

    widget.TextBox(
        text='',
        foreground=ColorPri
    ),
    widget.Memory(
        format="{MemUsed:4.1f} {mm}",
        foreground=ColorFG,
        measure_mem='G',
        width=48
    ),

    widget.TextBox(
        text='',
        foreground=ColorPri
    ),
    widget.CPU(
        format="{load_percent:2.0f}%",
        foreground=ColorFG,
        mouse_callbacks={'Button1': lazy.group['scratchpad'].dropdown_toggle('htop')},
        width=36
    ),

    widget.TextBox(
        text='',
        foreground=ColorPri
    ),
    widget.ThermalSensor(
        format='{temp:.0f} {unit}',
        tag_sensor='CPU',
        foreground=ColorFG,
        foreground_alert='ff00c0',
        threshold=50.0,
        width=50
    ),

    widget.Clock(
        format="%a %d %b  %H:%M",
        foreground=ColorFG,
        mouse_callbacks={'Button1': lazy.group['scratchpad'].dropdown_toggle('khal')},
        width=120
    ),
    widget.TextBox(
        text="",
        foreground=ColorPri,
        mouse_callbacks={'Button1': lazy.spawn(home + "/.config/scripts/powermenu.sh")}
    ),
]


# --------------------------------------------------------
# Screens
# --------------------------------------------------------

screens = [
    Screen(
        top=bar.Bar(
              widgets=widget_list,
              size=24,
              opacity=0.7,
              border_width=[3, 0, 3, 0],
              margin=[0,0,0,0],
              background=ColorBG
        ),
    ),
    Screen(
    ),
]

# --------------------------------------------------------
# Drag floating layouts
# --------------------------------------------------------

mouse = [
    Drag([mod], "Button1", lazy.window.set_position_floating(), start=lazy.window.get_position()),
    Drag([mod], "Button3", lazy.window.set_size_floating(), start=lazy.window.get_size()),
    Click([mod], "Button2", lazy.window.bring_to_front()),
]

# --------------------------------------------------------
# Define floating layouts
# --------------------------------------------------------

floating_layout = layout.Floating(
    border_width=3,
    border_focus=Color3,
    border_normal="FFFFFF",
    float_rules=[
        # Run the utility of `xprop` to see the wm class and name of an X client.
        *layout.Floating.default_float_rules,
        Match(wm_class="ssh-askpass"),  # ssh-askpass
        Match(title="pinentry"),  # GPG key password entry
    ]
)

# --------------------------------------------------------
# General Setup
# --------------------------------------------------------

dgroups_app_rules = []  # type: list
follow_mouse_focus = True
bring_front_click = False
cursor_warp = False
auto_fullscreen = True
focus_on_window_activation = "smart"
reconfigure_screens = True

# If things like steam games want to auto-minimize themselves when losing
# focus, should we respect this or not?
auto_minimize = True

# When using the Wayland backend, this can be used to configure input devices.
wl_input_rules = None

# XXX: Gasp! We're lying here. In fact, nobody really uses or cares about this
# string besides java UI toolkits; you can see several discussions on the
# mailing lists, GitHub issues, and other WM documentation that suggest setting
# this string if your java app doesn't work correctly. We may as well just lie
# and say that we're a working one by default.
#
# We choose LG3D to maximize irony: it is a 3D non-reparenting WM written in
# java that happens to be on java's whitelist.

# --------------------------------------------------------
# Windows Manager Name
# --------------------------------------------------------

wmname = "LG3D" # was QTILE

# --------------------------------------------------------
# Hooks
# --------------------------------------------------------

# HOOK startup
@hook.subscribe.startup_once
def autostart():
    home = os.path.expanduser('~/.config/qtile/autostart.sh')
    subprocess.Popen([home])
```


### Logs

```text
2024-01-19 11:59:09,466 WARNING libqtile manager.py:grab_key():L477 Key spec duplicated, over
riding previous: <Key (['mod4'], 1)>
2024-01-19 11:59:09,467 WARNING libqtile manager.py:grab_key():L477 Key spec duplicated, over
riding previous: <Key (['mod4', 'shift'], 1)>
2024-01-19 11:59:09,467 WARNING libqtile manager.py:grab_key():L477 Key spec duplicated, over
riding previous: <Key (['mod4', 'control'], 1)>
2024-01-19 11:59:09,467 WARNING libqtile manager.py:grab_key():L477 Key spec duplicated, over
riding previous: <Key (['mod4'], 2)>
2024-01-19 11:59:09,467 WARNING libqtile manager.py:grab_key():L477 Key spec duplicated, over
riding previous: <Key (['mod4', 'shift'], 2)>
2024-01-19 11:59:09,467 WARNING libqtile manager.py:grab_key():L477 Key spec duplicated, over
riding previous: <Key (['mod4', 'control'], 2)>
2024-01-19 11:59:09,467 WARNING libqtile manager.py:grab_key():L477 Key spec duplicated, over
riding previous: <Key (['mod4'], 3)>
2024-01-19 11:59:09,467 WARNING libqtile manager.py:grab_key():L477 Key spec duplicated, over
riding previous: <Key (['mod4', 'shift'], 3)>
2024-01-19 11:59:09,467 WARNING libqtile manager.py:grab_key():L477 Key spec duplicated, over
riding previous: <Key (['mod4', 'control'], 3)>
2024-01-19 11:59:09,467 WARNING libqtile manager.py:grab_key():L477 Key spec duplicated, over
riding previous: <Key (['mod4'], 4)>
2024-01-19 11:59:09,467 WARNING libqtile manager.py:grab_key():L477 Key spec duplicated, over
riding previous: <Key (['mod4', 'shift'], 4)>
2024-01-19 11:59:09,467 WARNING libqtile manager.py:grab_key():L477 Key spec duplicated, over
riding previous: <Key (['mod4', 'control'], 4)>
2024-01-19 11:59:09,467 WARNING libqtile manager.py:grab_key():L477 Key spec duplicated, over
riding previous: <Key (['mod4'], 5)>
2024-01-19 11:59:09,467 WARNING libqtile manager.py:grab_key():L477 Key spec duplicated, over
riding previous: <Key (['mod4', 'shift'], 5)>
2024-01-19 11:59:09,467 WARNING libqtile manager.py:grab_key():L477 Key spec duplicated, over
riding previous: <Key (['mod4', 'control'], 5)>
2024-01-19 11:59:09,467 WARNING libqtile manager.py:grab_key():L477 Key spec duplicated, over
riding previous: <Key (['mod4'], 6)>
2024-01-19 11:59:09,467 WARNING libqtile manager.py:grab_key():L477 Key spec duplicated, over
riding previous: <Key (['mod4', 'shift'], 6)>
2024-01-19 11:59:09,467 WARNING libqtile manager.py:grab_key():L477 Key spec duplicated, over
riding previous: <Key (['mod4', 'control'], 6)>
2024-01-19 11:59:09,467 WARNING libqtile manager.py:grab_key():L477 Key spec duplicated, over
riding previous: <Key (['mod4'], 7)>
2024-01-19 11:59:09,467 WARNING libqtile manager.py:grab_key():L477 Key spec duplicated, overriding previous: <Key (['mod4', 'shift'], 7)>
2024-01-19 11:59:09,467 WARNING libqtile manager.py:grab_key():L477 Key spec duplicated, overriding previous: <Key (['mod4', 'control'], 7)>
```


### Required

- [X] I have searched past issues to see if this bug has already been reported, and it hasn't been.
- [X] I understand that people give their precious time for free, and thus I've done my very best to make this problem as easy as possible to investigate.


Discussion:
I had this in my logs, but can't reproduce with the 0.24.0 tag; can you confirm that it happens with the recent release?
I just upgraded to 0.24.0 on Void Linux, rebooted, and the same messages are still there.  My bar is also messed up (one tray icon at the far left, on top of another widget, instead of in the tray icon area)---still need to look into that.
Can you post your config and I'll see if I can recreate the issue(s).
Hi @elParaguayo
It's still the same config that I posted above.  Thanks for having a look!

PS the two tray applications I am running (in my autostart.sh) are nm-applet and blueman-applet.  The blueman-applet appears in the same place it used to (far right of the tray area), but nm-applet somehow moved to the far left of the bar, and is painted directly on top of my menu launcher icon.  Oddly, I can still access both of them by moving the mouse a bit higher or a bit lower :)

Something also changed in the Battery widget.  I used to have battery icons but after upgrading to 0.24.0 it is now showing an asterisk.

Also, this morning I just noticed that a group sometimes spontaneously moves onto my second monitor (when it was just showing background) without me doing anything except typing this Github comment on the primary monitor.

No idea why any of this would have changed... it's very strange.
I can recreate the duplicate key issue. It seems to be triggered by this line:
```python
dgroups_key_binder = simple_key_binder(mod)
```
Commenting it out stops the errors. Will investigate more.

Haven't looked at the other two issues. Ideally, you could you separate them into separate issues here so I can deal with them individually and keep this thread just for the duplicate keys part.

EDIT: Ha - it was my commit that caused this: https://github.com/qtile/qtile/commit/9d65ae2e8f0a5283506f7db0d38ee14544fb5067  Need to find a fix that doesn't cause a regression of that other issue.

Resolving patch:
diff --git a/libqtile/core/manager.py b/libqtile/core/manager.py
--- a/libqtile/core/manager.py
+++ b/libqtile/core/manager.py
@@ -475,6 +475,9 @@ def grab_key(self, key: Key | KeyChord) -> None:
         """Grab the given key event"""
         syms = self.core.grab_key(key)
         if syms in self.keys_map:
+            if self.keys_map[syms] == key:
+                # We've already bound this key definition
+                return
             logger.warning("Key spec duplicated, overriding previous: %s", key)
         self.keys_map[syms] = key
 


Test patch: