惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

N
News and Events Feed by Topic
V
V2EX
博客园 - 【当耐特】
Vercel News
Vercel News
雷峰网
雷峰网
爱范儿
爱范儿
WordPress大学
WordPress大学
云风的 BLOG
云风的 BLOG
S
Securelist
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Microsoft Azure Blog
Microsoft Azure Blog
F
Full Disclosure
有赞技术团队
有赞技术团队
Hugging Face - Blog
Hugging Face - Blog
NISL@THU
NISL@THU
www.infosecurity-magazine.com
www.infosecurity-magazine.com
Attack and Defense Labs
Attack and Defense Labs
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Hacker News - Newest:
Hacker News - Newest: "LLM"
Microsoft Security Blog
Microsoft Security Blog
腾讯CDC
P
Proofpoint News Feed
B
Blog
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
K
Kaspersky official blog
I
InfoQ
Google Online Security Blog
Google Online Security Blog
L
LINUX DO - 最新话题
Project Zero
Project Zero
Engineering at Meta
Engineering at Meta
V
Visual Studio Blog
AI
AI
Schneier on Security
Schneier on Security
B
Blog RSS Feed
T
Tor Project blog
H
Help Net Security
H
Hackread – Cybersecurity News, Data Breaches, AI and More
L
LINUX DO - 热门话题
阮一峰的网络日志
阮一峰的网络日志
S
Security @ Cisco Blogs
T
Threat Research - Cisco Blogs
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
C
Cyber Attacks, Cyber Crime and Cyber Security
G
Google Developers Blog
Google DeepMind News
Google DeepMind News
V2EX - 技术
V2EX - 技术
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
A
Arctic Wolf
Webroot Blog
Webroot Blog
Recent Commits to openclaw:main
Recent Commits to openclaw:main

Stonecharioteer on Tech

I Traced My Traffic Through a Home Tailscale Exit Node What Was I Reading Last? In Three Not-So-Easy Pieces Dogfooding Is Hard Code blocks in your books, finally GoForGo v0.9.0 Merrilin - We built an app to read books I use a Macbook now Data Structures & Algorithms - Preparing for Interviews Using a local DNS namespace for local service discovery Direction KOllector - Publishing KOReader Highlights gbt: branches touched in the last 24 hours A Soiree into Symbols in Ruby Some Smalltalk about Ruby Loops Ruby Blocks Returning from Ruby Blocks, Procs and Lambdas My Linux Laptop Finally Works: How Claude Helped Me Fix Years of Annoyances TIL: Watchexec - Modern File Watching for Development Workflows A Less Busy Mind GoForGo - Learn Go through live examples Migrating My Old Blog to Hugo with Claude The Qtile Window Manager: A Python-Powered Tiling Experience Read the RFCs that Built the Internet Py-x-Protobuf - Or How I Learned to Stop Worrying and Love Protocol Buffers Python Reverse a List New Beginnings Leaving ChainSafe Systems Screen Lock for Cinnamon Desktop using Zenity and Terminal Commands Crews Not Teams A System for Getting Better at LeetCode So Far So Rust Retrying HTTP Requests with Rust A Primer on Control Charts Learning Rust Explicit is Better than Implicit: Rust for Pythonistas Using Custom Delimiters in Jinja Templates TIL: Creating Fixed Length Iterables in Python Documentation Without Assumption Vagrant Python - A Reflection in 2022 Learning Golang No, A Virtual Machine Is Not Enough: Why Developers Need Native Linux Empathy in Tech For Those Who Came in Late A Weekend With PostgreSQL TIL: Gooey and Python Fire for Quick GUIs and CLIs TIL: 2ality - Dr. Axel Rauschmayer's JavaScript Blog TIL: MassDNS - High-Performance Bulk DNS Lookups TIL: Matomo Analytics, Google Tech Writing, Memory Programming, and NES TV Signals TIL: MontyDB - MongoDB Implemented in Python Returning to the Craft of Programming TIL: CPUFetch, OneFetch, and Learn CSS TIL: DNS Performance Testing and Pi-hole with Unbound TIL: Eli Bendersky's Blog, Awesome By Example, NoCoDB, and Martin Kleppmann TIL: CRDTs, Extreme HTTP Performance, and BYTEPATH Game TIL: AutoInvent, ASGI, Python Packaging, RAPIDS GPU Computing, and FlaskCon TIL: MangaDesk - Terminal Client for MangaDex TIL: McFly - Smart Shell History Search TIL: Siege Load Testing and Awesome FastAPI Resources TIL: Ventoy Bootable USB and Justniffer Network Analysis TIL: CLI Code Review, Git Split Diffs, and Internal Combustion Engine TIL: Benford's Law, Web Security Headers, Event Sourcing, and Mozilla Security Guidelines How to Write Documentation - The README.md File The Importance of Documentation TIL: NNgroup UX Research, SponsorBlock, and Labella Python Library TIL: The Little Book of Rust Macros and Rust Performance Book TIL: Git-Bug Distributed Issue Tracker and Omni Kubernetes Monitoring TIL: Zellij - Modern Terminal Multiplexer TIL: How Discord Handles 2.5 Million Concurrent Voice Users TIL: Volumio - The Audiophile Music Player TIL: Areopagitica - Milton's Defense of Free Speech TIL: Fast Node Manager, Zoxide Smart CD, Technical Writing, PyO3, and Qubes OS TIL: Slurm Workload Manager for HPC Clusters TIL: Data Visualization Guide and Oso Authorization Academy TIL: CORS Deep Dive, Piku Tiny PaaS, Rust Strings, and Deno Standard Library TIL: Raspberry Pi OS Development, Vim Beginner Guide, Password Management, and QueryBook TIL: uBlock Origin Performance Optimization on Firefox TIL: Breaking PostgreSQL at Scale and LeetCode Problem Patterns TIL: Awesome Tmux Resources for Terminal Multiplexing TIL: Grit - A Multitree-Based Personal Task Manager TIL: Lens 4.2 Kubernetes IDE, Shell Scripting Guide, and Dark HTTP Server Do The Job You Hate So You Won't Hate The Job You Love TIL: Innernet VPN Solution and NoteCalc Calculator App TIL: Argo CD for GitOps and Lens Kubernetes IDE TIL: Modern Rust CLI Tools - System Monitoring, HTTP Requests, and DNS TIL: tz - A Time Zone Helper Tool TIL: Distributed Systems Education, Fallacies, and Self-Hosted Internet Archiving TIL: Real-Time Voice Cloning Technology TIL: ChartMuseum for Helm, AMD's Corporate Journey, and Kubernetes Pod Scaling TIL: Docker and Kubernetes Tools - Whaler, Descheduler, and Dive TIL: Post-Mortem Collection, Terminal Plotting, and Technical Twitter TIL: Dark Mode Toggle Web Component by Google Chrome Labs TIL: Python eval(), exec(), and compile() Functions TIL: Camelot PDF Tables, PostgreSQL Row Level Security, Zerodha Varsity, and Write Yourself a Git TIL: fuser Command for Process and File Investigation TIL: i Hate Regex - The Ultimate Regex Cheat Sheet TIL: Dolt - Git for Data and Database Version Control TIL: x86 Assembly Programming and SafeEyes Break Reminder TIL: Comprehensive Distributed Systems Reading List TIL: Cosmopolitan C Library, Distributed Systems Book, High Performance Browser Networking, and Rust Roguelike Tutorial
The Qtile Window Manager: A Python-Powered Tiling Experience
2025-07-29 · via Stonecharioteer on Tech

Important

This article was originally written in Dec 2021, but I’ve updated it to showcase my new config.

I’ve been an avid user of XFCE for a very long time. I’m fond of its lightweight nature, and I feel productive in it. But when I first discovered tiling window managers, I was mind-blown. I’ve wanted to use one forever.

My first experience with one was a few years ago, before I understood how Linux window managers worked. I couldn’t yet wrap my head around the fact that you could install more than one window manager and choose what you wanted during login. I think I’ve grown since then. I faintly remember trying to install i3wm, the most famous tiling window manager at the time. I think I was taken aback by the black screen, and more so with the mouse pointer which just said X.

A year or so ago, I came across DistroTube’s Youtube Channel, where he talks about xmonad, the tiling window manager that’s written in Haskell. While I’ve been wanting to learn Haskell for a very long time, my career trajectory hasn’t afforded me the chance to learn it so far.

I’ve since moved jobs and completely shifted to Linux everywhere. I no longer want to use a non-linux machine ever again. I’m sure there’s a whole blog article about how much of a Linux person I’ve become in the past year or so, somewhere in me.

Last week, I came across dt’s video on Qtile, the tiling window manager written entirely in Python. Now that was truly enticing. I’m adept enough in Python to be able to manage complex configurations all on my own. And after skimming through the documentation, I spent a day modularizing the default qtile config since the default config gives me goosebumps, and not in a good way.

In this article, I’ll describe what I did, and how I went about it.

Installing Qtile

I decided to abstract away the entire configuration so that it doesn’t live in my dotfiles repository. I wanted to create a python library for myself so that it would have a bunch of utilities for my own consumption.

Additionally, I disagreed with the default way of installing Qtile. As a principle, I never sudo pip install anything. Instead, I asked my friend Karthikeyan Singaravel, who is a Python core developer, and he recommended using the deadsnakes PPA for Ubuntu to install any version of Python that I chose. I tried compiling python 3.10 myself, installing to /opt/qtile/ using configure --prefix /opt/qtile/ during the configuration stage of the source code. However, I admit that using deadsnakes is a far better idea since I could create a virtual environment based on python3.10 into /opt/qtile/ instead. I had to change the owner of the folder to my user account. Note that I could store the virtual environment in my home folder and just use that, but I wanted to isolate this outside of my home folder.

Installation Approach

The key principle here is isolation - keeping Qtile’s dependencies separate from the system Python and user Python environments. This prevents conflicts and makes updates easier.

So, I installed python3.10-full and python3.10-dev (the development header files are necessary for building some of the dependencies of qtile), and I created a virtual environment using the venv module in /opt/qtile. Then, I changed the owner of the folder to my regular user account.

Then, it was time to install qtile.

Since I use the fish shell, I had to source activate /opt/qtile/bin/activate.fish to activate the virtual environment. And then I followed up by installing qtile. I didn’t pick a version right away, I decided to go with the latest version.

Qtile doesn’t setup an entry for your xsessions, so you need to do that yourself.

I created /usr/share/xsessions/qtile.desktop and filled it with the following:

1
2
3
4
5
6
[Desktop Entry]
Name=Qtile
Comment=Qtile Session
Exec=/opt/qtile/bin/qtile start
Type=Application
Keywords=wm;tiling

Notice how I used the absolute path for qtile.

After this, I logged out of my previous window manager and switched to the new entry for Qtile.

On loading qtile for the first time, I was fairly surprised with the default config. It wasn’t as blank as i3wm and xmonad were. It had a panel, a helpful text field on the panel about how to start the launcher, and it was very easy to use. I was liking it already.

But I wanted to configure it so that I could mess with the design.

The first thing that bothered me was the lack of a wallpaper. I’d used nitrogen before, so I installed it and started it up, setting a wallpaper. I restarted qtile and then… nothing.

That was me being silly and forgetting that Explicit is better than Implicit. Like all tiling window managers, Qtile did none of the work for us. You have to ensure that the wallpaper manager loads when Qtile is done loading. That’s where the .xsessionrc file comes in.

Since nitrogen can restore a wallpaper with ease, all I needed to do was:

This went into the ~/.xsessionrc file.

Configuring Qtile

Qtile’s config file rests at ~/.config/qtile/config.py. On start, Qtile will read this file. Since this file is just Python code, that also means every single line in this file is executed.

When you look at the default config, you will notice:

  1. It’s about 130 lines long. Not too big.
  2. It’s just a bunch of variable declarations.

This meant that all you needed to configure Qtile was to ensure you set the values of a few global variables in the config file. And Qtile would take care of the rest.

This was useful. All I needed to do was set some variables.

The default config constructs all these variables as it sets them, which is something I don’t recommend. Python’s error handling will not point out the right place where the error is occurring, and while Python 3.11 seeks to improve this, it’s generally not a good practice to have a long variable declaration step in your code.

For example, where the config does this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
screens = [
    Screen(
        bottom=bar.Bar(
            [
                widget.CurrentLayout(),
                widget.GroupBox(),
                widget.Prompt(),
                widget.WindowName(),
                widget.Chord(
                    chords_colors={
                        'launch': ("#ff0000", "#ffffff"),
                    },
                    name_transform=lambda name: name.upper(),
                ),
                widget.TextBox("default config", name="default"),
                widget.TextBox("Press <M-r> to spawn", foreground="#d75f5f"),
                widget.Systray(),
                widget.Clock(format='%Y-%m-%d %a %I:%M %p'),
                widget.QuickExit(),
            ],
            24,
        ),
    ),
]

If you want to reuse these objects, it’s better to just construct them separately and then use them in a panel. The same goes for reusing panels.

My Current Configuration

After months of tweaking and refinement, here’s what my current Qtile setup looks like. The key principles I’ve followed are:

  1. Modularity: Break down complex structures into functions
  2. Adaptive behavior: Detect hardware and adjust accordingly
  3. Practical shortcuts: Keybindings that make sense for daily use
  4. Visual consistency: A cohesive color scheme and layout

Color Scheme and Assets

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
colors = {
    "burgandy": "#b84d57",
    "midnight": "#1e2030",
    "light_blue_grey": "#d6dae8",
    "light_blue": "#8fafc7",
    "dark_slate_blue": "#2e3448"
}
colors["sys_tray"] = colors["dark_slate_blue"]
colors["bar"] = colors["dark_slate_blue"]

images = {
    "python": os.path.expanduser("~/.config/qtile/assets/python-logo-only.svg"),
    "straw-hat": os.path.expanduser("~/.config/qtile/assets/strawhat.png"),
    "linux-mint": os.path.expanduser("~/.config/qtile/assets/Linux_Mint.svg"),
    "cpu": os.path.expanduser("~/.config/qtile/assets/cpu.png"),
    "gpu": os.path.expanduser("~/.config/qtile/assets/gpu.png"),
    "ram": os.path.expanduser("~/.config/qtile/assets/ram.png"),
}

I use a consistent color palette and have custom icons for different system components. The straw hat is a personal touch - a nod to One Piece!

Smart Mouse Movement Between Monitors

One of my favorite custom functions handles multi-monitor setups elegantly:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@lazy.function
def move_mouse_to_next_monitor(qtile: Qtile):
    """Moves the mouse position to the next screen by calculating the position of the centre of the screen."""
    screen_count = len(qtile.screens)
    current_screen = qtile.current_screen
    current_index = next(
        (i for i, s in enumerate(qtile.screens) if s == current_screen), 0
    )
    next_index = (current_index + 1) % screen_count
    next_screen = qtile.screens[next_index]
    x = next_screen.x + next_screen.width // 2
    y = next_screen.y + next_screen.height // 2
    qtile.core.warp_pointer(x, y)

This automatically moves the mouse cursor to the center of the next monitor when I press Super + ., making multi-monitor workflows much smoother.

Key Bindings

My keybindings follow a logical pattern:

  • Super + hjkl: Vim-style window navigation
  • Super + Shift + hjkl: Move windows around
  • Super + Control + hjkl: Resize windows
  • Super + r: Launch rofi application launcher
  • Super + Shift + p: Screenshot utility
  • Super + Shift + l: Lock screen
  • Super + Shift + e: Power menu
1
2
# Example key binding
Key([mod], "r", lazy.spawn("rofi -show combi -combi-modes 'window,ssh,drun'"), desc="App launcher"),

Hardware-Aware Widgets

One of the most powerful aspects of a Python-based window manager is the ability to create intelligent, hardware-aware components:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def has_battery():
    """Check if the system has a battery"""
    import glob
    return bool(glob.glob("/sys/class/power_supply/BAT*"))

def get_ip_address():
    """Get the current IP address from WiFi or Ethernet connection"""
    import subprocess
    import re

    try:
        result = subprocess.run(['ip', 'route', 'get', '8.8.8.8'],
                              capture_output=True, text=True, timeout=5)
        if result.returncode == 0:
            match = re.search(r'src\s+(\d+\.\d+\.\d+\.\d+)', result.stdout)
            if match:
                ip = match.group(1)
                dev_match = re.search(r'dev\s+(\w+)', result.stdout)
                interface = dev_match.group(1) if dev_match else "unknown"
                return f"IP: {ip} ({interface})"
        return "IP: No connection"
    except Exception:
        return "IP: Error"

These functions automatically detect hardware capabilities and adjust the interface accordingly. The battery widget only appears on laptops, and the IP address widget shows the current network status.

AMD GPU Integration

Since I run AMD hardware, I’ve integrated amdgpu_top for real-time GPU monitoring:

 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
def amdgpu_metadata():
    """Retrieves the amdgpu metadata"""
    output = subprocess.check_output(
        "amdgpu_top -J -d".split(), stderr=subprocess.DEVNULL
    )
    return json.loads(output)

def get_vram_usage():
    data = amdgpu_metadata()
    if not data:
        return "GPU: N/A"

    parts = []
    for ix, gpu in enumerate(data):
        name = gpu.get("DeviceName", "AMD Radeon Graphics")
        if name == "AMD Radeon Graphics":
            name = "On-Chip"
        else:
            name = name.replace("AMD Radeon", "").strip()

        vram = gpu.get("VRAM", {})
        total = vram.get("Total VRAM", {}).get("value")
        used = vram.get("Total VRAM Usage", {}).get("value")
        if total is not None and used is not None:
            parts.append(f"[{name}]: {used}/{total} MiB")
        else:
            parts.append("[GPU]: N/A")
    return "\n".join(parts)

This provides real-time VRAM usage information directly in the status bar.

Dynamic Screen Configuration

The screen configuration automatically adapts to the number of connected monitors:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
def count_monitors():
    """Returns the number of monitors"""
    try:
        output = subprocess.check_output(["xrandr", "--query"]).decode()
        monitors = [line for line in output.splitlines() if " connected" in line]
        return len(monitors)
    except Exception as e:
        print(f"Error: {e}")
        return 0

screens = [screen(main=True)]
for _ in range(count_monitors() - 1):
    screens.append(screen())

The main screen gets additional widgets like system tray and network information, while secondary screens get a simplified layout.

Startup Hooks

Qtile provides hooks for running scripts at startup:

1
2
3
4
5
6
7
8
9
@hook.subscribe.startup_once
def startup_once():
    """Starts the first time qtile starts"""
    subprocess.call(os.path.expanduser("~/.config/qtile/autostart.sh"))

@hook.subscribe.startup
def startup_always():
    """Runs every time qtile is started/reloaded"""
    subprocess.call(os.path.expanduser("~/.config/qtile/reload.sh"))

This lets me separate one-time setup (like setting wallpapers) from things that should happen on every reload.

Current Setup in Action

My current setup includes:

  • Top bar: Shows Linux Mint logo, current layout, groups (workspaces), task list, and system tray
  • Bottom bar: CPU/GPU temperatures, VRAM usage, system resources, battery (if present), IP address, and clock
  • Custom separators: Visual dividers using the “⋮” character in my accent color
  • JetBrains Mono Nerd Font: For consistent icon rendering across all widgets

Font Choice

Using a Nerd Font is crucial for proper icon rendering in Qtile widgets. JetBrains Mono provides excellent readability while supporting all the necessary symbols.

Lessons Learned

After using Qtile daily for months, here are the key insights:

Python Configuration is Powerful

Having your window manager configuration in Python means you can:

  • Write complex logic for hardware detection
  • Create reusable functions and modules
  • Integrate with system tools seamlessly
  • Debug configuration issues using Python tools

Start Simple, Iterate

Don’t try to recreate someone else’s rice immediately. Start with the defaults and gradually customize:

  1. Basic keybindings first
  2. Add essential widgets
  3. Customize colors and fonts
  4. Add advanced features like custom functions

Hardware Awareness Matters

Modern systems vary significantly. Your configuration should adapt to:

  • Number of monitors
  • Battery presence
  • Available sensors
  • Network interfaces

Performance Considerations

Since widgets can run arbitrary Python code, be mindful of:

  • Update intervals for polling widgets
  • Error handling in custom functions
  • Resource usage of external commands

Future Plans

This configuration is continuously evolving. Some planned improvements:

  1. Custom Widgets:

    • One Piece chapter release notifications
    • Gmail filtering widget
    • tmux session manager
    • Kubernetes context indicator
  2. Better Multi-Monitor Support:

    • Per-monitor wallpaper management
    • Workspace binding to specific monitors
    • Dynamic layout switching based on monitor configuration
  3. Integration Improvements:

    • NordVPN status widget
    • NAS storage monitoring
    • Better notification management

Preview

Here’s a look at what my config looks like today.

Qtile Config

Conclusion

Qtile has transformed my Linux desktop experience. The ability to configure everything in Python, combined with the logical tiling approach, has made me significantly more productive. The learning curve is gentler than pure configuration-file-based window managers, and the extensibility is unmatched.

If you’re comfortable with Python and want a window manager that grows with your needs, Qtile is an excellent choice. The community is helpful, the documentation is comprehensive, and the possibilities are endless.

The configuration I’ve shared represents months of daily use and refinement. It’s not just about aesthetics (though it does look good!) - it’s about creating a workspace that adapts to your hardware, workflow, and preferences seamlessly.