Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

external_script: show notification with full output on click, refresh only on button_refresh #1439

Closed
110 changes: 105 additions & 5 deletions py3status/modules/external_script.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,68 @@
format: see placeholders below (default '{output}')
localize: should script output be localized (if available)
(default True)
notifications: specify a nested dict to send a notification if matched
against the keys, see ``Notifications`` section for more information
(default {})
script_path: script you want to show output of (compulsory)
(default None)
strip_output: shall we strip leading and trailing spaces from output
(default False)

Format placeholders:
{output} output of script given by "script_path"
{line} number of lines in the output
{output} first line of the output of script given by "script_path"
{output_full} full output of script given by "script_path"

i3status.conf example:
Notifications:
Specify a nested dictionary of notification states and options to use.

Notification states:
'changed': display a notification only if it is changed
'click': display a notification of last output on click
'current': display a current notification regardless

Notification options:
'title': notification title
'msg': notification message
'level': must be 'info', 'error' or 'warning'.
'rate_limit': time period in seconds not to be repeated
'icon': must be an icon path or icon name.

You can add `format` placeholders in `msg` and `title`.
The `msg` and `title` options will also be formatted.

Examples:
```
# add a script
external_script {
format = "my name is {output}"
script_path = "/usr/bin/whoami"
}

# display changed notifications, same output means no notification
external_script {
notifications = {'changed': {'msg': '{output}'}}
script_path = "~/my_script.py"
}

# display current notifications, no output means no notification
external_script {
notifications = {'current': {'msg': '{output}'}}
script_path = "~/my_script.py"
}

# display current notification only if output have more than 20 lines
external_script {
notifications = {'current': {'msg': '\?if=line>20 {output}'}}
script_path = "~/my_script.py"
}

# display a notification of last full output on click
external_script {
notifications = {'click': {'msg': '{output_full}'}}
script_path = "~/my_script.py"
}
```

@author frimdo [email protected]
Expand All @@ -52,20 +99,55 @@ class Py3status:
cache_timeout = 15
format = '{output}'
localize = True
notifications = {}
script_path = None
strip_output = False

def post_config_hook(self):
if not self.script_path:
raise Exception(STRING_ERROR)

self.button_refresh = 2
self.notification = {'normal': [], 'click': False}
if self.notifications:
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lasers could you describe what is happening in this if block?

Copy link
Contributor

@lasers lasers Nov 10, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

post_config_hook checking to find out which notifications to use... so we use a dict+list of booelans instead of repeatedly testing the conditions on each interval.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm looking at this right now and I think it's a bit over-engineered, both external_script and battery_level (and potentially updates) only need notifications on click (battery has extra special case of showing notification on certain threshold), is there any module that currently shows notifications always or when changed?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIRC I think battery's implementation of showing notification on clicks is a bit silly. We likely could make it show up on the bar instead. Maybe with a toggle to show/hide what we want.

Current and changed is similar. I'm not even sure if it makes sense to make a changed state... because we technically could make changed placeholder so you do \?if=changed {test} in current state. Maybe better not to. Fleshing out a nice notification solution is hard though.

  • wwan on changed.
  • github on always (always failing). Could be in post_config_hook instead.
  • battery_level is both click and changed.
    pomodoro is changed when timer reached zero. Not sure how we would add into this as it's not changed.

Things to take from this is that we are only making notification states to allow different kind of notifications to happen in a module. I guess we could use changed here.

  • graphite seems to be always.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • wwan on changed - 👍
  • github is not always, it's a weird way of showing error, completely custom use of notifications and should not be generalized
  • battery_level is click and on certain thresholds, this is again custom and is not "changed" (i.e. it will not show notification when changing from 81% to 80%).
  • pomodoro yes, it's again not changed but some custom logic.

I'm not sure how much we will be able to generalize if every module has some special use of notifications...

for x in ['changed', 'click', 'current']:
self.notification[x] = self.notifications.get(x, False)
if self.notification[x]:
if x in ['changed', 'current']:
self.notification['normal'].append(x)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still cannot grasp this logic, if notification is added for "changed", we add it to "normal"? What is normal?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With only two, it would be just if current or changed:.
With only 10 or 100, it would be just if current or changed or .. or .. or .. or .. False:

I add "True" to the list and we check the list itself to see if it's empty or not... instead of going through the list to find a first "True". When we go in, we now have (already optimized) list.

It's a bit different here because of notifications so we check both if statements. In other scenarios, we might use that list to loop through something.

Normal is both current+changed, but I don't want to say current. Idk.

self.last_changed = self.py3.storage_get('changed')

def _get_notification(self, state):
temporary = self.notification[state].copy()
for x in ['title', 'msg']:
if x in temporary:
temporary[x] = self.py3.get_composite_string(
self.py3.safe_format(temporary[x], self.script_data)
)
return temporary

def _notify_user(self, state=None):
if state is None:
if self.notification['changed']:
changed = self._get_notification('changed')
if changed != self.last_changed:
self.last_changed = changed
self.py3.storage_set('changed', changed)
self.py3.notify_user(**changed)
if self.notification['current']:
self.py3.notify_user(**self._get_notification('current'))
elif state == 'click':
self.py3.notify_user(**self._get_notification('click'))

def external_script(self):
output_lines = None
response = {}
response['cached_until'] = self.py3.time_in(self.cache_timeout)
try:
output = self.py3.command_output(self.script_path, shell=True, localized=self.localize)
output_lines = output.splitlines()
output_full = self.py3.command_output(
self.script_path, shell=True, localized=self.localize
)
output_lines = output_full.splitlines()
if len(output_lines) > 1:
output_color = output_lines[1]
if re.search(r'^#[0-9a-fA-F]{6}$', output_color):
Expand Down Expand Up @@ -96,10 +178,28 @@ def external_script(self):
else:
output = ''

self.script_data = {
'line': len(output_lines),
'output': output,
'output_full': ' '.join(output_full.splitlines())
}
response['full_text'] = self.py3.safe_format(
self.format, {'output': output})
self.format, self.script_data
)

self.script_data['output_full'] = output_full
if self.notification['normal']:
self._notify_user()

return response

def on_click(self, event):
button = event["button"]
if button != self.button_refresh:
if self.notification['click']:
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a reminder for future self: this throws exception if notification object doesn't contain click key

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What configs? I'm not able to reproduce. The notification dict should already have all three.

self.notification = {'normal': []}
if self.notifications:
for x in ['changed', 'click', 'current']:
self.notification[x] = self.notifications.get(x, False)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is my exact config:

order += 'external_script date'

external_script date {
  cache_timeout = 60
  script_path = 'date +"%a, %d %b"'
  format = 'Calendar: {output}'
  on_click 1 = 'exec gsimplecal'
}

Add this, then restart i3 (important), then click on the module.

I get an error, and in journalctl I see

py3status[12014]: on_click event in `external_script date` failed (KeyError) external_script.py line 199.
py3status[12014]: on_click event in `external_script date` failed (KeyError) external_script.py line 199. Please try to fix this and reload i3wm (Mod+Shift+R)

self._notify_user('click')
self.py3.prevent_refresh()


if __name__ == "__main__":
"""
Expand Down