qutebrowser profile manager

mostly functional click setup

+111 -177
+1 -1
default.nix
··· 11 doCheck = true; 12 SETUPTOOLS_SCM_PRETEND_VERSION = version; 13 nativeBuildInputs = [pkgs.scdoc setuptools-scm]; 14 - propagatedBuildInputs = [pyxdg]; 15 checkInputs = [pytest]; 16 postInstall = '' 17 mkdir -p $out/share/fish/vendor_completions.d
··· 11 doCheck = true; 12 SETUPTOOLS_SCM_PRETEND_VERSION = version; 13 nativeBuildInputs = [pkgs.scdoc setuptools-scm]; 14 + propagatedBuildInputs = [pyxdg click]; 15 checkInputs = [pytest]; 16 postInstall = '' 17 mkdir -p $out/share/fish/vendor_completions.d
+1
flake.nix
··· 20 (pkgs.python3.withPackages (ps: 21 with ps; [ 22 pyxdg 23 setuptools-scm 24 pytest 25 pylint
··· 20 (pkgs.python3.withPackages (ps: 21 with ps; [ 22 pyxdg 23 + click 24 setuptools-scm 25 pytest 26 pylint
+95 -148
qbpm/main.py
··· 1 - import argparse 2 import inspect 3 from os import environ 4 from pathlib import Path 5 from typing import Any, Callable, Optional 6 7 from xdg import BaseDirectory 8 9 from . import __version__, operations, profiles 10 from .profiles import Profile 11 - from .utils import SUPPORTED_MENUS, error 12 13 - DEFAULT_PROFILE_DIR = Path(BaseDirectory.xdg_data_home) / "qutebrowser-profiles" 14 15 16 - def main(mock_args: Optional[list[str]] = None) -> None: 17 - parser = argparse.ArgumentParser(description="qutebrowser profile manager") 18 - parser.set_defaults( 19 - operation=lambda args: parser.print_help(), passthrough=False, launch=False 20 - ) 21 - parser.add_argument( 22 - "-P", 23 - "--profile-dir", 24 - metavar="directory", 25 - type=Path, 26 - help="directory in which profiles are stored", 27 - ) 28 - parser.add_argument( 29 - "--version", 30 - action="version", 31 - version=__version__, 32 - ) 33 34 - subparsers = parser.add_subparsers() 35 - new = subparsers.add_parser("new", help="create a new profile") 36 - new.add_argument("profile_name", metavar="profile", help="name of the new profile") 37 - new.add_argument("home_page", metavar="url", nargs="?", help="profile's home page") 38 - new.set_defaults(operation=build_op(profiles.new_profile)) 39 - creator_args(new) 40 41 - session = subparsers.add_parser( 42 - "from-session", help="create a new profile from a qutebrowser session" 43 - ) 44 - session.add_argument( 45 - "session", 46 - help="path to session file or name of session. " 47 - "e.g. ~/.local/share/qutebrowser/sessions/example.yml or example", 48 - ) 49 - session.add_argument( 50 - "profile_name", 51 - metavar="profile", 52 - nargs="?", 53 - help="name of the new profile. if unset the session name will be used", 54 - ) 55 - session.set_defaults(operation=build_op(operations.from_session)) 56 - creator_args(session) 57 58 - desktop = subparsers.add_parser( 59 - "desktop", help="create a desktop file for an existing profile" 60 - ) 61 - desktop.add_argument( 62 - "profile_name", metavar="profile", help="profile to create a desktop file for" 63 - ) 64 - desktop.set_defaults(operation=build_op(operations.desktop)) 65 66 - launch = subparsers.add_parser( 67 - "launch", help="launch qutebrowser with the given profile" 68 - ) 69 - launch.add_argument( 70 - "profile_name", 71 - metavar="profile", 72 - help="profile to launch. it will be created if it does not exist, unless -s is set", 73 - ) 74 - launch.add_argument( 75 - "-n", 76 - "--new", 77 - action="store_false", 78 - dest="strict", 79 - help="create the profile if it doesn't exist", 80 - ) 81 - launch.add_argument( 82 - "-f", 83 - "--foreground", 84 - action="store_true", 85 - help="launch qutebrowser in the foreground and print its stdout and stderr to the console", 86 - ) 87 - launch.set_defaults(operation=build_op(operations.launch), passthrough=True) 88 89 - list_ = subparsers.add_parser("list", help="list existing profiles") 90 - list_.set_defaults(operation=operations.list_) 91 92 - choose = subparsers.add_parser( 93 - "choose", 94 - help="interactively choose a profile to launch", 95 - ) 96 - menus = sorted(SUPPORTED_MENUS) 97 - choose.add_argument( 98 - "-m", 99 - "--menu", 100 - 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}', 101 - ) 102 - choose.add_argument( 103 - "-f", 104 - "--foreground", 105 - action="store_true", 106 - help="launch qutebrowser in the foreground and print its stdout and stderr to the console", 107 - ) 108 - choose.set_defaults(operation=operations.choose, passthrough=True) 109 110 - edit = subparsers.add_parser("edit", help="edit a profile's config.py") 111 - edit.add_argument("profile_name", metavar="profile", help="profile to edit") 112 - edit.set_defaults(operation=build_op(operations.edit)) 113 114 - raw_args = parser.parse_known_args(mock_args) 115 - args = raw_args[0] 116 - if args.passthrough: 117 - args.qb_args = raw_args[1] 118 - elif len(raw_args[1]) > 0: 119 - error(f"unrecognized arguments: {' '.join(raw_args[1])}") 120 - exit(1) 121 122 - if not args.profile_dir: 123 - args.profile_dir = Path(environ.get("QBPM_PROFILE_DIR") or DEFAULT_PROFILE_DIR) 124 125 - result = args.operation(args) 126 - if args.launch and result: 127 - profile = result if isinstance(result, Profile) else Profile.of(args) 128 - result = operations.launch( 129 - profile, False, args.foreground, getattr(args, "qb_args", []) 130 - ) 131 - if not result: 132 - exit(1) 133 134 135 - def creator_args(parser: argparse.ArgumentParser) -> None: 136 - parser.add_argument( 137 - "-l", 138 - "--launch", 139 - action="store_true", 140 - help="launch the profile after creating", 141 - ) 142 - parser.add_argument( 143 - "-f", 144 - "--foreground", 145 - action="store_true", 146 - help="if --launch is set, launch qutebrowser in the foreground", 147 - ) 148 - parser.add_argument( 149 - "--no-desktop-file", 150 - dest="desktop_file", 151 - action="store_false", 152 - help="do not generate a desktop file for the profile", 153 - ) 154 - parser.add_argument( 155 - "--overwrite", 156 - action="store_true", 157 - help="replace existing profile config", 158 - ) 159 - parser.set_defaults(strict=True) 160 161 162 - def build_op(operation: Callable[..., Any]) -> Callable[[argparse.Namespace], Any]: 163 - def op(args: argparse.Namespace) -> Any: 164 - params = [ 165 - param.name 166 - for param in inspect.signature(operation).parameters.values() 167 - if param.kind == param.POSITIONAL_OR_KEYWORD 168 - ] 169 - kwargs = {param: getattr(args, param, None) for param in params} 170 - if "profile" in params: 171 - kwargs["profile"] = Profile.of(args) 172 - return operation(**kwargs) 173 174 - return op 175 176 177 if __name__ == "__main__": 178 - main()
··· 1 import inspect 2 from os import environ 3 from pathlib import Path 4 from typing import Any, Callable, Optional 5 6 + import click 7 from xdg import BaseDirectory 8 9 from . import __version__, operations, profiles 10 from .profiles import Profile 11 + from .utils import SUPPORTED_MENUS, default_profile_dir, error 12 13 14 + @click.group() 15 + @click.option( 16 + "-P", 17 + "--profile-dir", 18 + type=click.Path(file_okay=False, writable=True, path_type=Path), 19 + envvar="QBPM_PROFILE_DIR", 20 + default=default_profile_dir, 21 + ) 22 + @click.pass_context 23 + def main(ctx, profile_dir: Path) -> None: 24 + # TODO version, documentation 25 + # TODO -h as --help 26 + ctx.ensure_object(dict) 27 + ctx.obj["PROFILE_DIR"] = profile_dir 28 29 30 + @main.command() 31 + @click.argument("profile_name") 32 + @click.argument("home_page", required=False) 33 + @click.option("--desktop-file/--no-desktop-file", default=True, is_flag=True) 34 + @click.option("--overwrite", is_flag=True) 35 + @click.option("-l", "--launch", is_flag=True) 36 + @click.option("-f", "--foreground", is_flag=True) 37 + @click.pass_context 38 + def new(ctx, profile_name: str, launch: bool, foreground: bool, **kwargs): 39 + profile = Profile(profile_name, ctx.obj["PROFILE_DIR"]) 40 + result = profiles.new_profile(profile, **kwargs) 41 + if result and launch: 42 + # TODO args? 43 + then_launch(profile, foreground, []) 44 45 46 + @main.command() 47 + @click.argument("session") 48 + @click.argument("profile_name", required=False) 49 + @click.option("--desktop-file/--no-desktop-file", default=True, is_flag=True) 50 + @click.option("--overwrite", is_flag=True) 51 + @click.option("-l", "--launch", is_flag=True) 52 + @click.option("-f", "--foreground", is_flag=True) 53 + @click.pass_context 54 + def from_session( 55 + ctx, 56 + launch: bool, 57 + foreground: bool, 58 + **kwargs, 59 + ): 60 + profile = operations.from_session(profile_dir=ctx.obj["PROFILE_DIR"], **kwargs) 61 + if profile and launch: 62 + # TODO args? 63 + then_launch(profile, foreground, []) 64 65 66 + @main.command() 67 + @click.argument("profile_name") 68 + @click.pass_context 69 + def desktop( 70 + ctx, 71 + profile_name: str, 72 + ): 73 + profile = Profile(profile_name, ctx.obj["PROFILE_DIR"]) 74 + return operations.desktop(profile) 75 76 77 + @main.command() 78 + @click.argument("profile_name") 79 + @click.option("-c", "--create", is_flag=True) 80 + @click.option("-f", "--foreground", is_flag=True) 81 + @click.pass_context 82 + def launch(ctx, profile_name: str, **kwargs): 83 + profile = Profile(profile_name, ctx.obj["PROFILE_DIR"]) 84 + # TODO qb args 85 + return operations.launch(profile, **kwargs) 86 87 88 + @main.command() 89 + @click.option("-m", "--menu") 90 + @click.option("-f", "--foreground", is_flag=True) 91 + @click.pass_context 92 + def choose(ctx, **kwargs): 93 + # TODO qb args 94 + return operations.choose(profile_dir=ctx.obj["PROFILE_DIR"], qb_args=[], **kwargs) 95 96 97 + @main.command() 98 + @click.argument("profile_name") 99 + @click.pass_context 100 + def edit(ctx, profile_name): 101 + breakpoint() 102 + profile = Profile(profile_name, ctx.obj["PROFILE_DIR"]) 103 + if not profile.exists(): 104 + error(f"profile {profile.name} not found at {profile.root}") 105 + return False 106 + click.edit(filename=profile.root / "config" / "config.py") 107 108 109 + @main.command(name="list") 110 + @click.pass_context 111 + def list_(ctx): 112 + for profile in sorted(ctx.obj["PROFILE_DIR"].iterdir()): 113 + print(profile.name) 114 + return True 115 116 117 + def then_launch(profile: Profile, foreground: bool, qb_args: list[str]): 118 + result = False 119 + if profile: 120 + result = operations.launch(profile, False, foreground, qb_args) 121 + return result 122 123 124 if __name__ == "__main__": 125 + main(obj={})
+10 -28
qbpm/operations.py
··· 1 - import argparse 2 import os 3 import shutil 4 import subprocess ··· 42 43 44 def launch( 45 - profile: Profile, strict: bool, foreground: bool, qb_args: list[str] 46 ) -> bool: 47 - if not profiles.ensure_profile_exists(profile, not strict): 48 return False 49 50 args = profile.cmdline() + qb_args ··· 78 return exists 79 80 81 - def list_(args: argparse.Namespace) -> bool: 82 - for profile in sorted(args.profile_dir.iterdir()): 83 - print(profile.name) 84 - return True 85 - 86 - 87 - def choose(args: argparse.Namespace) -> bool: 88 - menu = args.menu or next(installed_menus()) 89 if not menu: 90 error(f"No menu program found, please install one of: {AUTO_MENUS}") 91 return False 92 if menu == "applescript" and platform != "darwin": 93 error(f"Menu applescript cannot be used on a {platform} host") 94 return False 95 - profiles = [profile.name for profile in sorted(args.profile_dir.iterdir())] 96 if len(profiles) == 0: 97 error("No profiles") 98 return False 99 100 - command = menu_command(menu, profiles, args) 101 if not command: 102 return False 103 ··· 111 selection = out and out.read().decode(errors="ignore").rstrip("\n") 112 113 if selection: 114 - profile = Profile(selection, args.profile_dir) 115 - launch(profile, True, args.foreground, args.qb_args) 116 else: 117 error("No profile selected") 118 return False 119 return True 120 121 122 - def menu_command( 123 - menu: str, profiles: list[str], args: argparse.Namespace 124 - ) -> Optional[str]: 125 - arg_string = " ".join(args.qb_args) 126 if menu == "applescript": 127 profile_list = '", "'.join(profiles) 128 return f"""osascript -e \'set profiles to {{"{profile_list}"}} ··· 149 return None 150 profile_list = "\n".join(profiles) 151 return f'echo "{profile_list}" | {command}' 152 - 153 - 154 - def edit(profile: Profile) -> bool: 155 - if not profile.exists(): 156 - error(f"profile {profile.name} not found at {profile.root}") 157 - return False 158 - editor = os.environ.get("VISUAL") or os.environ.get("EDITOR") or "vim" 159 - os.execlp(editor, editor, str(profile.root / "config" / "config.py")) 160 - return True
··· 1 import os 2 import shutil 3 import subprocess ··· 41 42 43 def launch( 44 + profile: Profile, create: bool, foreground: bool, qb_args: list[str] 45 ) -> bool: 46 + if not profiles.ensure_profile_exists(profile, create): 47 return False 48 49 args = profile.cmdline() + qb_args ··· 77 return exists 78 79 80 + def choose(profile_dir: Path, menu: str, foreground: bool, qb_args: list[str]) -> bool: 81 + menu = menu or next(installed_menus()) 82 if not menu: 83 error(f"No menu program found, please install one of: {AUTO_MENUS}") 84 return False 85 if menu == "applescript" and platform != "darwin": 86 error(f"Menu applescript cannot be used on a {platform} host") 87 return False 88 + profiles = [profile.name for profile in sorted(profile_dir.iterdir())] 89 if len(profiles) == 0: 90 error("No profiles") 91 return False 92 93 + command = menu_command(menu, profiles, qb_args) 94 if not command: 95 return False 96 ··· 104 selection = out and out.read().decode(errors="ignore").rstrip("\n") 105 106 if selection: 107 + profile = Profile(selection, profile_dir) 108 + launch(profile, True, foreground, qb_args) 109 else: 110 error("No profile selected") 111 return False 112 return True 113 114 115 + def menu_command(menu: str, profiles: list[str], qb_args: list[str]) -> Optional[str]: 116 + arg_string = " ".join(qb_args) 117 if menu == "applescript": 118 profile_list = '", "'.join(profiles) 119 return f"""osascript -e \'set profiles to {{"{profile_list}"}} ··· 140 return None 141 profile_list = "\n".join(profiles) 142 return f'echo "{profile_list}" | {command}'
+4
qbpm/utils.py
··· 18 print(f"error: {msg}", file=stderr) 19 20 21 def user_data_dir() -> Path: 22 if platform.system() == "Linux": 23 return Path(BaseDirectory.xdg_data_home) / "qutebrowser"
··· 18 print(f"error: {msg}", file=stderr) 19 20 21 + def default_profile_dir(): 22 + return Path(BaseDirectory.save_data_path("qutebrowser-profiles")) 23 + 24 + 25 def user_data_dir() -> Path: 26 if platform.system() == "Linux": 27 return Path(BaseDirectory.xdg_data_home) / "qutebrowser"