Skip to content

Commit

Permalink
Add docs and examples
Browse files Browse the repository at this point in the history
  • Loading branch information
miphreal committed Aug 4, 2020
1 parent 47d7065 commit 354be98
Show file tree
Hide file tree
Showing 9 changed files with 154 additions and 25 deletions.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Changelog

## 0.4.0

Lots of backward incompatible changes (mostly introduced to fix issues raised here [#2](https://github.com/miphreal/python-rofi-menu/issues/2))

- added ability to store state during rofi session (e.g. to store previously selected menus); added `FileSession` (keeps the state in a `~/.cache/rofi-menu/*.json`)
- changed `def bind()` methods to `async def build()`
- changed props of `meta` object (now it has `raw_script_input`, `selected_id` and `user_input` props)
- introduced new menu methods `propagate_select` and `propagate_user_input`
- added "middlewares" mechanism to enrich "meta" object
117 changes: 116 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,23 @@ Rofi allows defining custom modes ([see the spec](https://github.com/davatorium/

This lib is a reference implementation with some extra "sugar".

Features:

- simple menu definition via python
- extendable
- async in first place
- allows keeping state during rofi session

Simple demo:

![custom menu](https://github.com/miphreal/python-rofi-menu/raw/master/docs/demo.gif)

## Requirements

- rofi >= 1.5.4
- python >= 3.6


## Installation

Using pip
Expand All @@ -29,6 +42,7 @@ e.g. `example.py` (don't forget to mark it as executable -- `chmod +x ./example.

Assuming you installed `rofi-menu` into a virtual environment (let's say it's `~/.pyenv/versions/rofi/bin/python`).
Make sure shebang points to the right python executable, e.g. `#!/home/user/pyenv/versions/rofi/bin/python`.

```python
#!/home/user/pyenv/versions/rofi/bin/python
import rofi_menu
Expand Down Expand Up @@ -79,4 +93,105 @@ $ rofi -modi mymenu:/path/to/example.py -show mymenu

It'll result in

![rofi menu](https://github.com/miphreal/python-rofi-menu/raw/master/docs/menu-example.png)
![rofi menu](https://github.com/miphreal/python-rofi-menu/raw/master/docs/menu-example.png)


### Advanced example


```
#!/home/user/pyenv/versions/rofi/bin/python
import asyncio
from datetime import datetime
import os
import rofi_menu
class OutputSomeTextItem(rofi_menu.Item):
"""Output arbitrary text on selection"""
async def on_select(self, meta):
# any python code
await asyncio.sleep(0.1)
return rofi_menu.Operation(rofi_menu.OP_OUTPUT, (
"💢 simple\n"
"💥 multi-\n"
"💫 <b>line</b>\n"
"💣 <i>text</i>\n"
))
class DoAndExitItem(rofi_menu.Item):
"""Do something and exit"""
async def on_select(self, meta):
os.system('notify-send msg')
return rofi_menu.Operation(rofi_menu.OP_EXIT)
class CurrentDatetimeItem(rofi_menu.Item):
"""Show current datetime inside menu item"""
async def load(self, meta):
self.state = datetime.now().strftime('%A %d. %B %Y (%H:%M:%S)')
async def render(self, meta):
return f"🕑 {self.state}"
class CounterItem(rofi_menu.Item):
"""Increment counter on selection"""
async def load(self, meta):
await super().load(meta)
self.state = self.state or 0
meta.session.setdefault("counter_total", 0)
async def on_select(self, meta):
self.state += 1
meta.session["counter_total"] += 1
return await super().on_select(meta)
async def render(self, meta):
per_menu_item = self.state
total = meta.session["counter_total"]
return f"🏃 Selected #{per_menu_item} time(s) (across menu items #{total})"
class HandleUserInputMenu(rofi_menu.Menu):
class CustomItem(rofi_menu.Item):
async def render(self, meta):
entered_text = meta.session.get("text", "[ no text ]")
return f"You entered: {entered_text}"
items = [CustomItem()]
async def on_user_input(self, meta):
meta.session['text'] = meta.user_input
return rofi_menu.Operation(rofi_menu.OP_REFRESH_MENU)
main_menu = rofi_menu.Menu(
prompt="menu",
items=[
OutputSomeTextItem("Output anything"),
DoAndExitItem("Do something and exit"),
CurrentDatetimeItem(),
CounterItem(),
CounterItem(),
rofi_menu.NestedMenu("User input", HandleUserInputMenu()),
],
)
if __name__ == "__main__":
rofi_menu.run(main_menu)
```

![advanced example](https://github.com/miphreal/python-rofi-menu/raw/master/docs/menu-example-advanced.png)

## TODO

- [ ] documentation of API
- [ ] examples
- [ ] tests
- [ ] `nonselectable` and other new
- [ ] check what ROFI_* envs can do
Binary file added docs/menu-example-advanced.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "rofi-menu"
version = "0.3.0"
version = "0.4.0"
description = "Create rofi menus via python"
authors = ["miphreal <[email protected]>"]
license = "MIT"
Expand Down
2 changes: 1 addition & 1 deletion rofi_menu/contrib/shell.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def __init__(self, text=None, command="echo OK", **kwargs):
def command(self):
return self._command

async def on_select(self, item_id, meta):
async def on_select(self, meta):
command = f"nohup {self.command}" if self.detached else self.command
proc = await asyncio.create_subprocess_shell(
command,
Expand Down
6 changes: 3 additions & 3 deletions rofi_menu/contrib/touchpad.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,8 @@ def __init__(self, *args, **kwargs):
self.detached = False
self.show_output = False

@property
def icon(self):
return "input-touchpad-symbolic" if self.state else "touchpad-disabled-symbolic"
def get_icon(self, is_touchpad_enabled: bool):
return "input-touchpad-symbolic" if is_touchpad_enabled else "touchpad-disabled-symbolic"

@property
def command(self):
Expand All @@ -32,6 +31,7 @@ async def load(self, meta):
)
data = (await proc.stdout.read()).decode("utf-8")
self.state = bool(self.re_enabled_device.search(data))
self.icon = self.get_icon(is_touchpad_enabled=self.state)

async def render(self, *args, **kwargs):
state_on = '<span background="green"><b>ON</b></span>'
Expand Down
2 changes: 1 addition & 1 deletion rofi_menu/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def run(menu: Menu, stateful: bool=True, middlewares=None) -> None:

middlewares = list(middlewares or [])
if stateful:
middlewares.append(session_middleware(FileSession))
middlewares.append(session_middleware(FileSession()))

handler = main
for middleware in middlewares:
Expand Down
34 changes: 19 additions & 15 deletions rofi_menu/menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,18 @@ class Item:
icon = None
text = None

def __init__(self, text=None, *, flags=None, item_id=None):
self.text = text or self.text
self.flags = flags or set()
self.id = item_id
def __init__(self, text=None, **kwargs):
self.id = kwargs.get("item_id")

# per menu-item state
self.state = None
self.loaded = False

# render options
self.text = text or self.text
self.icon = kwargs.get("icon", self.icon)
self.flags = kwargs.get("flags") or set()

# filled after attaching to menu
self.parent_menu = None

Expand Down Expand Up @@ -116,8 +120,8 @@ async def on_select(self, meta):


class NestedMenu(Item):
def __init__(self, text=None, menu=None, *, flags=None, item_id=None):
super().__init__(text, flags=flags, item_id=item_id)
def __init__(self, text=None, menu=None, **kwargs):
super().__init__(text, **kwargs)
self.sub_menu = menu or Menu()

async def build(self, parent_menu, item_id, meta):
Expand Down Expand Up @@ -167,28 +171,28 @@ class Menu:
prompt = "menu"
items = ()

def __init__(self, prompt=None, items=None, menu_id=None):
def __init__(self, prompt=None, items=None, **kwargs):
self.id = kwargs.get("menu_id")
self.prompt = prompt or self.prompt
self.items = items or self.items
self.id = menu_id

def clone(self):
obj = self.__class__()
obj.__dict__.update(self.__dict__)
return obj

async def build(self, menu_id, meta):
"""Link all nested items to the current menu and return "bound" element."""
self.id = menu_id
obj = self.clone()
obj.id = menu_id
obj.items = await obj.build_menu_items(meta=meta)
return obj

async def build_menu_items(self, meta):
items = await self.generate_menu_items(meta=meta)
# generate bound items
obj = self.clone()
obj.items = await asyncio.gather(*[
item.build(parent_menu=obj, item_id=item.id or [*obj.id, str(item_index)], meta=meta)
return await asyncio.gather(*[
item.build(parent_menu=self, item_id=item.id or [*self.id, str(item_index)], meta=meta)
for item_index, item in enumerate(items)
])
return obj

async def generate_menu_items(self, meta):
return self.items
Expand Down
5 changes: 2 additions & 3 deletions rofi_menu/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,18 @@
import typing


def session_middleware(session_class):
def session_middleware(session, clear_session: bool = True):
"""Handle loading and persisting session."""

def wrap_middleware(func):
session = session_class()

async def wrapper(**kwargs):
meta = kwargs['meta']
meta.session = session

await session.load()

if meta.raw_script_input is None:
if clear_session and meta.raw_script_input is None:
# First run of the script (no passed params) => we can start new session
session.clear()

Expand Down

0 comments on commit 354be98

Please sign in to comment.