sxhkd icon indicating copy to clipboard operation
sxhkd copied to clipboard

Keys for a specific application

Open D1mon opened this issue 5 years ago • 15 comments

Now the sxhkd daemon is working globally, I would like to be able to send commands for a specific application (when the application is active (in focus)). If the program is not active then ignore pressing.

It will look something like this:

XF86Audio{Prev,Next} (<app_name>)
    <command> {vol-,vol+} //arrow down,up

Or:

XF86Audio{Prev,Next} for <app_name>
    <command> {vol-,vol+}
XF86Audio{Prev,Next} for <window_title>
    <command> {vol-,vol+}

Adjusting the volume is not for the entire system, but for a specific application. The example above is probably not suitable, but the most important is the sense of how this should work.

~/script.sh for <app_name>
    ...

D1mon avatar Aug 15 '18 08:08 D1mon

I've written a script which does just this, see https://gist.github.com/gandalf3/970179ccf929e9e1224ac5faa3d58c3c

gandalf3 avatar Aug 22 '18 08:08 gandalf3

Thank you. But I wrote about loudness as an example. This is one of all possible functions, but not the only one.

D1mon avatar Aug 22 '18 14:08 D1mon

Oh I see, I misread. Is there any reason you couldn't perform such filtering in a script?

gandalf3 avatar Aug 22 '18 23:08 gandalf3

I do not understand what you were talking about. If you can filter the program in a script, then I saw similar solutions. But it seems to me better to implement it in the program than to perform thousands of checks on each command in the script. It is necessary each time to write a separate script with a heap of checks. And in the examples written above, this is done only by adding a few words.

D1mon avatar Aug 23 '18 06:08 D1mon

I mean, put something like

name=$(xwininfo -id $(xdo id) | grep '^xwininfo' | cut -d ' ' -f5-)
[[ $name != "<window_title>" ]] && exit 1

at the top of the script.

You could even put filtering logic in a re-usable script which calls whatever you really want only if the filters match. For example, something which might look like:

XF86Audio{Prev,Next}
    run_if_window_matches --name "<window_title>" --class "<window_class>" <command>

gandalf3 avatar Aug 25 '18 22:08 gandalf3

That was a nice idea, so I implemented it. run_if_window_matches

#!/usr/bin/env bash
#
# run_if_window_matches [--not] [--name <window_title> | --class <window_class>]* [--] <command>
#
# At present the conditions have && between them, whether they are negated or not.

#winid="$(xdo id)"
winid="$(xdotool getwindowfocus)"

#wininfo="$(xwininfo -id "$winid" | grep "^xwininfo: ")"
#echo "[run_if_window_matches] wininfo: $wininfo"
#wintitle="$(echo "$wininfo" | sed 's+^[^"]*"++ ; s+"$++')"

getprop() {
  xprop -id "$winid" "$1" | cut -d '"' -f 2
}

#winclass="$(getprop WM_CLASS)"

negate_match=''

check_if_prop_matches() {
  if getprop "$1" | egrep "^$2$" >/dev/null
  then [ -n "$negate_match" ] && exit 1   # Matched
  else [ -z "$negate_match" ] && exit 1   # Did not match
  fi
}

while true
do
  case "$1" in
    --not)
      negate_match=1
      shift
      ;;
    --class)
      check_if_prop_matches WM_CLASS "$2"
      shift; shift
      ;;
    --name)
      check_if_prop_matches WM_NAME "$2"
      shift; shift
      ;;
    --)
      shift
      break
      ;;
    *)
      break
      ;;
  esac
done

# Conditions passed; run command!
"$@"
  • You can send the same stroke to multiple applications, using OR in the regexp. For example: --name '(app1|app2|app3)'

  • You can send different stroke to different applications. To run multiple commands in one binding, you can do this:

    alt + shift + braceleft
      run_if_window_matches ... ; \
      run_if_window_matches ...
    

Providing a fallback stroke if none of the windows match:

  • There is no simple way to send a fallback stroke (we could perhaps add --else). For now you must use multiple commands as above, and a --not match.

Disadvantages:

  • If none of the applications match, the original keypress does not get passed to the window; sxhkd swallows it. (And we cannot try to emit it ourselves: that would just trigger sxhkd again!)
  • It can be a bit slow. (We could perhaps get the WM_* data in one call, and use bash string comparison instead of egrep.)

joeytwiddle avatar Jan 12 '19 06:01 joeytwiddle

That was a nice idea, so I implemented it. [run_if_window_matches]

Maybe I am having a brain fart of some kind, but I feel like it would be really nice if you mentioned how one goes about implementing your function, because as of right now I am at a little bit at a loss on how you implemented this.

UPDATE Wow.. there had to be have been a better way of implementing that in your other script rather than to create a symlink to this script so it appears to be in /usr/bin/. That's the only way I can figure this script was designed, because it does not work by simply sourcing it via zshrc, bashrc or creating aliases (sourcing within the alias or not).

rbreaves avatar Jan 31 '19 02:01 rbreaves

@rbreaves It's not a function, it's a script. So you need to save it somewhere on your $PATH. How you do that is up to you.

(You could add $HOME/bin to your PATH, or you could sudo symlink the script into /usr/bin/)

You will also need to make it executable, with: chmod a+x run_if_window_matches

joeytwiddle avatar Feb 02 '19 14:02 joeytwiddle

Well, it's a script being called like a function. I'm sure I had set to execute, but via chmod +x, at any rate I did get it to work eventually.

Also I've found a better alternative that's a lot simpler that accomplishes the same thing more or less. As far as swapping my keys out the way I wanted. I am using 2 sets of Xmodmap files that simply swap out based on whether I have my terminal application, konsole, open or note. The Xmodmap though is specific to my usb Apple keyboard, so if it is a Windows keyboard then a couple of the keycodes will have to change before it works.

https://github.com/rbreaves/xosx/blob/master/keyswap.service https://github.com/rbreaves/xosx/blob/master/keyswap.py https://github.com/rbreaves/xosx/tree/master/keyboard/apple

rbreaves avatar Feb 04 '19 02:02 rbreaves

@joeytwiddle Coming back round looking at this again.. and I think I understand your approach better now, but I'd still prefer solving it on the xkb level with setxkbmap than to use xte or xautomation. I have gone as far as to rewrite kinto.sh from a bash with xprop script to a C program based on some code I had found. Users can also modify simple json files to add additional app support.

But I am bothered by only having word-wise from macOS partially implemented at this point.. I need to update the symbols and types files to remap the Alt+arrow keys and I am just not sure how to do it at the moment. I've given a few tries but they have failed.

https://github.com/rbreaves/kinto/blob/master/.xkb/symbols/mac_gui https://github.com/rbreaves/kinto/blob/master/.xkb/types/mac_gui

There is a 3rd file that'll be created in ~/.xkb/keymaps/kbd.mac.gui, it looks something like the following (depending on your keyboard type)

xkb_keymap {
	xkb_keycodes  { include "evdev+aliases(qwerty)"	};
	xkb_types     { include "complete+mac_gui(addmac_levels)"	};
	xkb_compat    { include "complete"	};
	xkb_symbols   { include "pc+us+us:2+inet(evdev)+ctrl(swap_lwin_lctl)+ctrl(swap_rwin_rctl)+mac_gui(mac_levelssym)"	};
	xkb_geometry  { include "pc(pc105)"	};
};

I have tried modifying them so that I have a level modifier for alt in the same manner I have one for control, but it just seems to fail to work.

rbreaves avatar Feb 12 '20 21:02 rbreaves

@joeytwiddle Looks like I solved Alt+Left/Right => Ctrl+Left/Right while also keeping the actual Ctrl+Left/Right to use the HOME and END keys the way I want for a more complete macOS word-wise solution. And all of this is happening system wide for GUI apps, if exceptions for some apps need to be created later that can easily be done via a simple json file in kinto.

At any rate I like the Kinto solution better since it is not really intercepting keys with a 3rd party program so much as it is just using the xkb that is already built into all distros plus just listening for what the current focused window is via a C program and Xlib/x11 so it can trigger/change the xkb layout. As far as I can tell it is the lightest and most direct solution I can imagine writing.

https://github.com/rbreaves/kinto/commit/5d135afe5e3b792b80167b9cdcec269025c66d91

rbreaves avatar Feb 13 '20 02:02 rbreaves

This PR has some progress?

cherryramatisdev avatar Oct 18 '21 13:10 cherryramatisdev

Would love to see this too. The biggest showstopper with the script solution is, as @joeytwiddle mentioned:

If none of the applications match, the original keypress does not get passed to the window; sxhkd swallows it. (And we cannot try to emit it ourselves: that would just trigger sxhkd again!)

Window-specific keybinds is one of the things I miss from AHK, and it would be nice to see it implemented here too!

musjj avatar Mar 24 '23 18:03 musjj

It's possible to do this with lucky. Perhaps it's bad form to link my own program as a solution to the problem, but since sxhkd is barely maintained and doesn't seem to have interest in adding new features, it feels appropriate.

BanchouBoo avatar Mar 24 '23 19:03 BanchouBoo

@musjj

Would love to see this too. The biggest showstopper with the script solution is, as @joeytwiddle mentioned:

If none of the applications match, the original keypress does not get passed to the window; sxhkd swallows it. (And we cannot try to emit it ourselves: that would just trigger sxhkd again!)

Not really a showstopper. I adjusted @joeytwiddle's script a little, because it wasn't flexible enough for me. Now we'll just use it's exit code and chain anything we like.

#!/usr/bin/env bash
# program-has-focus [--name <window_title regex>] [--class <window_class regex>]

while true
do
  case "$1" in
    --class)
      CLASS="$2";
      shift; shift;
      ;;
    --name)
      NAME="$2";
      shift; shift;
      ;;
    --)
      shift;
      break;
      ;;
    *)
      break;
      ;;
  esac
done

winid="$(xdotool getwindowfocus)";

window_property_matches () {
    if ! xprop -id "$winid" ${1} | cut -d '"' -f 2 | grep -Ei "^${2}$" > /dev/null;
    then
        exit 1;
    fi
}

if [ -n "$winid" ]; then
    if [ -n "$CLASS" ]; then
        window_property_matches WM_CLASS "${CLASS}";
    fi
    if [ -n "$NAME" ]; then
        window_property_matches WM_NAME "${NAME}";
    fi
fi

exit 0;

In sxhkdrc use it like this

ctrl + m
    (program-has-focus --class code          && echo do something in vs code) || \
    (program-has-focus --class pcmanfm       && echo do something in pcmanfm) || \
    (program-has-focus --class arandr        && echo do something in arandr) || \
    (program-has-focus --class brave-browser && echo do something in brave) || \
    echo send original keystrokes && pkill sxhkd && xdotool key ctrl+m && sxhkd

Only downside - if you send the original keystrokes - you have to kill and restart sxhkd, otherwise you'll stuck in a loop... Maybe someone has a better solution for this

dubitabam avatar Nov 03 '23 21:11 dubitabam