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