···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+4040+# 1.0rc4
4141+ - `choose`: support `walker`, `tofi`, and `wmenu`
4242+ - better detection of invalid/nonexistent profiles
4343+4444+# 1.0rc3
4545+ - breaking: stop sourcing files from `~/.config/qutebrowser/conf.d/`
4646+ - this was undocumented, nonstandard, and didn't work as well as it could
4747+ - switch to `pyproject.toml`
4848+ - hopefully use the right qutebrowser dirs on Windows
4949+ - `choose`: add `qutebrowser` menu item for the main qutebrowser profile
5050+ - added `-C` argument to support referencing qutebrowser configs other than the one in ~/.config
5151+ - added `click`-generated completions for bash and zsh
5252+ - added option support to fish completions
5353+ - removed `--create` from `qbpm launch`
5454+ - make generated `.desktop` files match qutebrowser's more closely
5555+5656+# 1.0rc2:
5757+ - `choose`: support `fzf` and `fuzzel`
5858+ - use `click `for CLI parsing
5959+ - `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
+47-53
README.md
···11# qutebrowser profile manager
2233-[](https://builds.sr.ht/~pvsr/qbpm?)
33+[](https://builds.sr.ht/~pvsr/qbpm/commits/main?)
44+[](https://pypi.python.org/pypi/qbpm)
4555-qutebrowser profile manager (qbpm) is a tool for creating and managing
66-[qutebrowser](https://github.com/qutebrowser/qutebrowser) profiles. There isn't
77-any built in concept of profiles in qutebrowser, but there is a `--basedir` flag
66+qbpm (qutebrowser profile manager) is a tool for creating, managing, and running
77+[qutebrowser](https://github.com/qutebrowser/qutebrowser) profiles. Profile support
88+isn't built in to qutebrowser, at least not directly, but it does have a `--basedir` flag
89which allows qutebrowser to use any directory as the location of its config and
910data and effectively act as a profile. qbpm creates profiles that source your
1011main qutebrowser `config.py`, but have their own separate `autoconfig.yml`, bookmarks, cookies,
1111-history, and other data. It also acts as a wrapper around qutebrowser that sets
1212-up `--basedir` for you, so you can treat `qbpm launch` as an alias for
1313-`qutebrowser`, such as to open a url: `qbpm launch my-profile example.org`.
1212+history, and other data. Profiles can be run by starting qutebrowser with the
1313+appropriate `--basedir`, or more conveniently using the `qbpm launch` and `qbpm choose` commands.
14141515qutebrowser shares session depending on the basedir, so launching the same
1616profile twice will result in two windows sharing a session, which means running
···1919instances of qutebrowser which can be opened and closed independently.
20202121## Usage
2222-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:
2323```
2424$ qbpm new python
2525-$ qbpm edit python
2625$ qbpm launch python docs.python.org
2727-$ qbpm choose # run dmenu or another launcher to pick a profile
2826```
29273030-`qbpm from-session` can copy the tabs of a [saved qutebrowser
3131-session](https://qutebrowser.org/doc/help/commands.html#session-save) to a new
3232-profile. If you have a window full of tabs related to planning a vacation, you
3333-could save it to a session called "vacation" using `:session-save -o vacation`
3434-in qutebrowser, then create a new profile with those tabs:
3535-```
3636-$ qbpm from-session vacation
3737-```
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`.
3030+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.
38373939-The default profile directory is `$XDG_DATA_HOME/qutebrowser-profiles`, where
4040-`$XDG_DATA_HOME` is usually `$HOME/.local/share`, but you can create and launch
4141-profiles from anywhere using `--profile-dir`/`-P`:
4242-```
4343-$ qbpm --profile-dir ~/dev/my-project new qb-profile
4444-$ cd ~/dev/my-project
4545-$ qbpm -P . launch qb-profile
4646-# or
4747-$ qutebrowser --basedir qb-profile
4848-```
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.
49445045## Installation
5151- - Pip: `pip install git+https://github.com/pvsr/qbpm.git#egg=qbpm`
5252- - Arch: [qbpm-git](https://aur.archlinux.org/packages/qbpm-git) in the AUR
5353- - Nix: clone the repository and run `nix-env -if default.nix`
5454- - MacOS: For command-line only usage, the pip command above is sufficient, but
5555- if you would like to set qbpm as the default browser app, first clone this
5656- repository, then install platypus by running `brew install playtpus`, and
5757- finally install the app by running `platypus -P contrib/qbpm.platypus
5858- /Applications/qbpm.app` inside the cloned repository. You should then be
5959- able to select qbpm as your default browser under: System Preferences
6060- \> General > Default web browser. Note that there is currently [an
6161- issue](https://github.com/qutebrowser/qutebrowser/issues/3719) with
6262- qutebrowser itself that results in unnecessary `file:///*` tabs being
6363- opened.
6464- - If you're on linux, you can copy `contrib/qbpm.desktop` to `~/.local/share/applications`.
6565- That desktop entry will run `qbpm choose`, which shows an application
6666- launcher (dmenu or rofi) with your qutebrowser profiles as the options.
4646+If you use Nix, you can install or run qbpm as a [Nix flake](https://nixos.wiki/wiki/Flakes).
4747+For example, to run qbpm without installing it you can use `nix run github:pvsr/qbpm -- new my-profile`.
67486868-## Future ideas that may or may not happen
6969-- Release through github
7070-- More shared or copied config and data
7171-- Use any profile as a base for new profiles (currently only the main config in
7272- `$XDG_CONFIG_HOME` is supported)
7373-- Source `autoconfig.yml` instead of `config.py`
7474-- Bundled config file optimized for single-site browsing
7575-- `qbpm.conf` to configure the features above
7676-- Someday: qutebrowser plugin
4949+On Arch and derivatives, you can install the AUR package: [qbpm-git](https://aur.archlinux.org/packages/qbpm-git).
5050+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)
5555+and shell completions will not be installed automatically.
5656+5757+On Linux you can copy [`contrib/qbpm.desktop`](https://raw.githubusercontent.com/pvsr/qbpm/main/contrib/qbpm.desktop)
5858+to `~/.local/share/applications` to create a qbpm desktop application that runs
5959+`qbpm choose`.
6060+6161+### MacOS
6262+6363+Nix and uv will install qbpm as a command-line application, but if you want a
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`.
6868+6969+Note that there is currently [a qutebrowser bug](https://github.com/qutebrowser/qutebrowser/issues/3719)
7070+that results in unnecessary `file:///*` tabs being opened.
+28-12
completions/qbpm.fish
···11function __fish_qbpm
22- set -l saved_args $argv
33- set -l global_args
44- set -l cmd (commandline -opc)
55- set -e cmd[1]
66- argparse -si P/profile-dir= -- $cmd 2>/dev/null
77- set -q _flag_P
88- and set global_args "-P $_flag_P"
99- eval qbpm $global_args $saved_args
22+ set -l saved_args $argv
33+ set -l global_args
44+ set -l cmd (commandline -opc)
55+ set -e cmd[1]
66+ argparse -si P/profile-dir= -- $cmd 2>/dev/null
77+ set -q _flag_P
88+ and set global_args "-P $_flag_P"
99+ eval qbpm $global_args $saved_args
1010end
11111212-set -l commands new from-session desktop launch run list edit
1212+set -l commands new from-session desktop launch list edit choose
1313+set -l data_home (set -q XDG_DATA_HOME; and echo $XDG_DATA_HOME; or echo ~/.local/share)
13141415complete -c qbpm -f
1515-complete -c qbpm -n "not __fish_seen_subcommand_from $commands" -a "launch new from-session edit list"
1616-complete -c qbpm -n "__fish_seen_subcommand_from launch edit" -a "(__fish_qbpm list)"
1717-set -l data_home (set -q XDG_DATA_HOME; and echo $XDG_DATA_HOME; or echo ~/.local/share)
1616+complete -c qbpm -s h -l help
1717+complete -c qbpm -s l -l log-level -a "debug info error"
1818+complete -c qbpm -s C -l config-dir -r
1919+complete -c qbpm -s P -l profile-dir -r
2020+2121+complete -c qbpm -n "not __fish_seen_subcommand_from $commands" -a "$commands"
2222+2323+complete -c qbpm -n "__fish_seen_subcommand_from new from_session" -s l -l launch
2424+complete -c qbpm -n "__fish_seen_subcommand_from new from_session" -l desktop-file
2525+complete -c qbpm -n "__fish_seen_subcommand_from new from_session" -l no-desktop-file
2626+complete -c qbpm -n "__fish_seen_subcommand_from new from_session" -l overwrite
2727+complete -c qbpm -n "__fish_seen_subcommand_from new from_session launch choose" -s f -l foreground
2828+2929+complete -c qbpm -n "__fish_seen_subcommand_from launch" -s c -l create
3030+complete -c qbpm -n "__fish_seen_subcommand_from choose" -s m -l menu -r
3131+complete -c qbpm -n "__fish_seen_subcommand_from launch choose" -w qutebrowser
3232+3333+complete -c qbpm -n "__fish_seen_subcommand_from launch edit desktop" -a "(__fish_qbpm list)"
1834complete -c qbpm -n "__fish_seen_subcommand_from from-session" -a "(ls $data_home/qutebrowser/sessions | xargs basename -a -s .yml)"
···11-try:
22- from qbpm.version import version as __version__
33-except ImportError:
44- __version__ = "unknown"
-204
qbpm/main.py
···11-import argparse
22-from os import environ
33-from pathlib import Path
44-from typing import Any, Callable, Optional
55-66-from xdg import BaseDirectory # type: ignore
77-88-from qbpm import __version__, operations, profiles
99-from qbpm.profiles import Profile
1010-from qbpm.utils import SUPPORTED_MENUS, error
1111-1212-DEFAULT_PROFILE_DIR = Path(BaseDirectory.xdg_data_home) / "qutebrowser-profiles"
1313-1414-1515-def main(mock_args=None) -> None:
1616- parser = argparse.ArgumentParser(description="qutebrowser profile manager")
1717- parser.set_defaults(operation=lambda args: parser.print_help(), passthrough=False)
1818- parser.add_argument(
1919- "-P",
2020- "--profile-dir",
2121- metavar="directory",
2222- type=Path,
2323- help="directory in which profiles are stored",
2424- )
2525- parser.add_argument(
2626- "--version",
2727- action="version",
2828- version=__version__,
2929- )
3030-3131- subparsers = parser.add_subparsers()
3232- new = subparsers.add_parser("new", help="create a new profile")
3333- new.add_argument("profile_name", metavar="profile", help="name of the new profile")
3434- new.add_argument("home_page", metavar="url", nargs="?", help="profile's home page")
3535- new.set_defaults(
3636- operation=lambda args: profiles.new_profile(
3737- build_profile(args),
3838- args.home_page,
3939- args.desktop_file,
4040- args.overwrite,
4141- )
4242- )
4343- creator_args(new)
4444-4545- session = subparsers.add_parser(
4646- "from-session", help="create a new profile from a qutebrowser session"
4747- )
4848- session.add_argument(
4949- "session",
5050- help="path to session file or name of session. "
5151- "e.g. ~/.local/share/qutebrowser/sessions/example.yml or example",
5252- )
5353- session.add_argument(
5454- "profile_name",
5555- metavar="profile",
5656- nargs="?",
5757- help="name of the new profile. if unset the session name will be used",
5858- )
5959- session.set_defaults(
6060- operation=lambda args: operations.from_session(
6161- args.session,
6262- args.profile_name,
6363- args.profile_dir,
6464- args.desktop_file,
6565- args.overwrite,
6666- )
6767- )
6868- creator_args(session)
6969-7070- desktop = subparsers.add_parser(
7171- "desktop", help="create a desktop file for an existing profile"
7272- )
7373- desktop.add_argument(
7474- "profile_name", metavar="profile", help="profile to create a desktop file for"
7575- )
7676- desktop.set_defaults(operation=lambda args: operations.desktop(build_profile(args)))
7777-7878- launch = subparsers.add_parser(
7979- "launch", aliases=["run"], help="launch qutebrowser with the given profile"
8080- )
8181- launch.add_argument(
8282- "profile_name",
8383- metavar="profile",
8484- help="profile to launch. it will be created if it does not exist, unless -s is set",
8585- )
8686- launch.add_argument(
8787- "-n",
8888- "--new",
8989- action="store_false",
9090- dest="strict",
9191- help="create the profile if it doesn't exist",
9292- )
9393- launch.add_argument(
9494- "-f",
9595- "--foreground",
9696- action="store_true",
9797- help="launch qutebrowser in the foreground and print its stdout and stderr to the console",
9898- )
9999- launch.set_defaults(
100100- operation=lambda args: operations.launch(
101101- build_profile(args), args.strict, args.foreground, args.qb_args
102102- ),
103103- passthrough=True,
104104- )
105105-106106- list_ = subparsers.add_parser("list", help="list existing profiles")
107107- list_.set_defaults(operation=operations.list_)
108108-109109- choose = subparsers.add_parser(
110110- "choose",
111111- help="choose profile using a dmenu-compatible launcher or an applescript dialog",
112112- )
113113- menus = sorted(SUPPORTED_MENUS)
114114- choose.add_argument(
115115- "-m",
116116- "--menu",
117117- help=f'menu application to use. this may be any dmenu-compatible command (e.g. "dmenu -i -p qbpm" or "/path/to/rofi -d") or one of the following menus with built-in support: {menus}',
118118- )
119119- choose.add_argument(
120120- "-f",
121121- "--foreground",
122122- action="store_true",
123123- help="launch qutebrowser in the foreground and print its stdout and stderr to the console",
124124- )
125125- choose.set_defaults(operation=operations.choose, passthrough=True)
126126-127127- edit = subparsers.add_parser(
128128- "edit", help="edit a profile's config.py using $EDITOR"
129129- )
130130- edit.add_argument("profile_name", metavar="profile", help="profile to edit")
131131- edit.set_defaults(operation=lambda args: operations.edit(build_profile(args)))
132132-133133- raw_args = parser.parse_known_args(mock_args)
134134- args = raw_args[0]
135135- if args.passthrough:
136136- args.qb_args = raw_args[1]
137137- elif len(raw_args[1]) > 0:
138138- error(f"unrecognized arguments: {' '.join(raw_args[1])}")
139139- exit(1)
140140-141141- if not args.profile_dir:
142142- args.profile_dir = Path(environ.get("QBPM_PROFILE_DIR") or DEFAULT_PROFILE_DIR)
143143- if not args.operation(args):
144144- exit(1)
145145-146146-147147-def creator_args(parser: argparse.ArgumentParser) -> None:
148148- parser.add_argument(
149149- "-l",
150150- "--launch",
151151- action=ThenLaunchAction,
152152- dest="operation",
153153- help="launch the profile after creating",
154154- )
155155- parser.add_argument(
156156- "-f",
157157- "--foreground",
158158- action="store_true",
159159- help="if --launch is set, launch qutebrowser in the foreground",
160160- )
161161- parser.add_argument(
162162- "--no-desktop-file",
163163- dest="desktop_file",
164164- action="store_false",
165165- help="do not generate a desktop file for the profile",
166166- )
167167- parser.add_argument(
168168- "--overwrite",
169169- action="store_true",
170170- help="replace existing profile config",
171171- )
172172- parser.set_defaults(strict=True)
173173-174174-175175-class ThenLaunchAction(argparse.Action):
176176- def __init__(self, option_strings, dest, nargs=0, **kwargs):
177177- super(ThenLaunchAction, self).__init__(
178178- option_strings, dest, nargs=nargs, **kwargs
179179- )
180180-181181- def __call__(self, parser, namespace, values, option_string=None):
182182- if operation := getattr(namespace, self.dest):
183183- setattr(namespace, self.dest, lambda args: then_launch(args, operation))
184184-185185-186186-def then_launch(
187187- args: argparse.Namespace,
188188- operation: Callable[[argparse.Namespace], Optional[Any]],
189189-) -> bool:
190190- if result := operation(args):
191191- if isinstance(result, Profile):
192192- profile = result
193193- else:
194194- profile = build_profile(args)
195195- return operations.launch(profile, False, args.foreground, args.qb_args)
196196- return False
197197-198198-199199-def build_profile(args: argparse.Namespace) -> Profile:
200200- return Profile(args.profile_name, args.profile_dir)
201201-202202-203203-if __name__ == "__main__":
204204- main()
-158
qbpm/operations.py
···11-import argparse
22-import os
33-import shutil
44-import subprocess
55-from pathlib import Path
66-from sys import platform, stderr
77-from typing import List, Optional
88-99-from xdg import BaseDirectory # type: ignore
1010-from xdg.DesktopEntry import DesktopEntry # type: ignore
1111-1212-from qbpm import profiles
1313-from qbpm.profiles import Profile
1414-from qbpm.utils import SUPPORTED_MENUS, error, get_default_menu, user_data_dir
1515-1616-1717-def from_session(
1818- session: str,
1919- profile_name: Optional[str] = None,
2020- profile_dir: Optional[Path] = None,
2121- desktop_file: bool = True,
2222- overwrite: bool = False,
2323-) -> Optional[Profile]:
2424- if session.endswith(".yml"):
2525- session_file = Path(session).expanduser()
2626- session_name = session_file.stem
2727- else:
2828- session_name = session
2929- session_file = user_data_dir() / "sessions" / (session_name + ".yml")
3030- if not session_file.is_file():
3131- error(f"{session_file} is not a file")
3232- return None
3333-3434- profile = Profile(profile_name or session_name, profile_dir)
3535- if not profiles.new_profile(profile, None, desktop_file, overwrite):
3636- return None
3737-3838- session_dir = profile.root / "data" / "sessions"
3939- session_dir.mkdir(parents=True, exist_ok=overwrite)
4040- shutil.copy(session_file, session_dir / "_autosave.yml")
4141-4242- return profile
4343-4444-4545-def launch(
4646- profile: Profile, strict: bool, foreground: bool, qb_args: List[str]
4747-) -> bool:
4848- if not profiles.ensure_profile_exists(profile, not strict):
4949- return False
5050-5151- args = profile.cmdline() + qb_args
5252- if not shutil.which(args[0]):
5353- error("qutebrowser is not installed")
5454- return False
5555-5656- if foreground:
5757- return subprocess.run(args).returncode == 0
5858- else:
5959- p = subprocess.Popen(args, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE)
6060- try:
6161- # give qb a chance to validate input before returning to shell
6262- stdout, stderr = p.communicate(timeout=0.1)
6363- print(stderr.decode(errors="ignore"), end="")
6464- except subprocess.TimeoutExpired:
6565- pass
6666-6767- return True
6868-6969-7070-application_dir = Path(BaseDirectory.xdg_data_home) / "applications" / "qbpm"
7171-7272-7373-def desktop(profile: Profile) -> bool:
7474- exists = profile.exists()
7575- if exists:
7676- profiles.create_desktop_file(profile)
7777- else:
7878- error(f"profile {profile.name} not found at {profile.root}")
7979- return exists
8080-8181-8282-def list_(args: argparse.Namespace) -> bool:
8383- for profile in sorted(args.profile_dir.iterdir()):
8484- print(profile.name)
8585- return True
8686-8787-8888-def choose(args: argparse.Namespace) -> bool:
8989- menu = args.menu or get_default_menu()
9090- if not menu:
9191- error(f"No menu program found, please install one of: {SUPPORTED_MENUS}")
9292- return False
9393- if menu == "applescript" and platform != "darwin":
9494- error(f"Menu applescript cannot be used on a {platform} host")
9595- return False
9696- profiles = [profile.name for profile in sorted(args.profile_dir.iterdir())]
9797- if len(profiles) == 0:
9898- error("No profiles")
9999- return False
100100-101101- command = menu_command(menu, profiles, args)
102102- program = command.split(" ")[0]
103103- if not shutil.which(program):
104104- error(f"'{program}' not found on path")
105105- return False
106106-107107- selection_cmd = subprocess.Popen(
108108- command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE
109109- )
110110- out = selection_cmd.stdout
111111- if not out:
112112- error(f"Could not read stdout from {command}")
113113- return False
114114- selection = out.read().decode(errors="ignore").rstrip("\n")
115115-116116- if selection:
117117- profile = Profile(selection, args.profile_dir)
118118- launch(profile, True, args.foreground, args.qb_args)
119119- else:
120120- error("No profile selected")
121121- if err := selection_cmd.stderr:
122122- msg = err.read().decode(errors="ignore").rstrip("\n")
123123- if msg:
124124- for line in msg.split("\n"):
125125- print(f"stderr: {line}", file=stderr)
126126- return False
127127- return True
128128-129129-130130-def menu_command(menu: str, profiles, args: argparse.Namespace) -> str:
131131- arg_string = " ".join(args.qb_args)
132132- if menu == "applescript":
133133- profile_list = '", "'.join(profiles)
134134- return f"""osascript -e \'set profiles to {{"{profile_list}"}}
135135-set profile to choose from list profiles with prompt "qutebrowser: {arg_string}" default items {{item 1 of profiles}}
136136-item 1 of profile\'"""
137137-138138- prompt = "-p qutebrowser"
139139- command = menu
140140- if len(menu.split(" ")) == 1:
141141- program = Path(menu).name
142142- if program == "rofi":
143143- command = f"{menu} -dmenu -no-custom {prompt} -mesg {arg_string}"
144144- elif program == "wofi":
145145- command = f"{menu} --dmenu {prompt}"
146146- elif program in ["dmenu", "dmenu-wl"]:
147147- command = f"{menu} {prompt}"
148148- profile_list = "\n".join(profiles)
149149- return f'echo "{profile_list}" | {command}'
150150-151151-152152-def edit(profile: Profile) -> bool:
153153- if not profile.exists():
154154- error(f"profile {profile.name} not found at {profile.root}")
155155- return False
156156- editor = os.environ.get("VISUAL") or os.environ.get("EDITOR") or "vim"
157157- os.execlp(editor, editor, str(profile.root / "config" / "config.py"))
158158- return True
-123
qbpm/profiles.py
···11-from functools import partial
22-from pathlib import Path
33-from sys import platform
44-from typing import List, Optional
55-66-from xdg import BaseDirectory # type: ignore
77-from xdg.DesktopEntry import DesktopEntry # type: ignore
88-99-from qbpm.utils import error, user_config_dir
1010-1111-1212-class Profile:
1313- name: str
1414- profile_dir: Path
1515- root: Path
1616-1717- def __init__(self, name: str, profile_dir: Optional[Path]) -> None:
1818- self.name = name
1919- self.profile_dir = profile_dir or Path(
2020- BaseDirectory.save_data_path("qutebrowser-profiles")
2121- )
2222- self.root = self.profile_dir / name
2323-2424- def check(self) -> Optional["Profile"]:
2525- if "/" in self.name:
2626- error("profile name cannot contain slashes")
2727- return None
2828- if not self.profile_dir.resolve().is_dir():
2929- error(f"{self.profile_dir} is not a directory")
3030- return None
3131- return self
3232-3333- def exists(self) -> bool:
3434- return self.root.exists() and self.root.is_dir()
3535-3636- def cmdline(self) -> List[str]:
3737- macos_app = "/Applications/qutebrowser.app/Contents/MacOS/qutebrowser"
3838- if platform == "darwin" and Path(macos_app).exists():
3939- qb = macos_app
4040- else:
4141- qb = "qutebrowser"
4242- return [
4343- qb,
4444- "-B",
4545- str(self.root),
4646- "--qt-arg",
4747- "name",
4848- self.name,
4949- "--desktop-file-name",
5050- self.name,
5151- ]
5252-5353-5454-def create_profile(profile: Profile, overwrite: bool = False) -> bool:
5555- if not profile.check():
5656- return False
5757-5858- if not overwrite and profile.root.exists():
5959- error(f"{profile.root} already exists")
6060- return False
6161-6262- config_dir = profile.root / "config"
6363- config_dir.mkdir(parents=True, exist_ok=overwrite)
6464- print(profile.root)
6565- return True
6666-6767-6868-def create_config(
6969- profile: Profile, home_page: Optional[str] = None, overwrite: bool = False
7070-) -> None:
7171- user_config = profile.root / "config" / "config.py"
7272- with user_config.open(mode="w" if overwrite else "x") as dest_config:
7373- out = partial(print, file=dest_config)
7474- out("config.load_autoconfig()")
7575- title_prefix = "{perc}{current_title}{title_sep}"
7676- out(f"c.window.title_format = '{title_prefix} qutebrowser ({profile.name})'")
7777- if home_page:
7878- out(f"c.url.start_pages = ['{home_page}']")
7979- main_config_dir = user_config_dir()
8080- out(f"config.source('{main_config_dir / 'config.py'}')")
8181- for conf in main_config_dir.glob("conf.d/*.py"):
8282- out(f"config.source('{conf}')")
8383-8484-8585-application_dir = Path(BaseDirectory.xdg_data_home) / "applications" / "qbpm"
8686-8787-8888-def create_desktop_file(profile: Profile):
8989- desktop = DesktopEntry(str(application_dir / f"{profile.name}.desktop"))
9090- desktop.set("Name", f"{profile.name} (qutebrowser profile)")
9191- # TODO allow passing in an icon value
9292- desktop.set("Icon", "qutebrowser")
9393- desktop.set("Exec", " ".join(profile.cmdline()) + " %u")
9494- desktop.set("Categories", ["Network"])
9595- desktop.set("Terminal", False)
9696- desktop.set("StartupNotify", True)
9797- desktop.write()
9898-9999-100100-def ensure_profile_exists(profile: Profile, create: bool = True) -> bool:
101101- if profile.root.exists() and not profile.root.is_dir():
102102- error(f"{profile.root} is not a directory")
103103- return False
104104- if not profile.root.exists() and create:
105105- return new_profile(profile)
106106- if not profile.root.exists():
107107- error(f"{profile.root} does not exist")
108108- return False
109109- return True
110110-111111-112112-def new_profile(
113113- profile: Profile,
114114- home_page: Optional[str] = None,
115115- desktop_file: bool = True,
116116- overwrite: bool = False,
117117-) -> bool:
118118- if create_profile(profile, overwrite):
119119- create_config(profile, home_page, overwrite)
120120- if desktop_file:
121121- create_desktop_file(profile)
122122- return True
123123- return False
-45
qbpm/utils.py
···11-import platform
22-import subprocess
33-import sys
44-from pathlib import Path
55-from shutil import which
66-from sys import exit, stderr
77-from typing import Optional
88-99-from xdg import BaseDirectory # type: ignore
1010-1111-SUPPORTED_MENUS = ["wofi", "rofi", "dmenu", "dmenu-wl", "applescript"]
1212-1313-1414-def error(msg: str) -> None:
1515- print(f"error: {msg}", file=stderr)
1616-1717-1818-def user_data_dir() -> Path:
1919- if platform.system() == "Linux":
2020- return Path(BaseDirectory.xdg_data_home) / "qutebrowser"
2121- if platform.system() == "Darwin":
2222- return Path.home() / "Library" / "Application Support" / "qutebrowser"
2323- error("This operation is only implemented for linux and macOS.")
2424- print(
2525- "If you're interested in adding support for another OS, send a PR "
2626- "to github.com/pvsr/qbpm adding the location of qutebrowser data such "
2727- "as history.sqlite on your OS to user_data_dir() in qbpm/utils.py.",
2828- file=stderr,
2929- )
3030- exit(1)
3131-3232-3333-def user_config_dir() -> Path:
3434- return Path(BaseDirectory.xdg_config_home) / "qutebrowser"
3535-3636-3737-def get_default_menu() -> Optional[str]:
3838- if sys.platform == "darwin":
3939- return "applescript"
4040- for menu_cmd in SUPPORTED_MENUS:
4141- if menu_cmd == "applescript":
4242- continue
4343- if which(menu_cmd) is not None:
4444- return menu_cmd
4545- return None
+44-27
qbpm.1.scd
···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
12121313-qbpm is a tool for creating and running qutebrowser profiles. qutebrowser
1414-doesn't have a native concept of profiles, but it does have a --basedir flag
1515-that allows qutebrowser's config, cache, and data to be stored in any directory.
1616-So a profile is simply a directory meant to be set as qutebrowser's basedir.
1717-qbpm creates profiles that source the config.py from your qutebrowser config dir
1818-in $XDG_CONFIG_HOME (typically $HOME/.config/qutebrowser). By default profiles
1919-are stored in a qutebrowser-profiles/ dir in $XDG_DATA_HOME (typically
2020-$HOME/.local/share), but this can be set to another directory by passing
2121-\--profile-dir to qbpm or setting the $QBPM_PROFILE_DIR environment variable.
1313+qbpm is a tool for creating, managing, and running qutebrowser profiles. Profile support
1414+isn't built in to qutebrowser, at least not directly, but it does have a \--basedir flag
1515+which allows qutebrowser to use any directory as the location of its config and
1616+data and effectively act as a profile. qbpm creates profiles that source your
1717+main qutebrowser config.py, but have their own separate autoconfig.yml, bookmarks, cookies,
1818+history, and other data. Profiles can be run by starting qutebrowser with the
1919+appropriate \--basedir, or more conveniently using the qbpm launch and qbpm choose commands.
22202321# OPTIONS
2422···3230 Use _path_ as the profile directory instead of the default location. Takes
3331 precedence over the QBPM_PROFILE_DIR environment variable.
34323333+*-c, --config-file* <path>
3434+ Read configuration for qbpm from _path_. Defaults to ~/.config/qbpm/config.toml.
3535+3536# COMMANDS
36373738*new* [options] <profile> [<url>]
3839 Create a new qutebrowser profile named _profile_. If _url_ is present it will
3939- be used as the profile's home page. By default, a .desktop file will be
4040- created for the profile in $XDG_DATA_HOME/applications/qbpm/.
4040+ be used as the profile's home page.
41414242 Options:
4343···4747 *-f, --foreground*
4848 If --launch is set, run qutebrowser in the foreground.
49495050- *--no-desktop-file*
5151- Do not generate a .desktop file for the profile.
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
5757+ for information on desktop entries.
52585359 *--overwrite*
5460 By default qbpm will refuse to create a profile if one with the same name
5555- already exists. --overwrite disables this check and rewrites the existing
5656- profile's configuration files from scratch. Profile data is left untouched.
6161+ already exists. --overwrite disables this check and replaces the existing
6262+ profile's configuration files. Profile data is left untouched.
57635858-*launch* [options] <profile> [argument...]
6464+*launch* [options] <profile> [arguments...]
5965 Start qutebrowser with --basedir set to the location of _profile_. All
6066 arguments following _profile_ will be passed on to qutebrowser.
6167···6470 *-f, --foreground*
6571 Run qutebrowser in the foreground instead of forking off a new process.
66726767- *-n, --new*
7373+ *-c, --create*
6874 Create the profile if it does not exist.
69757076 Examples:
···7379 \# launch my profile called work and open internal.mycompany.com
7480 qbpm launch work internal.mycompany.com
75817676- \# launch a new profile called qb-dev, passing the flags to qutebrowser
8282+ \# launch a new profile called qb-dev, passing the debugging flags to qutebrowser
7783 qbpm launch -n qb-dev --debug --json-logging
7884 ```
79858080-*choose* [options]
8686+*choose* [options] [arguments...]
8187 Open a menu to choose a qutebrowser profile to launch. On linux this defaults
8288 to dmenu or another compatible menu program such as rofi, and on macOS this
8383- will be an applescript dialog.
8989+ will be an applescript dialog. All arguments are passed to qutebrowser.
84908591 *-m, --menu* <menu>
8692 Use _menu_ instead of the default menu program. This may be the name of a
8793 program on $PATH or a path to a program, in which case it will be run in
8888- dmenu mode if qbpm knows about the program, or a full command line.
9494+ dmenu mode if qbpm knows about the program, or a full command line. On
9595+ MacOS the special value "applescript" is accepted. Run `qbpm choose --help`
9696+ for a list of known menu programs for your environment.
89979098 Examples:
919992100 ```
9393- \# runs "echo {profiles} | /path/to/rofi -dmenu -no-custom -p qutebrowser"
9494- qbpm choose -m /path/to/rofi
101101+ qbpm choose --menu fzf
102102+103103+ qbpm choose --menu "./build/my-cool-menu --dmenu-mode --prompt qutebrowser"
104104+105105+ \# qbpm knows about fuzzel so it can automatically invoke it as "~/.local/bin/fuzzel --dmenu"
106106+ qbpm choose --menu ~/.local/bin/fuzzel
951079696- qbpm choose -m "my-cool-menu --dmenu-mode --prompt qutebrowser"
108108+ \# if more than one word is provided it will be invoked as is, so `--dmenu` must be included
109109+ qbpm choose --menu 'fuzzel --dmenu --width 100'
97110 ```
9811199112*from-session* [options] <session> [<name>]
···104117 *new*.
105118106119*desktop* <profile>
107107- Generate a .desktop file for _profile_.
120120+ Generate an XDG desktop entry for _profile_.
108121109122*edit* <profile>
110123 Open _profile_'s config.py in your default editor.
···116129117130Peter Rice
118131119119-Contribute at https://github.com/pvsr/qbpm
132132+# CONTRIBUTE
133133+134134+_https://github.com/pvsr/qbpm_
135135+136136+_https://codeberg.org/pvsr/qbpm_
···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