streamdeck-ui icon indicating copy to clipboard operation
streamdeck-ui copied to clipboard

Is it possible to set button icon programmatically?

Open cmccandless opened this issue 3 years ago • 14 comments

My use case: I have a button that runs a script which mutes my mic. As part of that script, I would like to be able to change the icon on the mute button to indicate whether the mic is muted or not.

Is this possible?

cmccandless avatar Feb 09 '21 21:02 cmccandless

Been giving this some thought. Writing this down in case I forget :)

I think a simpler way to handle this is with "toggle" buttons. The basic idea is that the button would have two states. Each state has an icon and a command.

It starts on the first state and icon. If you press the button, it runs the first command and transitions to the second state and shows the second icon. When you press the button again, it executes the second command and transitions back to the first state and shows the first icon again.

This will work in cases where you have distinct commands for "on" and "off". In many instances this could be workable. In cases where you actually don't have two commands, things may get out of sync. I.e. if the command "toggles" the microphone, then you can't actually be sure if it's currently on or off. One solution may be that a "long press" of the button just changes the state. I.e. if you realize that it's showing the incorrect icon, a long press just switches to the other state.

Another enhancement could be that we observe the return value of the command. If it's 0 (typical for success), you transition. If it's non-zero, something went wrong, we don't transition. This way, you could in theory add some feedback that when you clicked mute, it actually muted before showing the next icon and vice versa. Unfortunately, not all commands return useful return values, but you could of course proxy via a script that returns the appropriate return value.

Lastly - perhaps a bit more complex - is that you use a separate command to query the current state, since the approach above won't always work on startup and your initial state could be incorrect. Lets call this the "query state" command. This one's sole job is to determine which state you're currently in and does not transition when called. During startup, we call the query state command and set your state to 1 or 2. Now you're always guaranteed to start off in the correct state. When you transition, you call the appropriate command followed by the query state command. A failed command would be detected and keep you in the current state.

dodgyrabbit avatar Apr 21 '21 02:04 dodgyrabbit

This is really close to what I was hoping to find. I was thinking about creating simple script that would poll the status of a service. This would allow me to use the stream deck as a server status monitoring tool. One button per server and clicking would just open a ssh terminal to that server. This would need the ability to poll the script periodically, but the end result would be nice.

hefax avatar Apr 26 '21 13:04 hefax

I see the risk of toggle buttons getting out out of sync whenever there's also a way to perform the action outside of streamdeck-ui, e.g. a multimedia key on the keyboard could also mute audio without streamdeck-ui noticing or a click in the notification area. Also, this would limit the button to two states.

The status command approach is a lot more flexible, the question here is, do you want to poll the status from streamdeck-ui or do you want to have the status pushed.

It would be interesting to have a button showing the number of currently connected WiFi clients, WAN up/down speeds, or integrating with a smart home so a button turns red when the front door is open and green when it's closed again.

Easiest would be having an API (maybe OSC?) and have others then handle the heavy lifting outside of streamdeck-ui.

capncrunch avatar May 01 '21 23:05 capncrunch

I actually implemented this locally, it was only a few lines of code. It was for the exact same action (mute/unmute my mic via xdotool sending XF86AudioMicMute). I needed a visual indicator on the streamdeck to know if the mic was currently muted or unmuted (since this method is essentially a toggle between muted/unmuted.

What I ended up doing, which I know perhaps is not the most pretty way, was to have the script determine which image should be pushed to the streamdeck. Since I always use this button to mute/unmute, state is almost always current (I despise having to open the plasma-pa KCM to do this :). The script simply checks the current status, figures out where it will be, and based on that simply outputs the path to the desired image. From the streamdeck-ui side, I changed the api.py to check if, when running a script, it outputs the path to an image and if it does, push that to the streamdeck:

$ diff streamdeck-ui/streamdeck_ui/api.py streamdeck_ui_img_callback/api.py 
7c7
< from subprocess import Popen  # nosec - Need to allow users to specify arbitrary commands
---
> from subprocess import Popen, PIPE  # nosec - Need to allow users to specify arbitrary commands
9a10
> from pathlib import Path
40c41,47
<                 Popen(shlex.split(command))
---
>                 p = Popen(shlex.split(command), stdout=PIPE)
>                 output = p.stdout.read()
>                 output = output.decode('utf-8').replace('\n', '')
>                 image_path = Path(output)
>                 if image_path.exists():
>                     set_button_icon(deck_id, page, key, str(image_path))

All this I did on the @dodgyrabbit 's fork. Like I said, not pretty, but since I control the script and the change is quite trivial (and if I break it it's my fault :), I ended up using it. Putting this out there in the case it gives someone any ideas on implementing this.

godlike64 avatar Jul 17 '21 15:07 godlike64

I actually implemented this locally, it was only a few lines of code. It was for the exact same action (mute/unmute my mic via xdotool sending XF86AudioMicMute). I needed a visual indicator on the streamdeck to know if the mic was currently muted or unmuted (since this method is essentially a toggle between muted/unmuted.

What I ended up doing, which I know perhaps is not the most pretty way, was to have the script determine which image should be pushed to the streamdeck. Since I always use this button to mute/unmute, state is almost always current (I despise having to open the plasma-pa KCM to do this :). The script simply checks the current status, figures out where it will be, and based on that simply outputs the path to the desired image. From the streamdeck-ui side, I changed the api.py to check if, when running a script, it outputs the path to an image and if it does, push that to the streamdeck:

$ diff streamdeck-ui/streamdeck_ui/api.py streamdeck_ui_img_callback/api.py 
7c7
< from subprocess import Popen  # nosec - Need to allow users to specify arbitrary commands
---
> from subprocess import Popen, PIPE  # nosec - Need to allow users to specify arbitrary commands
9a10
> from pathlib import Path
40c41,47
<                 Popen(shlex.split(command))
---
>                 p = Popen(shlex.split(command), stdout=PIPE)
>                 output = p.stdout.read()
>                 output = output.decode('utf-8').replace('\n', '')
>                 image_path = Path(output)
>                 if image_path.exists():
>                     set_button_icon(deck_id, page, key, str(image_path))

All this I did on the @dodgyrabbit 's fork. Like I said, not pretty, but since I control the script and the change is quite trivial (and if I break it it's my fault :), I ended up using it. Putting this out there in the case it gives someone any ideas on implementing this.

thats actually not a bad idea. Will try to implement that in my own fork since i was looking for the same as you did

unk1nd avatar Aug 16 '21 18:08 unk1nd

If you want to catch any sound status properly you need to listen events from pulseaudio/alsa. Any other way can't work properly because if any another software manage the sound you will display the wrong sound state. So you need to do it without polling the sound state but by catching state modifications events asynchronously.

To listen pulseaudio events:

For alsa, you need to watch modifications of alsa status file with pyinotify then to read mixer state:

zen2 avatar Jan 08 '22 09:01 zen2

What about a checkbox to "Redraw icon" When that checkbox is selected, on button press:

  • clear cache for that button (image_cache.pop(f"{deck_id}.{page}.{button}", None))
  • redraw (api.render())

Then, you'll have to ensure the command being executed overrides the icon file (as per the path specified in the configuration). This would reload the new icon after the script executes and replaces the icon ...

This solution is similar in spirit to https://github.com/timothycrosley/streamdeck-ui/issues/99#issuecomment-899710503

ipeevski avatar Feb 10 '22 02:02 ipeevski

I just updated to 2.0.3 (I had noticed a weird behaviour, as if commands were being "queued" and only one button press opened at a time - turned out to be my ugly change, ha!) and updated my patch from https://github.com/timothycrosley/streamdeck-ui/issues/99#issuecomment-881919193 so it can be used on the newer version (the function to change is no longer in api.py but rather in gui.py):

$ diff gui.py.orig gui.py
7c7
< from subprocess import Popen  # nosec - Need to allow users to specify arbitrary commands
---
$ diff gui.py.orig gui.py
7c7
< from subprocess import Popen  # nosec - Need to allow users to specify arbitrary commands
---
> from subprocess import Popen, PIPE  # nosec - Need to allow users to specify arbitrary commands
8a9,10
> from pathlib import Path
> from select import select
150c152,159
<                 Popen(shlex.split(command))
---
>                 p = Popen(shlex.split(command), stdout=PIPE)
>                 #output = p.stdout.read()
>                 output = select([p.stdout], [], [], 0.1)[0]
>                 if len(output) > 0:
>                     output = output[0].read().decode('utf-8').replace('\n', '')
>                     image_path = Path(output)
>                     if image_path.exists() and not image_path.is_dir():
>                         api.set_button_icon(deck_id, page, key, str(image_path))

Changes from the previous patch version:

  • Now it no longer breaks your streamdeck if the path it detects from the command is a directory (which used to happen to me, somehow detecting '.' and then streamdeck would never open again until I edited ~/.streamdeck_ui.json to correct it manually).
  • Added a select call on the output of the process: the previous patch would block on read() on commands which do not exit (such as opening a Dolphin, Kwrite or any other window). It should now no longer block so you can use different keys to open different apps at roughly the same time and it works as if you don't have this patch in.
  • No change to ugliness, it's still ugly :D

godlike64 avatar Apr 08 '22 18:04 godlike64

Wouln't it be better to optionally give a cli path that is periodically called with a configurable delay that outputs the filename of an image? That way the state is (optionally) polled and external changes are reflected.

michaelkleinhenz avatar Jul 19 '22 14:07 michaelkleinhenz

Thanks for this amazing little code snippet, @godlike64, I am currently using this to make some toggle buttons. Hope it will make it to the next release!

A solution to "wrong toggle button state at startup" could be that every button should have not only a script for when it is pressed, but also a script for when it is first shown on the stream deck, so it can update the icon with a similar logic to the button press script.

@michaelkleinhenz, I agree, every button should have a script that gets polled every x seconds when the button is visible on the deck, this makes it finally possible to do like an audio meter, a clock, a weather applet... you name it. UI side only need 2 more text fields - one for "startup script" and one for "update script". This also solves the problem @zen2 has pointed out.

OWKenobi avatar Aug 01 '22 11:08 OWKenobi

Been thinking about this as i have similar use case with OBS, where obs-cli is able to read the state of OBS, to say if it is recording or not. I want to be able to see the icon on the Stream Deck, to determinate if im currently recording my screen or not.

The output of obs-cli state reading looks like this:

$ obs-cli recording state
Recording: false

Idea would be to add an icon per state. As many icons or states as you would need. Today's icon could be main state. When the set Command is executed, we could read the output of the command as the state.

So in my example my Recording: false state would be attached to the icon it belongs to I could also add an icon where the state is Recording: true

This solves the syncing issue with toggling, because you expect to read the state output from the command.

In the case of the Command, we might want a separate State Command to read the output im looking for. As one solution to read the state would be to separate it away from "On Key Press" to "On Key Release"

Just some thoughts What do you think?

stiangrindvoll avatar Dec 08 '22 16:12 stiangrindvoll

I think a simpler way to handle this is with "toggle" buttons.

Just my 2¢: I think exposing an API would be more useful as this could easily be expanded to also change the behaviour programmatically, allowing for more freedom in configuring (and more than two states)

mpldr avatar Apr 08 '23 17:04 mpldr

What is the status of this? Especially with the new stream deck and the screen. Would be great to have a status check so buttons can show current state of services. This would unlock so many possibilities.

kevmimcc avatar May 04 '23 08:05 kevmimcc

hi, this project seems to be stale and a new fork as been made with the intention of replacing this one.

if this still an issue please reopen it at: https://github.com/streamdeck-linux-gui/streamdeck-linux-gui

coolapso avatar Jul 27 '23 21:07 coolapso