···11+# 2.4
22+ - `qbpm choose`: an entry named `qutebrowser` that launches qutebrowser without a profile will no longer be included by default. Set `qutebrowser_in_choose = true` in `config.toml` to restore it
33+ - `config_py_template` is no longer required to be set if `config.toml` exists
44+ - if `config_py_template` is set to an empty string, no `config.py` will be generated
55+66+# 2.3
77+ - new profiles will have a symlink to `$XDG_DATA_HOME/qutebrowser/qtwebengine_dictionaries` and thus have access to any spellchecking dictionaries have been installed for the main qutebrowser profile
88+ - `qbpm config` now has a `--help` flag
99+1010+# ~2.1~ 2.2
1111+ - `config.toml` supports `application_name` for generated XDG desktop files
1212+ - defaults to `{profile_name} (qutebrowser profile)`, you may want just `{profile_name}`
1313+ - `qbpm desktop` can be used to replace existing desktop files
1414+ - bumped to 2.2 because I pushed a 2.1 tag prematurely
1515+1616+# 2.0
1717+## config
1818+qbpm now reads configuration options from `$XDG_CONFIG_HOME/qbpm/config.toml`!
1919+ - to install the default config file:
2020+ - run `qbpm config path` and confirm that it prints out a path
2121+ - run `qbpm config default > "$(qbpm config path)"`
2222+ - supported configuration options:
2323+ - `config_py_template`: control the contents of `config.py` in new profiles
2424+ - `symlink_autoconfig`: symlink qutebrowser's `autoconfig.yml` in new profiles
2525+ - `profile_directory` and `qutebrowser_config_directory`
2626+ - equivalent to `--profile-dir` and `--qutebrowser-config-dir`
2727+ - `generate_desktop_file` and `desktop_file_directory`
2828+ - whether to generate XDG desktop entries for new profiles and where to put them
2929+ - `menu`: equivalent to `--menu` for `qbpm choose`
3030+ - `menu_prompt`: prompt shown in most menus
3131+ - see default config file for more detailed documentation
3232+3333+## other
3434+ - support for symlinking `autoconfig.yml` in addition to or instead of sourcing `config.py`
3535+ - `qbpm new --overwrite`: back up existing config files by moving to e.g. `config.py.bak`
3636+ - `contrib/qbpm.desktop`: add `MimeType` and `Keywords`, fix incorrect formatting of `Categories`
3737+ - allow help text to be slightly wider to avoid awkward line breaks
3838+ - macOS: fix detection of qutebrowser binary in `/Applications`
3939+140# 1.0rc4
22- - built in support for more wayland menus:
33- - walker
44- - tofi
55- - wmenu
4141+ - `choose`: support `walker`, `tofi`, and `wmenu`
4242+ - better detection of invalid/nonexistent profiles
643744# 1.0rc3
845 - breaking: stop sourcing files from `~/.config/qutebrowser/conf.d/`
···1754 - make generated `.desktop` files match qutebrowser's more closely
18551956# 1.0rc2:
2020- - `choose`: builtin support for `fzf` and `fuzzel`
2121- - moved argument handling to click
5757+ - `choose`: support `fzf` and `fuzzel`
5858+ - use `click `for CLI parsing
2259 - `qbpm launch`'s `-n`/`--new` renamed to `-c`/`--create`
6060+ - expand fish shell completions
6161+6262+# 1.0rc1:
6363+ - add a man page
6464+6565+# 0.6
6666+ - better error handling
6767+6868+# 0.5
6969+ - `choose`: support custom menu command
7070+ - `choose`: support `dmenu-wl` and `wofi`
7171+7272+# 0.4
7373+ - `choose` subcommand (thanks, @mtoohey31!)
7474+ - load autoconfig.yml by default
7575+ - shell completions for fish
+25-31
README.md
···11# qutebrowser profile manager
2233[](https://builds.sr.ht/~pvsr/qbpm/commits/main?)
44+[](https://pypi.python.org/pypi/qbpm)
4556qbpm (qutebrowser profile manager) is a tool for creating, managing, and running
67[qutebrowser](https://github.com/qutebrowser/qutebrowser) profiles. Profile support
···1819instances of qutebrowser which can be opened and closed independently.
19202021## Usage
2121-Create a new profile called "python", edit its `config.py`, then launch it:
2222+To create a new profile called "python" and launch it with the python docs open:
2223```
2324$ qbpm new python
2424-$ qbpm edit python
2525$ qbpm launch python docs.python.org
2626-$ qbpm choose # run dmenu or another launcher to pick a profile
2726```
28272929-`qbpm from-session` can copy the tabs of a [saved qutebrowser
3030-session](https://qutebrowser.org/doc/help/commands.html#session-save) to a new
3131-profile. If you have a window full of tabs related to planning a vacation, you
3232-could save it to a session called "vacation" using `:session-save -o vacation`
3333-in qutebrowser, then create a new profile with those tabs:
3434-```
3535-$ qbpm from-session vacation
3636-```
2828+Note that all arguments after `qbpm launch PROFILE` are passed to qutebrowser,
2929+so options can be passed too: `qbpm launch python --target window pypi.org`.
37303838-The default profile directory is `$XDG_DATA_HOME/qutebrowser-profiles`, where
3939-`$XDG_DATA_HOME` is usually `~/.local/share`, but you can create and launch
4040-profiles from anywhere using `--profile-dir`/`-P`:
4141-```
4242-$ qbpm --profile-dir ~/dev/my-project new qb-profile
4343-$ cd ~/dev/my-project
4444-$ qbpm -P . launch qb-profile
4545-# or
4646-$ qutebrowser --basedir qb-profile
4747-```
3131+If you have multiple profiles you can use `qbpm choose` to bring up a list of
3232+profiles and select one to launch. Depending on what your system has available
3333+the menu may be `dmenu`, `fuzzel`, `fzf`, an applescript dialog, or one of many
3434+other menu programs qbpm can detect. Any dmenu-compatible menu can be used with
3535+`--menu`, e.g. `qbpm choose --menu 'fuzzel --dmenu'`. As with `qbpm launch`,
3636+extra arguments are passed to qutebrowser.
3737+3838+Run `qbpm --help` to see other available commands.
3939+4040+By default when you create a new profile a `.desktop` file is created that
4141+launches the profile. This launcher does not depend on qbpm at all, so if you
4242+want you can run `qbpm new` once and keep using the profile without needing
4343+qbpm installed on your system.
48444945## Installation
5046If you use Nix, you can install or run qbpm as a [Nix flake](https://nixos.wiki/wiki/Flakes).
···52485349On Arch and derivatives, you can install the AUR package: [qbpm-git](https://aur.archlinux.org/packages/qbpm-git).
54505555-Otherwise you'll need to install from source, directly or using a tool like [uv](https://docs.astral.sh/uv/guides/tools/).
5656-Using uv you can run qbpm without installing it using
5757-`uv tool run --with git+https://github.com/pvsr/qbpm qbpm`, or install to `~/.local/bin` with
5858-`uv tool install --with git+https://github.com/pvsr/qbpm qbpm`.
5959-The downside of a source installation is that the [man page](https://github.com/pvsr/qbpm/blob/main/qbpm.1.scd)
5151+Otherwise you can install directly from PyPI using [uv](https://docs.astral.sh/uv/guides/tools/),
5252+pip, or your preferred client. With uv it's `uv tool run qbpm` to run qbpm
5353+without installing and `uv tool install qbpm` to install to `~/.local/bin`.
5454+The downside of going through PyPI is that the [man page](https://github.com/pvsr/qbpm/blob/main/qbpm.1.scd)
6055and shell completions will not be installed automatically.
61566257On Linux you can copy [`contrib/qbpm.desktop`](https://raw.githubusercontent.com/pvsr/qbpm/main/contrib/qbpm.desktop)
···6661### MacOS
67626863Nix and uv will install qbpm as a command-line application, but if you want a
6969-native Mac application you can clone this repository or copy the contents of
7070-[`contrib/qbpm.platypus`](https://raw.githubusercontent.com/pvsr/qbpm/main/contrib/qbpm.platypus)
7171-to a local file, install [platypus](https://sveinbjorn.org/platypus),
7272-and create a qbpm app by running `platypus -P qbpm.platypus /Applications/qbpm.app`.
7373-That will also make qbpm available as a default browser in `System Preferences > General > Default web browser`.
6464+native Mac application you can download [`contrib/qbpm.platypus`](https://raw.githubusercontent.com/pvsr/qbpm/main/contrib/qbpm.platypus),
6565+install [platypus](https://sveinbjorn.org/platypus), and create a qbpm app with
6666+`platypus -P qbpm.platypus /Applications/qbpm.app`. That will also make qbpm
6767+available as a default browser in `System Preferences > General > Default web browser`.
74687569Note that there is currently [a qutebrowser bug](https://github.com/qutebrowser/qutebrowser/issues/3719)
7670that results in unnecessary `file:///*` tabs being opened.
···6677# SYNOPSIS
8899-*qbpm* [--profile-dir=<path>|-P <path>] <command> [<args>]
99+*qbpm* [--profile-dir=<path>|-P <path>] [--config-file|-c <path>] <command> [<args>]
10101111# DESCRIPTION
1212···3030 Use _path_ as the profile directory instead of the default location. Takes
3131 precedence over the QBPM_PROFILE_DIR environment variable.
32323333-*-C, --config-dir*
3434- Source config files from the provided directory instead of the global
3535- qutebrowser config location.
3333+*-c, --config-file* <path>
3434+ Read configuration for qbpm from _path_. Defaults to ~/.config/qbpm/config.toml.
36353736# COMMANDS
3837···4847 *-f, --foreground*
4948 If --launch is set, run qutebrowser in the foreground.
50495151- *--no-desktop-file*
5252- Do not generate an XDG desktop entry for the profile. Always true on
5353- non-linux systems. See https://wiki.archlinux.org/title/Desktop_entries
5050+ *-C, --qutebrowser-config-dir* <path>
5151+ Source config files from the provided directory instead of the global
5252+ qutebrowser config location.
5353+5454+ *--desktop-file/--no-desktop-file*
5555+ Whether to generate an XDG desktop entry for the profile. Only relevant
5656+ on linux systems. See https://wiki.archlinux.org/title/Desktop_entries
5457 for information on desktop entries.
55585659 *--overwrite*
···5861 already exists. --overwrite disables this check and replaces the existing
5962 profile's configuration files. Profile data is left untouched.
60636161-*launch* [options] <profile> [argument...]
6464+*launch* [options] <profile> [arguments...]
6265 Start qutebrowser with --basedir set to the location of _profile_. All
6366 arguments following _profile_ will be passed on to qutebrowser.
6467···8083 qbpm launch -n qb-dev --debug --json-logging
8184 ```
82858383-*choose* [options]
8686+*choose* [options] [arguments...]
8487 Open a menu to choose a qutebrowser profile to launch. On linux this defaults
8588 to dmenu or another compatible menu program such as rofi, and on macOS this
8686- will be an applescript dialog.
8989+ will be an applescript dialog. All arguments are passed to qutebrowser.
87908891 *-m, --menu* <menu>
8992 Use _menu_ instead of the default menu program. This may be the name of a
···126129127130Peter Rice
128131129129-Contribute at https://github.com/pvsr/qbpm
132132+# CONTRIBUTE
133133+134134+_https://github.com/pvsr/qbpm_
135135+136136+_https://codeberg.org/pvsr/qbpm_
+5-9
src/qbpm/__init__.py
···11from pathlib import Path
22-from typing import Optional
3243from .log import error
54from .paths import qutebrowser_exe
···2019 self.profile_dir = profile_dir
2120 self.root = self.profile_dir / name
22212323- def check(self) -> Optional["Profile"]:
2424- if "/" in self.name:
2525- error("profile name cannot contain slashes")
2626- return None
2727- return self
2828-2929- def exists(self) -> bool:
3030- return self.root.exists() and self.root.is_dir()
2222+ def check_name(self) -> bool:
2323+ if "/" in self.name or self.name in [".", ".."]:
2424+ error("profile name cannot be a path")
2525+ return False
2626+ return True
31273228 def cmdline(self) -> list[str]:
3329 return [
···11+# template that new config.py files are generated from
22+# supported placeholders: {profile_name}, {source_config_py}
33+config_py_template = """
44+config.source(r'{source_config_py}')
55+66+c.window.title_format += ' ({profile_name})'
77+88+config.load_autoconfig()
99+"""
1010+1111+# symlink autoconfig.yml in new profiles if the os supports it
1212+# symlink_autoconfig = false
1313+1414+# location to store qutebrowser profiles
1515+# profile_directory = "~/.local/share/qutebrowser-profiles"
1616+1717+# location of the qutebrowser config to inherit from
1818+# qutebrowser_config_directory = "~/.config/qutebrowser"
1919+2020+# when creating a profile also generate an XDG desktop file that launches the profile
2121+# defaults to true on linux
2222+# generate_desktop_file = false
2323+# desktop_file_directory = "~/.local/share/applications/qbpm"
2424+2525+# application name in XDG desktop file (replace existing with `qbpm desktop PROFILE_NAME`)
2626+# supported placeholders: {profile_name}
2727+# application_name = "{profile_name} (qutebrowser profile)"
2828+2929+# profile selection menu for `qbpm choose`
3030+# when not set, qbpm will try to find a menu program on your $PATH
3131+# run `qbpm choose --help` for a list of known menu programs
3232+# if menu is a known menu, dmenu-mode flags are set automatically
3333+# menu = "fuzzel" # gets turned into "fuzzel --dmenu", /path/to/fuzzel also works
3434+# otherwise menu must be a dmenu-compatible commandline
3535+# supported placeholders: {prompt}, {qb_args}
3636+# menu = "~/bin/my-dmenu"
3737+# menu = "fuzzel --dmenu --prompt '{prompt}> ' --lines 20 --width 50"
3838+# optionally menu can be written as a list to simplify quoting
3939+# menu = ["fuzzel", "--dmenu", "--prompt", "{prompt}> ", "--lines", "20", "--width", "50"]
4040+4141+# value of {prompt} in menu commands
4242+# supported placeholders: {qb_args}
4343+# defaults to "qutebrowser"
4444+# menu_prompt = "qbpm"
4545+# menu_prompt = "profiles"
4646+# menu_prompt = "qutebrowser {qb_args}"
4747+4848+# include a `qutebrowser` entry in `qbpm choose` that starts qutebrowser without a profile
4949+# qutebrowser_in_choose = false
···2424 p = subprocess.Popen(args, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE)
2525 try:
2626 # give qb a chance to validate input before returning to shell
2727- stdout, stderr = p.communicate(timeout=0.1)
2727+ _stdout, stderr = p.communicate(timeout=0.1)
2828 print(stderr.decode(errors="ignore"), end="")
2929 except subprocess.TimeoutExpired:
3030 pass
+100-50
src/qbpm/main.py
···8899import click
10101111-from . import Profile, operations, profiles
1111+from . import Profile, profiles
1212from .choose import choose_profile
1313+from .config import DEFAULT_CONFIG_FILE, Config, find_config
1414+from .desktop import create_desktop_file
1315from .launch import launch_qutebrowser
1414-from .log import error, or_phrase
1516from .menus import supported_menus
1616-from .paths import default_profile_dir, qutebrowser_data_dir
1717+from .paths import default_qbpm_config_dir
1818+from .session import profile_from_session
17191818-CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]}
2020+CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"], "max_content_width": 91}
192120222123@dataclass
2224class Context:
2323- profile_dir: Path
2525+ cli_profile_dir: Path | None
2626+ cli_config_file: Path | None
2727+2828+ def load_config(self) -> Config:
2929+ config = find_config(self.cli_config_file)
3030+ if self.cli_profile_dir:
3131+ config.profile_directory = self.cli_profile_dir
3232+ return config
243325342635@dataclass
···2837 qb_config_dir: Path | None
2938 launch: bool
3039 foreground: bool
3131- desktop_file: bool
4040+ desktop_file: bool | None
3241 overwrite: bool
33423443···4150 qb_config_dir: Path | None,
4251 launch: bool,
4352 foreground: bool,
4444- desktop_file: bool,
5353+ desktop_file: bool | None,
4554 overwrite: bool,
4655 *args: Any, # noqa: ANN401
4756 **kwargs: Any, # noqa: ANN401
···6170 "--qutebrowser-config-dir",
6271 "qb_config_dir",
6372 type=click.Path(file_okay=False, readable=True, path_type=Path),
6464- help="Location of the qutebrowser config to inherit from.",
7373+ help="Location of the qutebrowser config to source.",
6574 ),
6675 click.option("-l", "--launch", is_flag=True, help="Launch the profile."),
6776 click.option(
···7180 help="If --launch is set, run qutebrowser in the foreground.",
7281 ),
7382 click.option(
7474- "--no-desktop-file",
7575- "desktop_file",
7676- default=True,
7777- is_flag=True,
7878- flag_value=False,
7979- help="Do not generate an XDG desktop entry for the profile.",
8383+ "--desktop-file/--no-desktop-file",
8484+ default=None,
8585+ help="Generate an XDG desktop entry for the profile.",
8086 ),
8187 click.option(
8288 "--overwrite",
···102108 "--profile-dir",
103109 type=click.Path(file_okay=False, writable=True, path_type=Path),
104110 envvar="QBPM_PROFILE_DIR",
105105- show_envvar=True,
111111+ show_envvar=False,
106112 default=None,
107113 help="Location to store qutebrowser profiles.",
108114)
109115@click.option(
116116+ "-c",
117117+ "--config-file",
118118+ type=click.Path(dir_okay=False, writable=True, path_type=Path),
119119+ help="Location of qbpm config file.",
120120+)
121121+@click.option(
110122 "-l",
111123 "--log-level",
112124 default="error",
113125 type=click.Choice(["debug", "info", "error"], case_sensitive=False),
114126)
115127@click.pass_context
116116-def main(ctx: click.Context, profile_dir: Path | None, log_level: str) -> None:
128128+def main(
129129+ ctx: click.Context,
130130+ profile_dir: Path | None,
131131+ config_file: Path | None,
132132+ log_level: str,
133133+) -> None:
117134 root_logger = logging.getLogger()
118135 root_logger.setLevel(log_level.upper())
119136 handler = logging.StreamHandler()
120137 handler.setFormatter(LowerCaseFormatter("{levelname}: {message}", style="{"))
121138 root_logger.addHandler(handler)
122122- ctx.obj = Context(profile_dir or default_profile_dir())
139139+ ctx.obj = Context(profile_dir, config_file)
123140124141125142@main.command()
···134151 c_opts: CreatorOptions,
135152) -> None:
136153 """Create a new profile."""
137137- profile = Profile(profile_name, **vars(context))
154154+ config = context.load_config()
155155+ profile = Profile(profile_name, config.profile_directory)
156156+ if c_opts.qb_config_dir:
157157+ config.qutebrowser_config_directory = c_opts.qb_config_dir.absolute()
158158+ if c_opts.desktop_file is not None:
159159+ config.generate_desktop_file = c_opts.desktop_file
138160 exit_with(
139161 profiles.new_profile(
140162 profile,
141141- c_opts.qb_config_dir,
163163+ config,
142164 home_page,
143143- c_opts.desktop_file,
144165 c_opts.overwrite,
145166 )
146167 and ((not c_opts.launch) or launch_qutebrowser(profile, c_opts.foreground))
···163184 SESSION may be the name of a session in the global qutebrowser profile
164185 or a path to a session yaml file.
165186 """
166166- profile, session_path = session_info(session, profile_name, context)
187187+ config = context.load_config()
188188+ if c_opts.qb_config_dir:
189189+ config.qutebrowser_config_directory = c_opts.qb_config_dir.absolute()
190190+ if c_opts.desktop_file is not None:
191191+ config.generate_desktop_file = c_opts.desktop_file
192192+ profile = profile_from_session(
193193+ session,
194194+ profile_name,
195195+ config,
196196+ c_opts.overwrite,
197197+ )
167198 exit_with(
168168- operations.from_session(
169169- profile,
170170- session_path,
171171- c_opts.qb_config_dir,
172172- c_opts.desktop_file,
173173- c_opts.overwrite,
174174- )
199199+ profile is not None
175200 and ((not c_opts.launch) or launch_qutebrowser(profile, c_opts.foreground))
176201 )
177202···189214 """Launch qutebrowser with a specific profile.
190215191216 All QB_ARGS are passed on to qutebrowser."""
192192- profile = Profile(profile_name, **vars(context))
217217+ profile = Profile(profile_name, context.load_config().profile_directory)
218218+ if not profiles.check(profile):
219219+ sys.exit(1)
193220 exit_with(launch_qutebrowser(profile, foreground, qb_args))
194221195222···214241 Support is built in for many X and Wayland launchers, as well as applescript dialogs.
215242 All QB_ARGS are passed on to qutebrowser.
216243 """
217217- exit_with(choose_profile(context.profile_dir, menu, foreground, qb_args))
244244+ config = context.load_config()
245245+ if menu:
246246+ config.menu = menu
247247+ exit_with(
248248+ choose_profile(
249249+ config,
250250+ foreground,
251251+ qb_args,
252252+ )
253253+ )
218254219255220256@main.command()
···222258@click.pass_obj
223259def edit(context: Context, profile_name: str) -> None:
224260 """Edit a profile's config.py."""
225225- profile = Profile(profile_name, **vars(context))
226226- if not profile.exists():
227227- error(f"profile {profile.name} not found at {profile.root}")
261261+ profile = Profile(profile_name, context.load_config().profile_directory)
262262+ if not profiles.check(profile):
228263 sys.exit(1)
229264 click.edit(filename=str(profile.root / "config" / "config.py"))
230265···233268@click.pass_obj
234269def list_(context: Context) -> None:
235270 """List existing profiles."""
236236- for profile in sorted(context.profile_dir.iterdir()):
271271+ for profile in sorted(context.load_config().profile_directory.iterdir()):
237272 print(profile.name)
238273239274···245280 profile_name: str,
246281) -> None:
247282 """Create an XDG desktop entry for an existing profile."""
248248- profile = Profile(profile_name, **vars(context))
249249- exit_with(operations.desktop(profile))
283283+ config = context.load_config()
284284+ profile = Profile(profile_name, config.profile_directory)
285285+ exists = profiles.check(profile)
286286+ if exists:
287287+ create_desktop_file(
288288+ profile, config.desktop_file_directory, config.application_name
289289+ )
290290+ exit_with(exists)
291291+292292+293293+@main.group()
294294+def config() -> None:
295295+ """Commands to create a qbpm config file.
250296297297+ qbpm config default > "$(qbpm config path)"
298298+ """
299299+ pass
251300252252-def session_info(
253253- session: str, profile_name: str | None, context: Context
254254-) -> tuple[Profile, Path]:
255255- user_session_dir = qutebrowser_data_dir() / "sessions"
256256- session_paths = []
257257- if "/" not in session:
258258- session_paths.append(user_session_dir / (session + ".yml"))
259259- session_paths.append(Path(session))
260260- session_path = next(filter(lambda path: path.is_file(), session_paths), None)
301301+302302+@config.command()
303303+@click.pass_obj
304304+def path(context: Context) -> None:
305305+ """Print the location where qbpm will look for a config file."""
306306+ if context.cli_config_file:
307307+ print(context.cli_config_file.absolute())
308308+ else:
309309+ config_dir = default_qbpm_config_dir()
310310+ config_dir.mkdir(parents=True, exist_ok=True)
311311+ print(config_dir / "config.toml")
261312262262- if not session_path:
263263- tried = or_phrase([str(p.resolve()) for p in session_paths])
264264- error(f"could not find session file at {tried}")
265265- sys.exit(1)
266313267267- return (Profile(profile_name or session_path.stem, **vars(context)), session_path)
314314+@config.command
315315+def default() -> None:
316316+ """Print the default qbpm config file."""
317317+ print(DEFAULT_CONFIG_FILE.read_text(), end="")
268318269319270320def exit_with(result: bool) -> NoReturn:
+29-21
src/qbpm/menus.py
···2121 return which(self.name()) is not None
22222323 def command(self, _profiles: list[str], prompt: str, qb_args: str) -> list[str]:
2424+ prompt = prompt.format(qb_args=qb_args)
2425 return [arg.format(prompt=prompt, qb_args=qb_args) for arg in self.menu_command]
25262627···4546 ]
464747484848-def find_menu(menu: str | None) -> Dmenu | ApplescriptMenu | None:
4949+def find_menu(menu: str | list[str] | None) -> Dmenu | ApplescriptMenu | None:
5050+ if menu:
5151+ dmenu = custom_dmenu(menu)
5252+ if not dmenu.installed():
5353+ error(f"{dmenu.name()} not found")
5454+ return None
5555+ return dmenu
4956 menus = list(supported_menus())
5050- if not menu:
5151- found = next(filter(lambda m: m.installed(), menus), None)
5252- if not found:
5353- error(
5454- "no menu program found, use --menu to provide a dmenu-compatible menu or install one of "
5555- + or_phrase([m.name() for m in menus if isinstance(m, Dmenu)])
5656- )
5757- return found
5858- dmenu = custom_dmenu(menu)
5959- if not dmenu.installed():
6060- error(f"{dmenu.name()} not found")
6161- return None
6262- return dmenu
5757+ found = next(filter(lambda m: m.installed(), menus), None)
5858+ if not found:
5959+ error(
6060+ "no menu program found, use --menu to provide a dmenu-compatible menu or install one of "
6161+ + or_phrase([m.name() for m in menus if isinstance(m, Dmenu)])
6262+ )
6363+ return found
636464656565-def custom_dmenu(command: str) -> Dmenu:
6666- split = shlex.split(command)
6666+def custom_dmenu(command: str | list[str]) -> Dmenu:
6767+ split = shlex.split(command) if isinstance(command, str) else command
6768 if len(split) == 1 or not split[1]:
6868- name = Path(command).name
6969+ command_path = Path(split[0])
7070+ name = command_path.name
6971 for menu in supported_menus():
7072 if isinstance(menu, Dmenu) and menu.name() == name:
7173 return (
7274 menu
7373- if name == command
7474- else replace(menu, menu_command=[command, *menu.menu_command[1::]])
7575+ if name == split[0]
7676+ else replace(
7777+ menu,
7878+ menu_command=[
7979+ str(command_path.expanduser()),
8080+ *menu.menu_command[1::],
8181+ ],
8282+ )
7583 )
7684 return Dmenu(split)
7785···8391 yield from [
8492 # default window is too narrow for a long prompt
8593 Dmenu(["fuzzel", "--dmenu"]),
8686- Dmenu(["walker", "--dmenu", "--placeholder", "{prompt} {qb_args}"]),
8787- Dmenu(["wofi", "--dmenu", "--prompt", "{prompt} {qb_args}"]),
9494+ Dmenu(["walker", "--dmenu", "--placeholder", "{prompt}"]),
9595+ Dmenu(["wofi", "--dmenu", "--prompt", "{prompt}"]),
8896 Dmenu(["tofi", "--prompt-text", "{prompt}> "]),
8997 Dmenu(["wmenu", "-p", "{prompt}"]),
9098 Dmenu(["dmenu-wl", "--prompt", "{prompt}"]),