The i3 FAQ has migrated to https://github.com/i3/i3/discussions. All content here is read-only.
Ask Your Question
7

Run or focus in i3

asked 2013-09-02 18:59:29 +0000

phairland gravatar image

I have a keybinding in my i3 config (bindsym Control+3 [class="Firefox"] focus) that focus on firefox with Ctrl+3. However I would like that if firefox is not open it will run it.

In awesome WM there was a function in lua called "Run or raise" which achieve this goal.

How can I do this in i3?

edit retag flag offensive close merge delete

4 answers

Sort by ยป oldest newest most voted
6

answered 2013-09-03 08:54:25 +0000

updated 2015-03-06 08:27:10 +0000

The following bash-script should do the job:

#!/bin/sh
count=`ps aux | grep -c firefox`
if [ $count -eq 1 ]; then
    firefox
else
    i3-msg "[class=Firefox] focus"
fi

Store it ie. to ~/runorraise.sh, make it executable and bind this via bindsym Control+3 exec ~/runorraise.sh

Edit: The fourth line of the script was changed according to zwl's comment.

edit flag offensive delete link more

Comments

Thank you.

phairland gravatar imagephairland ( 2013-09-03 14:21:19 +0000 )edit

Why use i3-msg "exec firefox" instead of just firefox?

zwl gravatar imagezwl ( 2015-03-05 17:51:16 +0000 )edit

That's a good question. I think it has to do with "missing the forest for the trees" ;-) So no reason at all, I corrected it.

mschaefer gravatar imagemschaefer ( 2015-03-06 08:26:02 +0000 )edit
3

answered 2013-10-28 19:03:06 +0000

GermainZ gravatar image

updated 2013-10-28 19:11:45 +0000

I wanted something that's similar to awesome's RoR. That is, matching by class and cycling through matching windows. gustavnikolaj's solution matches by window title, which I didn't want, so I wrote this (the walk_tree function is from his script):

import json
import subprocess
import sys

def get_output(cmd):
    process = subprocess.Popen(cmd, stdout=subprocess.PIPE)
    out = process.communicate()[0].decode()
    process.stdout.close()
    return out

def get_tree():
    cmd = ["i3-msg", "-t", "get_tree"]
    return json.loads(get_output(cmd))

def get_matching_class():
    cmd = ["xdotool", "search", "--class", sys.argv[1]]
    return get_output(cmd).split('\n')

windows = []
def walk_tree(tree):
    if tree['window']:
        windows.append({'window': str(tree['window']),
                        'focused': tree['focused']})
    if len(tree['nodes']) > 0:
        for node in tree['nodes']:
            walk_tree(node)

def get_matches():
    matches = []
    tree = get_tree()
    check = get_matching_class()
    walk_tree(tree)
    for window in windows:
        for winid in check:
            if window['window'] == winid:
                matches.append(window)
    return matches

def main():
    matches = get_matches()
    # Sort the list by window IDs
    matches = [(match['window'], match) for match in matches]
    matches.sort()
    matches = [match for (key, match) in matches]
    # Iterate over the matches to find the first focused one, then focus the
    # next one.
    for ind, match in enumerate(matches):
        if match['focused'] == True:
            subprocess.call(["i3-msg", "[id=%s] focus" % matches[(ind+1)%len(matches)]['window']])
            return
    # No focused match was found, so focus the first one
    if len(matches) > 0:
            subprocess.call(["i3-msg", "[id=%s] focus" % matches[0]['window']])
            return
    # No matches found, launch program
    subprocess.call(["i3-msg", "exec", "--no-startup-id", sys.argv[2]])

if __name__ == '__main__':
    main()

Save it in e.g. ~.i3/runorfocus.py. To use it, just call the script (first argument is the class to match, the second argument is the program to launch if no matching window is found):

bindsym $mod+w exec python ~/.i3/runorfocus.py "Firefox" firefox

Note that you need xdotool to use the script.

edit flag offensive delete link more

Comments

Great! I didnt know about xdotool - that's a nifty little tool. I wanted to build my script so it could match both name and class - but I weren't able to find a good way to do it. If you don't mind I'd like to take a stab at combining the two scripts we have :-) Sorry for the late reply btw...

gustavnikolaj gravatar imagegustavnikolaj ( 2014-02-13 07:40:34 +0000 )edit

Just wanted to add that I've updated my answer in this thread with a new node.js based version. It has the functionality of our scripts combined.

gustavnikolaj gravatar imagegustavnikolaj ( 2014-03-24 22:18:53 +0000 )edit

I'd suggest changing `process.communicate()[0].decode()` to `process.communicate()[0].decode('utf8')` and `"exec", "--no-startup-id"` to `"exec --no-startup-id"`. (I don't have enough reputation to make the edits myself.)

Pkkm gravatar imagePkkm ( 2014-05-13 14:07:38 +0000 )edit
3

answered 2013-09-09 21:35:33 +0000

gustavnikolaj gravatar image

updated 2014-03-24 22:17:08 +0000

I just completed a long overdue item on my todo-list. A combination of the scripts that @GermainZ and I made. It can match by either name and class - both regex enabled. It's written as in node.js.

See this repository:

https://github.com/gustavnikolaj/i3-r...

You will need to have node installed and run npm install in the directory you've cloned it into. After that, you can either reference it by the full path to bin/i3-run-or-raise or you could add it somewhere on your path. (Remember that you'll have to manually set that path available to the X server, by adding it to .xsession/.xinitrc or where it would be appropriate on your system).

Rewriting the examples that I made in my first answer in this thread:

bindsym F1 exec ~/path/to/i3-run-or-raise -c "Google-chrome" google-chrome
bindsym F2 exec ~/path/to/i3-run-or-raise -n "Sublime Text 2$" sublime-text

Below is my original answer for reference.


I wrote a python script to solve this problem.

I use it in my configuration like this:

bindsym F1 exec python ~/.i3/scripts/next_window.py "Chrome$" google-chrome
bindsym F2 exec python ~/.i3/scripts/next_window.py "Sublime Text 2$" sublime-text

The first parameter is a regex to match the window title. The second parameter is the application to launch if no active window is found.

import json
import re
import subprocess
import sys


I3MSG = '/usr/bin/i3-msg'


windows = []


def parse_args():
    if len(sys.argv) == 3:
        return (sys.argv[1], sys.argv[2])
    else:
        sys.exit('Must provide 2 arguments.')


def get_tree():
    process = subprocess.Popen([I3MSG, "-t", "get_tree"], stdout=subprocess.PIPE)
    tree = str(process.communicate()[0])
    process.stdout.close()
    return json.loads(tree)


def walk_tree(tree):
    if tree['window']:
        windows.append({
            'name': tree['name'],
            'id': str(tree['id']),
            'focused': tree['focused']
        })
    if len(tree['nodes']) > 0:
        for node in tree['nodes']:
            walk_tree(node)


def main():
    pattern, command = parse_args()

    tree = get_tree()
    walk_tree(tree)

    focused = filter(lambda x: x['focused'], windows)
    if len(focused) == 1:
        focused = focused[0]
    else:
        focused = False

    filteredWindows = filter(lambda x: re.search(pattern, x['name']), windows)

    if len(filteredWindows) == 0:
        subprocess.call([I3MSG, 'exec', '--no-startup-id', command])
    else:
        nextWindow = False

        try:
            if not focused:
                raise ValueError

            nextIndex = filteredWindows.index(focused) + 1

            if nextIndex == len(filteredWindows):
                raise ValueError
            else:
                nextWindow = filteredWindows[nextIndex]

        except ValueError:
            nextWindow = filteredWindows[0]

        con_id = nextWindow['id']

        subprocess.call([I3MSG, '[con_id=' + con_id + ']', 'focus'])


if __name__ == '__main__':
    main()

You can see the code at my github dotfiles repository: https://github.com/gustavnikolaj/dotf...

edit flag offensive delete link more

Comments

Thanks for the script

phairland gravatar imagephairland ( 2013-09-10 15:39:10 +0000 )edit

Nice to see this done in python.

KJ44 gravatar imageKJ44 ( 2013-09-10 17:49:37 +0000 )edit

The `exec` call should be `subprocess.call([I3MSG, 'exec --no-startup-id', command])`, otherwise i3-msg complains about the unknown `--no-startup-id` argument.

blueyed gravatar imageblueyed ( 2014-02-25 11:52:36 +0000 )edit

It would be nice if this could be used in a more general way, e.g. by searching for the class and instance criteria (http://i3wm.org/docs/userguide.html#exec).

blueyed gravatar imageblueyed ( 2014-02-25 11:56:22 +0000 )edit

@blueyed I'm actually considering expanding this to do exactly that. I will of course update this when I get around to it. Thanks for the hint with the parameters to i3-msg :-)

gustavnikolaj gravatar imagegustavnikolaj ( 2014-03-11 12:46:23 +0000 )edit

Check out the new library: https://github.com/acrisci/i3ipc-python which is designed to help you do exactly this sort of thing.

TonyC gravatar imageTonyC ( 2014-03-24 23:43:51 +0000 )edit

This is a great thread. My bash script version now takes an argument to change to a named workspace (or not) when starting a program. In my experience matching windows with assign+criteria at program startup is problematic, I had to give up on simplicity and define the workspace explicitly.

KJ44 gravatar imageKJ44 ( 2014-03-25 09:38:10 +0000 )edit
2

answered 2013-09-04 18:00:38 +0000

KJ44 gravatar image

updated 2013-09-04 18:01:09 +0000

I have a script that I have bound to control-tab. An i3 input dialog (very minimal) appears. I type a short alias for the program I'm interested in. If it is running, focus is changed to make its window visible on its workspace, otherwise it is started.

How the script is invoked

# Launch a program using a string alias.                                                                                                                                                                          
bindsym    Control+Tab exec i3-input -F 'exec i3-wrapper "%s"'

The script:

#!/bin/bash
#
# i3-wrapper associates strings supplied by i3-input with programs.
#
# It is inspired by the configurable keyboard shortcuts for bookmarks
# in web browsers such as Firefox and Google Chrome.
#
# Type a string and the corresponding program is brought into focus;
# if it is already running focus moves to its associate workspace,
# otherwise it is launched in the current workspace.
#
# Take care to match long strings before short strings beginning with
# identical substrings.
#
# The easiest match to use is 'instance' because the instance sring
# usually matches the program name. Using 'title' is complicated by
# the need to enclose the matched substring in quotes but it can be a
# good choice in some cases.
#
# Contains at least one bashism: (link deleted so I can post)
#

focus ()
{
    case $1 in
        (instance)
        W=$(xdotool search --classname "$2" | head -1)
        ;;
        (class)
        W=$(xdotool search --class "$2" | head -1)
        ;;
        (title)
        W=$(xdotool search --name "$2" | head -1)
        ;;
        (*)
        W=''
        ;;
    esac
    if [ -z "$W" ]; then
        # Launch the program. 
        eval ${@:3} &  # bash
    else
        # Focus the window.
        # Used 'sed' to escape any whitepace to suit 'i3-msg'.
        i3-msg "[$1=$(echo $2 | sed s/\\x20/\\\\x20/g)] focus"
    fi
}

start ()
{
    case "$@" in
        (gc)
        focus instance google-chrome google-chrome
        ;;
        (sy)
        focus instance synaptic gksu synaptic
        ;;
        (th)
        focus instance thunar thunar
        ;;
        (dc|py)
        focus title Calculator python-interactive 
        ;;
        (wr)
        focus title 'LibreOffice Writer' libreoffice --writer
        ;;
        (pp)
        focus title 'LibreOffice Impress' libreoffice --impress
        ;;
        (ss)
        focus title 'LibreOffice Calc' libreoffice --calc
        ;;
        (pdf)
        focus instance okular okular
        ;;
        (am)
        focus instance clementine clementine
        ;;
        (vc|vlc)
        focus instance vlc vlc
        ;;
        (*)
        ;;
    esac
}

start "$@"

#
# Done.
#
edit flag offensive delete link more

Comments

I like your approach. Good for using xdotool. I used to use wmctrl but doesn't seem to work in i3.

phairland gravatar imagephairland ( 2013-09-05 13:19:00 +0000 )edit

Nice idea, I'm thinking about using this script as an addition to dmenu

mschaefer gravatar imagemschaefer ( 2013-09-06 08:56:43 +0000 )edit

The instance string for the well known web browser has changed from 'google-chrome' to 'Google-chrome' recently.

KJ44 gravatar imageKJ44 ( 2014-06-10 16:17:47 +0000 )edit

Question Tools

3 followers

Stats

Asked: 2013-09-02 18:59:29 +0000

Seen: 3,962 times

Last updated: Mar 06