tangled
alpha
login
or
join now
pvsr.dev
/
qbpm
qutebrowser profile manager
0
fork
atom
overview
issues
pulls
pipelines
mostly functional click setup
pvsr.dev
2 years ago
3d0a825b
bf1a8968
+111
-177
5 changed files
expand all
collapse all
unified
split
default.nix
flake.nix
qbpm
main.py
operations.py
utils.py
+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
0
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
0
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
0
0
0
0
0
0
0
0
0
0
0
0
0
0
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)
0
0
0
0
0
0
0
0
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))
0
0
0
0
0
0
0
0
0
0
0
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_)
0
0
0
0
0
0
0
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))
0
0
0
0
0
0
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)
0
0
0
0
0
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
0
0
0
0
0
0
0
0
0
0
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
0
0
0
0
0
0
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
0
0
0
0
175
176
177
if __name__ == "__main__":
178
-
main()
···
0
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
0
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
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
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
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
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
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
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
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
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
0
0
0
0
0
0
0
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
0
0
0
0
0
0
0
0
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
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
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
0
0
0
0
0
0
0
0
0
0
0
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
···
0
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())
0
0
0
0
0
0
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)
0
0
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}'
0
0
0
0
0
0
0
0
0
+4
qbpm/utils.py
···
18
print(f"error: {msg}", file=stderr)
19
20
0
0
0
0
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"