Skip to content

Commit 260a3f3

Browse files
committed
extract and refactor menu handling
1 parent 72b1c54 commit 260a3f3

File tree

4 files changed

+99
-85
lines changed

4 files changed

+99
-85
lines changed

src/qbpm/choose.py

Lines changed: 6 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,25 @@
1-
import shutil
21
import subprocess
3-
from collections.abc import Iterable
42
from pathlib import Path
5-
from sys import platform
63

74
from . import Profile
85
from .launch import launch_qutebrowser
9-
from .utils import env_menus, error, installed_menus, or_phrase
6+
from .menus import find_menu
7+
from .utils import error
108

119

1210
def choose_profile(
1311
profile_dir: Path, menu: str | None, foreground: bool, qb_args: tuple[str, ...]
1412
) -> bool:
15-
menu = menu or next(installed_menus(), None)
16-
if not menu:
17-
possible_menus = or_phrase([menu for menu in env_menus() if menu != "fzf-tmux"])
18-
error(
19-
"no menu program found, use --menu to provide a dmenu-compatible menu or install one of "
20-
+ possible_menus
21-
)
22-
return False
23-
if menu == "applescript" and platform != "darwin":
24-
error(f"applescript cannot be used on a {platform} host")
13+
dmenu = find_menu(menu)
14+
if not dmenu:
2515
return False
16+
2617
real_profiles = {profile.name for profile in profile_dir.iterdir()}
2718
if len(real_profiles) == 0:
2819
error("no profiles")
2920
return False
3021
profiles = [*real_profiles, "qutebrowser"]
31-
32-
command = menu_command(menu, profiles, qb_args)
33-
if not command:
34-
return False
35-
22+
command = dmenu.commandline(sorted(profiles), "qutebrowser", " ".join(qb_args))
3623
selection_cmd = subprocess.Popen(
3724
command,
3825
shell=True,
@@ -50,36 +37,3 @@ def choose_profile(
5037
else:
5138
error("no profile selected")
5239
return False
53-
54-
55-
def menu_command(
56-
menu: str, profiles: Iterable[str], qb_args: tuple[str, ...]
57-
) -> str | None:
58-
profiles = sorted(profiles)
59-
arg_string = " ".join(qb_args)
60-
if menu == "applescript":
61-
profile_list = '", "'.join(profiles)
62-
return f"""osascript -e \'set profiles to {{"{profile_list}"}}
63-
set profile to choose from list profiles with prompt "qutebrowser: {arg_string}" default items {{item 1 of profiles}}
64-
item 1 of profile\'"""
65-
66-
prompt = "-p qutebrowser"
67-
command = menu
68-
if len(menu.split(" ")) == 1:
69-
program = Path(menu).name
70-
if program == "rofi":
71-
command = f"{menu} -dmenu -no-custom {prompt} -mesg '{arg_string}'"
72-
elif program == "wofi":
73-
command = f"{menu} --dmenu {prompt}"
74-
elif program.startswith("dmenu"):
75-
command = f"{menu} {prompt}"
76-
elif program.startswith("fzf"):
77-
command = f"{menu} --prompt 'qutebrowser '"
78-
elif program == "fuzzel":
79-
command = f"{menu} -d"
80-
exe = command.split(" ")[0]
81-
if not shutil.which(exe):
82-
error(f"command '{exe}' not found")
83-
return None
84-
profile_list = "\n".join(profiles)
85-
return f'echo "{profile_list}" | {command}'

src/qbpm/main.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@
1010
from . import Profile, operations, profiles
1111
from .choose import choose_profile
1212
from .launch import launch_qutebrowser
13+
from .menus import supported_menus
1314
from .paths import default_profile_dir, qutebrowser_data_dir
14-
from .utils import SUPPORTED_MENUS, error, or_phrase
15+
from .utils import error, or_phrase
1516

1617
CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]}
1718

@@ -139,7 +140,8 @@ def launch(context: Context, profile_name: str, **kwargs: Any) -> None:
139140
"-m",
140141
"--menu",
141142
metavar="COMMAND",
142-
help=f"A dmenu-compatible command or one of the following supported menus: {', '.join(sorted(SUPPORTED_MENUS))}",
143+
help="A dmenu-compatible command or one of the following supported menus: "
144+
+ ", ".join(sorted([getattr(m, "name", "applescript") for m in supported_menus()])),
143145
)
144146
@click.option(
145147
"-f", "--foreground", is_flag=True, help="Run qutebrowser in the foreground."

src/qbpm/menus.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import platform
2+
import sys
3+
from collections.abc import Iterator
4+
from dataclasses import dataclass, replace
5+
from os import environ
6+
from pathlib import Path
7+
from shutil import which
8+
9+
from .utils import error, or_phrase
10+
11+
12+
@dataclass
13+
class Dmenu:
14+
name: str
15+
args: str
16+
17+
def installed(self) -> bool:
18+
return which(self.name) is not None
19+
20+
def command(self, prompt: str, qb_args: str) -> str:
21+
return f"{self.name} {self.args.format(prompt=prompt, qb_args=qb_args)}"
22+
23+
def commandline(self, profiles: list[str], prompt: str, qb_args: str) -> str:
24+
profile_list = "\n".join(profiles)
25+
return f'echo "{profile_list}" | {self.command(prompt, qb_args)}'
26+
27+
28+
class ApplescriptMenu:
29+
@classmethod
30+
def installed(cls) -> bool:
31+
return platform.system() == "Darwin"
32+
33+
@classmethod
34+
def commandline(cls, profiles: list[str], _prompt: str, qb_args: str) -> str:
35+
profile_list = '", "'.join(profiles)
36+
return f"""osascript -e \'set profiles to {{"{profile_list}"}}
37+
set profile to choose from list profiles with prompt "qutebrowser: {qb_args}" default items {{item 1 of profiles}}
38+
item 1 of profile\'"""
39+
40+
41+
def find_menu(menu: str | None) -> Dmenu | ApplescriptMenu | None:
42+
menus = list(supported_menus())
43+
if not menu:
44+
found = next(filter(lambda m: m.installed(), menus), None)
45+
if not found:
46+
error(
47+
"no menu program found, use --menu to provide a dmenu-compatible menu or install one of "
48+
+ or_phrase([m.name for m in menus if isinstance(m, Dmenu)])
49+
)
50+
return found
51+
dmenu = custom_dmenu(menu)
52+
if not dmenu.installed():
53+
error(f"{dmenu.name} not found")
54+
return None
55+
return dmenu
56+
57+
58+
def custom_dmenu(command: str) -> Dmenu:
59+
split = command.split(" ", maxsplit=1)
60+
if len(split) == 1 or not split[1]:
61+
name = Path(command).name
62+
for m in supported_menus():
63+
if isinstance(m, Dmenu) and m.name == name:
64+
return m if m.name == command else replace(m, name=command)
65+
return Dmenu(split[0], split[1] if len(split) == 2 else "")
66+
67+
68+
def supported_menus() -> Iterator[Dmenu | ApplescriptMenu]:
69+
if ApplescriptMenu.installed():
70+
yield ApplescriptMenu()
71+
if environ.get("WAYLAND_DISPLAY"):
72+
yield from [
73+
# default window is too narrow for a long prompt
74+
Dmenu("fuzzel", "--dmenu"),
75+
Dmenu("wofi", "--dmenu -p {prompt}"),
76+
Dmenu("dmenu-wl", "-p {prompt}"),
77+
]
78+
if environ.get("DISPLAY"):
79+
yield from [
80+
Dmenu(
81+
"rofi",
82+
"-dmenu -no-custom -p {prompt} -mesg '{qb_args}'",
83+
),
84+
Dmenu("dmenu", "-p {prompt}"),
85+
]
86+
if sys.stdin.isatty():
87+
if environ.get("TMUX"):
88+
yield Dmenu("fzf-tmux", "--prompt {prompt}")
89+
yield Dmenu("fzf", "--prompt {prompt}")

src/qbpm/utils.py

Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,4 @@
11
import logging
2-
import platform
3-
from collections.abc import Iterator
4-
from os import environ
5-
from shutil import which
6-
7-
WAYLAND_MENUS = ["fuzzel", "wofi", "dmenu-wl"]
8-
X11_MENUS = ["rofi", "dmenu"]
9-
SUPPORTED_MENUS = [*WAYLAND_MENUS, *X11_MENUS, "fzf", "applescript"]
102

113

124
def info(msg: str) -> None:
@@ -17,29 +9,6 @@ def error(msg: str) -> None:
179
logging.error(msg)
1810

1911

20-
def installed_menus() -> Iterator[str]:
21-
if platform.system() == "Darwin":
22-
yield "applescript"
23-
for menu_cmd in env_menus():
24-
if which(menu_cmd) is not None:
25-
if menu_cmd == "fzf":
26-
info("no graphical launchers found, trying fzf")
27-
yield menu_cmd
28-
29-
30-
def env_menus() -> Iterator[str]:
31-
if environ.get("WAYLAND_DISPLAY"):
32-
yield from WAYLAND_MENUS
33-
elif environ.get("DISPLAY"):
34-
yield from X11_MENUS
35-
if environ.get("TMUX"):
36-
yield "fzf-tmux"
37-
# if there's no display and fzf is installed we're probably(?) in a term
38-
if which("fzf") is not None:
39-
info("no graphical launchers found, trying fzf")
40-
yield "fzf"
41-
42-
4312
def or_phrase(items: list) -> str:
4413
strings = list(map(str, items))
4514
size = len(strings)

0 commit comments

Comments
 (0)