Skip to content

Commit 1306235

Browse files
committed
wip
1 parent de650b8 commit 1306235

File tree

6 files changed

+134
-57
lines changed

6 files changed

+134
-57
lines changed

flake.nix

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,15 @@
2121
// {
2222
buildInputs = with pkgs; [
2323
ruff
24-
xdg-utils.out
24+
xdg-utils
2525
(python3.withPackages (ps:
2626
with ps; [
2727
pyxdg
2828
favicon
2929
pillow
30+
types-pillow
3031
requests
32+
types-requests
3133
click
3234

3335
pytest

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ warn_return_any = true
4242
warn_unused_ignores = true
4343

4444
[[tool.mypy.overrides]]
45-
module = "xdg.*"
45+
module = ["xdg.*", "favicon.*"]
4646
ignore_missing_imports = true
4747

4848
[[tool.mypy.overrides]]

qbpm/icons.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import shutil
2+
from collections import namedtuple
3+
from pathlib import Path
4+
from tempfile import TemporaryDirectory
5+
from typing import Optional
6+
from urllib.parse import urlparse
7+
8+
import favicon
9+
import requests
10+
from PIL import Image
11+
12+
from .profiles import Profile
13+
14+
PREFERRED_ICONS = [
15+
"favicon.ico",
16+
"favicon.svg",
17+
"favicon.png",
18+
".ico",
19+
]
20+
21+
Icon = namedtuple("Icon", ["url", "width", "height", "format"])
22+
23+
24+
def choose_icon(icons: list[Icon]) -> Optional[Icon]:
25+
print(f"{icons=}")
26+
for key in PREFERRED_ICONS:
27+
for icon in icons:
28+
if key in icon.url:
29+
print(f"chose {icon}")
30+
return icon
31+
return None
32+
33+
34+
def download_icon(profile: Profile, home_page: str) -> Optional[Path]:
35+
if not urlparse(home_page).scheme:
36+
home_page = f"https://{home_page}"
37+
icons = favicon.get(home_page, timeout=10)
38+
icon = choose_icon(icons)
39+
if not icon:
40+
# TODO print
41+
return None
42+
43+
tmp_dir = TemporaryDirectory()
44+
work_dir = Path(tmp_dir.name)
45+
work_icon = work_dir / f"favicon.{icon.format}"
46+
resp = requests.get(icon.url, timeout=10)
47+
with work_icon.open("wb") as icon_file:
48+
for chunk in resp.iter_content(1024):
49+
icon_file.write(chunk)
50+
51+
icon_path = profile.root / f"icon.{icon.format}"
52+
if icon.format == "ico":
53+
icon_path = icon_path.with_suffix(".png")
54+
image = Image.open(work_icon)
55+
image.save(icon_path)
56+
else:
57+
shutil.move(work_icon, icon_path)
58+
59+
return icon_path.absolute()
60+
61+
62+
def find_icon_file(profile: Profile) -> Optional[Path]:
63+
icon_path = next(filter(lambda p: p.is_file(), profile.root.glob("icon.*")), None)
64+
return icon_path.absolute() if icon_path else icon_path

qbpm/main.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ def creator_options(f: Callable[..., Any]) -> Callable[..., Any]:
2020
is_flag=True,
2121
help="If --launch is set, run qutebrowser in the foreground.",
2222
),
23+
# TODO --no-icon?
2324
click.option(
2425
"--no-desktop-file",
2526
"desktop_file",
@@ -130,7 +131,8 @@ def choose(profile_dir: Path, **kwargs: Any) -> None:
130131
Support is built in for many X and Wayland launchers, as well as applescript dialogs.
131132
All QB_ARGS are passed on to qutebrowser."
132133
"""
133-
exit_with(operations.choose(profile_dir=profile_dir, **kwargs))
134+
# TODO force-icon
135+
exit_with(operations.choose(profile_dir=profile_dir, force_icon=False, **kwargs))
134136

135137

136138
@main.command()
@@ -155,14 +157,26 @@ def list_(profile_dir: Path) -> None:
155157

156158
@main.command()
157159
@click.argument("profile_name")
160+
@click.argument("icon")
158161
@click.pass_obj
159-
def icon(profile_dir: Path, profile_name: str) -> None:
160-
"""Edit a profile's config.py."""
162+
def icon(profile_dir: Path, profile_name: str, icon: str) -> None:
163+
# TODO support --all?
164+
"""Install an icon for the profile."""
161165
profile = Profile(profile_name, profile_dir)
162166
if not profile.exists():
163167
error(f"profile {profile.name} not found at {profile.root}")
164168
sys.exit(1)
165-
profile.root / "config" / "config.py"
169+
icon_file: Optional[Path]
170+
if Path(icon).is_file():
171+
# TODO move to profile dir
172+
# TODO overwrite protection
173+
icon_file = Path(icon)
174+
else:
175+
icon_file = profiles.download_icon(profile, icon)
176+
# TODO check if desktop file exists and should be updated
177+
desktop_file = False
178+
if desktop_file:
179+
profiles.create_desktop_file(profile, icon_file)
166180

167181

168182
def then_launch(

qbpm/operations.py

Lines changed: 45 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import shutil
22
import subprocess
3+
from dataclasses import dataclass
34
from pathlib import Path
45
from sys import platform
56
from typing import Optional
67

78
from xdg import BaseDirectory
89

910
from . import Profile, profiles
11+
from .icons import find_icon_file
1012
from .utils import AUTO_MENUS, error, installed_menus
1113

1214

@@ -64,7 +66,11 @@ def desktop(profile: Profile) -> bool:
6466

6567

6668
def choose(
67-
profile_dir: Path, menu: str, foreground: bool, qb_args: tuple[str, ...]
69+
profile_dir: Path,
70+
menu: str,
71+
foreground: bool,
72+
qb_args: tuple[str, ...],
73+
force_icon: bool,
6874
) -> bool:
6975
menu = menu or next(installed_menus())
7076
if not menu:
@@ -73,19 +79,24 @@ def choose(
7379
if menu == "applescript" and platform != "darwin":
7480
error(f"Menu applescript cannot be used on a {platform} host")
7581
return False
76-
profiles = [profile.name for profile in sorted(profile_dir.iterdir())]
82+
profiles = [
83+
Profile(profile.name, profile_dir) for profile in sorted(profile_dir.iterdir())
84+
]
7785
if len(profiles) == 0:
7886
error("No profiles")
7987
return False
8088

81-
command = menu_command(menu, profiles, qb_args)
82-
if not command:
89+
menu_cmd = menu_command(menu, profiles, qb_args)
90+
if not menu_cmd:
8391
return False
8492

93+
menu_items = build_menu_items(profiles, menu_cmd.icon_support or force_icon)
94+
8595
selection_cmd = subprocess.run(
86-
command[1],
87-
input=command[0],
96+
menu_cmd.command,
97+
input=menu_items,
8898
text=True,
99+
# TODO remove shell dependency
89100
shell=True,
90101
stdout=subprocess.PIPE,
91102
stderr=None,
@@ -102,24 +113,33 @@ def choose(
102113
return False
103114

104115

116+
@dataclass
117+
class Menu:
118+
command: str
119+
icon_support: bool
120+
121+
105122
def menu_command(
106-
menu: str, profiles: list[str], qb_args: tuple[str, ...]
107-
) -> Optional[tuple[str | None, str]]:
123+
menu: str, profiles: list[Profile], qb_args: tuple[str, ...]
124+
) -> Optional[Menu]:
125+
icon = False
108126
arg_string = " ".join(qb_args)
109127
if menu == "applescript":
110-
profile_list = '", "'.join(profiles)
111-
return (
112-
None,
128+
profile_list = '", "'.join([p.name for p in profiles])
129+
return Menu(
113130
f"""osascript -e \'set profiles to {{"{profile_list}"}}
114131
set profile to choose from list profiles with prompt "qutebrowser: {arg_string}" default items {{item 1 of profiles}}
115132
item 1 of profile\'""",
133+
icon,
116134
)
117135

118136
prompt = "-p qutebrowser"
119137
command = menu
138+
# TODO arg
120139
if len(menu.split(" ")) == 1:
121140
program = Path(menu).name
122141
if program == "rofi":
142+
icon = True
123143
command = f"{menu} -dmenu -no-custom {prompt} -mesg '{arg_string}'"
124144
elif program == "wofi":
125145
command = f"{menu} --dmenu {prompt}"
@@ -128,12 +148,22 @@ def menu_command(
128148
elif program.startswith("fzf"):
129149
command = f"{menu} --prompt 'qutebrowser '"
130150
elif program == "fuzzel":
151+
icon = True
131152
command = f"{menu} -d"
132-
profiles = [f"{p}\0icon\x1fqbpm-{p}\n" for p in profiles]
133-
print(f"{profiles=}")
134153
exe = command.split(" ")[0]
135154
if not shutil.which(exe):
136155
error(f"command '{exe}' not found")
137156
return None
138-
profile_list = "".join(profiles)
139-
return (profile_list, command)
157+
return Menu(command, icon)
158+
159+
160+
def build_menu_items(profiles: list[Profile], icon: bool) -> str:
161+
if icon and any(icons := [find_icon_file(p) for p in profiles]):
162+
menu_items = [
163+
f"{p.name}\0icon\x1f{icon or 'qutebrowser'}"
164+
for (p, icon) in zip(profiles, icons)
165+
]
166+
else:
167+
menu_items = [p.name for p in profiles]
168+
169+
return "\n".join(menu_items)

qbpm/profiles.py

Lines changed: 3 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,12 @@
1-
import subprocess
21
from functools import partial
32
from pathlib import Path
4-
from tempfile import TemporaryDirectory
53
from typing import Optional
64

7-
import favicon
8-
import requests
9-
from PIL import Image
105
from xdg import BaseDirectory
116
from xdg.DesktopEntry import DesktopEntry
127

138
from . import Profile
9+
from .icons import download_icon
1410
from .utils import error, user_config_dir
1511

1612

@@ -48,7 +44,7 @@ def create_config(
4844
application_dir = Path(BaseDirectory.xdg_data_home) / "applications" / "qbpm"
4945

5046

51-
def create_desktop_file(profile: Profile, icon: str | None) -> None:
47+
def create_desktop_file(profile: Profile, icon: Path | str | None = None) -> None:
5248
desktop = DesktopEntry(str(application_dir / f"{profile.name}.desktop"))
5349
desktop.set("Name", f"{profile.name} (qutebrowser profile)")
5450
# TODO allow passing in an icon value
@@ -60,36 +56,6 @@ def create_desktop_file(profile: Profile, icon: str | None) -> None:
6056
desktop.write()
6157

6258

63-
def download_icon(profile: Profile, home_page: str) -> str | None:
64-
icons = favicon.get(home_page)
65-
wanted = None
66-
print(icons)
67-
for icon in icons:
68-
if ".ico" in icon.url:
69-
wanted = icon
70-
print(f"chose {icon}")
71-
72-
tmp_dir = TemporaryDirectory()
73-
work_dir = Path(tmp_dir.name)
74-
fav = work_dir / "favicon.ico"
75-
if wanted:
76-
resp = requests.get(icon.url, timeout=10)
77-
with open(fav, "wb") as im:
78-
for chunk in resp.iter_content(1024):
79-
im.write(chunk)
80-
81-
name = f"qbpm-{profile.name}"
82-
image = Image.open(fav)
83-
png = work_dir / "icon.png"
84-
image.save(png)
85-
subprocess.run(
86-
f"xdg-icon-resource install --context apps {png} --size {image.size[0]} {name}",
87-
shell=True,
88-
)
89-
# image.save(Path(BaseDirectory.save_data_path("icons")) / "hicolor" / f"{image.size[0]}x{image.size[0]}" / "apps" / f"{name}.png")
90-
return name
91-
92-
9359
def ensure_profile_exists(profile: Profile, create: bool = True) -> bool:
9460
if profile.root.exists() and not profile.root.is_dir():
9561
error(f"{profile.root} is not a directory")
@@ -111,6 +77,7 @@ def new_profile(
11177
if create_profile(profile, overwrite):
11278
create_config(profile, home_page, overwrite)
11379
if home_page:
80+
# TODO catch errors?
11481
icon = download_icon(profile, home_page)
11582
if desktop_file:
11683
create_desktop_file(profile, icon)

0 commit comments

Comments
 (0)