Compare commits

..

3 commits

Author SHA1 Message Date
pull[bot]
1cc52196dd
Merge pull request #238 from dmitmel/master
[pull] master from dmitmel:master
2021-04-18 16:31:04 +00:00
Dmytro Meleshko
5585f9c693 begin adding types to the Python scripts 2021-04-18 18:06:47 +03:00
Dmytro Meleshko
fa2406e572 [common_script_utils] add support for using fzf as a chooser
Among other things this means that the emote copier is finally supported
on Android.
2021-04-18 17:59:35 +03:00
7 changed files with 92 additions and 55 deletions

View file

@ -2,19 +2,32 @@ import sys
import os import os
import subprocess import subprocess
from pathlib import Path from pathlib import Path
from typing import Iterable, NoReturn
if os.name == "posix": if os.name == "posix":
DOTFILES_CONFIG_DIR = Path.home() / ".config" / "dotfiles" DOTFILES_CONFIG_DIR: Path = Path.home() / ".config" / "dotfiles"
DOTFILES_CACHE_DIR = Path.home() / ".cache" / "dotfiles" DOTFILES_CACHE_DIR: Path = Path.home() / ".cache" / "dotfiles"
def platform_not_supported_error(): def platform_not_supported_error() -> NoReturn:
raise Exception("platform '{}' is not supported!".format(sys.platform)) raise Exception("platform '{}' is not supported!".format(sys.platform))
def run_chooser(choices, prompt=None, async_read=False): def run_chooser(
if sys.platform == "darwin": choices: Iterable[str], prompt: str = None, async_read: bool = False
) -> int:
supports_result_index = True
if os.isatty(sys.stderr.fileno()):
process_args = [
"fzf",
"--with-nth=2..",
"--height=50%",
"--reverse",
"--tiebreak=index",
]
supports_result_index = False
elif sys.platform == "darwin":
process_args = ["choose", "-i"] process_args = ["choose", "-i"]
elif os.name == "posix": elif os.name == "posix":
process_args = ["rofi", "-dmenu", "-i", "-format", "i"] process_args = ["rofi", "-dmenu", "-i", "-format", "i"]
@ -30,24 +43,23 @@ def run_chooser(choices, prompt=None, async_read=False):
) )
with chooser_process.stdin as pipe: with chooser_process.stdin as pipe:
for choice in choices: for index, choice in enumerate(choices):
assert "\n" not in choice assert "\n" not in choice
if not supports_result_index:
pipe.write(str(index).encode())
pipe.write(b" ")
pipe.write(choice.encode()) pipe.write(choice.encode())
pipe.write(b"\n") pipe.write(b"\n")
exit_code = chooser_process.wait() exit_code: int = chooser_process.wait()
if exit_code != 0: if exit_code != 0:
raise Exception("chooser process failed with exit code {}".format(exit_code)) raise Exception("chooser process failed with exit code {}".format(exit_code))
chosen_index = int( chosen_index = int(chooser_process.stdout.read().strip().split()[0])
# an extra newline is inserted by rofi for whatever reason
chooser_process.stdout.read().rstrip(b"\n")
)
return chosen_index return chosen_index
def send_notification(title, message, url=None): def send_notification(title: str, message: str, url: str = None) -> None:
if sys.platform == "darwin": if sys.platform == "darwin":
process_args = [ process_args = [
"terminal-notifier", "terminal-notifier",
@ -73,7 +85,7 @@ def send_notification(title, message, url=None):
subprocess.run(process_args, check=True) subprocess.run(process_args, check=True)
def set_clipboard(text): def set_clipboard(text: str) -> None:
# TODO: somehow merge program selection with the logic in `zsh/functions.zsh` # TODO: somehow merge program selection with the logic in `zsh/functions.zsh`
if sys.platform == "darwin": if sys.platform == "darwin":
process_args = ["pbcopy"] process_args = ["pbcopy"]

View file

@ -5,21 +5,22 @@
# <https://www.devdungeon.com/content/working-binary-data-python> # <https://www.devdungeon.com/content/working-binary-data-python>
import struct import struct
from typing import Any, IO
def read_bool(buf): def read_bool(buf: IO[bytes]) -> bool:
return buf.read(1)[0] == 1 return buf.read(1)[0] == 1
def read_number(buf): def read_number(buf: IO[bytes]) -> float:
return struct.unpack("<d", buf.read(8))[0] return struct.unpack("<d", buf.read(8))[0]
def _read_length(buf): def _read_length(buf: IO[bytes]) -> int:
return struct.unpack("<I", buf.read(4))[0] return struct.unpack("<I", buf.read(4))[0]
def read_string(buf): def read_string(buf: IO[bytes]) -> str:
is_empty = read_bool(buf) is_empty = read_bool(buf)
if is_empty: if is_empty:
return "" return ""
@ -29,25 +30,25 @@ def read_string(buf):
return buf.read(len_).decode("utf8") return buf.read(len_).decode("utf8")
def read_dictionary(buf): def read_dictionary(buf: IO[bytes]) -> dict[str, Any]:
len_ = _read_length(buf) len_ = _read_length(buf)
value = {} value: dict[str, Any] = {}
for _ in range(len_): for _ in range(len_):
key = read_string(buf) key = read_string(buf)
value[key] = read(buf) value[key] = read(buf)
return value return value
def read_list(buf): def read_list(buf: IO[bytes]) -> list[Any]:
len_ = _read_length(buf) len_ = _read_length(buf)
value = [] value: list[Any] = []
for _ in range(len_): for _ in range(len_):
read_string(buf) read_string(buf)
value.append(read(buf)) value.append(read(buf))
return value return value
def read(buf): def read(buf: IO[bytes]) -> Any:
type_, _any_type_flag = buf.read(2) type_, _any_type_flag = buf.read(2)
if type_ == 0: if type_ == 0:
return None return None

View file

@ -24,10 +24,11 @@ def humanize_bytes(bytes):
units = ["B", "kB", "MB", "GB"] units = ["B", "kB", "MB", "GB"]
factor = 1 factor = 1
for _unit in units: unit = ""
for unit in units:
next_factor = factor << 10 next_factor = factor << 10
if bytes < next_factor: if bytes < next_factor:
break break
factor = next_factor factor = next_factor
return "%.2f %s" % (float(bytes) / factor, _unit) return "%.2f %s" % (float(bytes) / factor, unit)

View file

@ -2,8 +2,10 @@
import sys import sys
import os import os
from pathlib import Path
from configparser import ConfigParser from configparser import ConfigParser
import json import json
from typing import Any, Generator, Optional, Union
import urllib.parse import urllib.parse
import urllib.request import urllib.request
@ -15,23 +17,25 @@ DEFAULT_REGISTRY_DUMP_URL = "https://stronghold.crosscode.ru/~ccbot/emote-regist
if os.name == "posix": if os.name == "posix":
config_path = ( config_path: Path = (
common_script_utils.DOTFILES_CONFIG_DIR / "copy-crosscode-emoji-url.ini" common_script_utils.DOTFILES_CONFIG_DIR / "copy-crosscode-emoji-url.ini"
) )
default_registry_dump_file = common_script_utils.DOTFILES_CACHE_DIR / "dotfiles" default_registry_dump_file: Path = (
common_script_utils.DOTFILES_CACHE_DIR / "dotfiles"
)
else: else:
common_script_utils.platform_not_supported_error() common_script_utils.platform_not_supported_error()
config = ConfigParser(interpolation=None) config = ConfigParser(interpolation=None)
config.read(config_path) config.read(config_path)
emotes = [] emotes: list[dict[str, Any]] = []
def emote_downloader_and_iterator(): def emote_downloader_and_iterator() -> Generator[str, None, None]:
global emotes global emotes
registry_dump_file = config.get( registry_dump_file: Optional[Union[str, Path]] = config.get(
"default", "ccbot_emote_registry_dump_file", fallback=None "default", "ccbot_emote_registry_dump_file", fallback=None
) )
if registry_dump_file is not None: if registry_dump_file is not None:
@ -43,6 +47,7 @@ def emote_downloader_and_iterator():
"default", "ccbot_emote_registry_dump_url", fallback=DEFAULT_REGISTRY_DUMP_URL "default", "ccbot_emote_registry_dump_url", fallback=DEFAULT_REGISTRY_DUMP_URL
) )
emote_registry_data: dict[str, Any]
try: try:
with open(registry_dump_file, "r") as f: with open(registry_dump_file, "r") as f:
emote_registry_data = json.load(f) emote_registry_data = json.load(f)
@ -66,8 +71,8 @@ chosen_index = common_script_utils.run_chooser(
if chosen_index >= 0: if chosen_index >= 0:
chosen_emote = emotes[chosen_index] chosen_emote = emotes[chosen_index]
emote_url = urllib.parse.urlparse(chosen_emote["url"]) emote_url: urllib.parse.ParseResult = urllib.parse.urlparse(chosen_emote["url"])
emote_url_query = urllib.parse.parse_qs(emote_url.query) emote_url_query: dict[str, list[str]] = urllib.parse.parse_qs(emote_url.query)
if config.getboolean("default", "add_emote_name_to_url", fallback=False): if config.getboolean("default", "add_emote_name_to_url", fallback=False):
emote_url_query["name"] = [chosen_emote["name"]] emote_url_query["name"] = [chosen_emote["name"]]
@ -78,10 +83,12 @@ if chosen_index >= 0:
if default_emote_image_size is not None: if default_emote_image_size is not None:
emote_url_query["size"] = [str(default_emote_image_size)] emote_url_query["size"] = [str(default_emote_image_size)]
emote_url_query = urllib.parse.urlencode(emote_url_query, doseq=True) emote_url_query_str = urllib.parse.urlencode(emote_url_query, doseq=True)
emote_url = urllib.parse.urlunparse(emote_url._replace(query=emote_url_query)) emote_url_str = urllib.parse.urlunparse(
emote_url._replace(query=emote_url_query_str)
)
common_script_utils.set_clipboard(emote_url) common_script_utils.set_clipboard(emote_url_str)
common_script_utils.send_notification( common_script_utils.send_notification(
os.path.basename(__file__), os.path.basename(__file__),

View file

@ -16,6 +16,10 @@ import factorio.property_tree
with open(Path.home() / ".factorio" / "mods" / "mod-settings.dat", "rb") as f: with open(Path.home() / ".factorio" / "mods" / "mod-settings.dat", "rb") as f:
version_main: int
version_major: int
version_minor: int
version_developer: int
version_main, version_major, version_minor, version_developer = struct.unpack( version_main, version_major, version_minor, version_developer = struct.unpack(
"<HHHH", f.read(8) "<HHHH", f.read(8)
) )

View file

@ -3,9 +3,9 @@
# helper script for query-bookmarks.sh # helper script for query-bookmarks.sh
# currently supports only Firefox # currently supports only Firefox
# useful links: # useful links:
# http://kb.mozillazine.org/Profiles.ini_file # <http://kb.mozillazine.org/Profiles.ini_file>
# https://stackoverflow.com/a/740183/12005228 # <https://stackoverflow.com/a/740183/12005228>
# https://wiki.mozilla.org/Places:BookmarksComments # <https://wiki.mozilla.org/Places:BookmarksComments>
import sys import sys
import os import os
@ -14,16 +14,16 @@ from configparser import ConfigParser
import tempfile import tempfile
import shutil import shutil
import sqlite3 import sqlite3
import collections from typing import Optional, Tuple, Generator
sys.path.insert(1, os.path.join(os.path.dirname(__file__), "..", "script-resources")) sys.path.insert(1, os.path.join(os.path.dirname(__file__), "..", "script-resources"))
import common_script_utils import common_script_utils
if sys.platform == "darwin": if sys.platform == "darwin":
firefox_home = Path.home() / "Library" / "Application Support" / "Firefox" firefox_home: Path = Path.home() / "Library" / "Application Support" / "Firefox"
elif os.name == "posix": elif os.name == "posix":
firefox_home = Path.home() / ".mozilla" / "firefox" firefox_home: Path = Path.home() / ".mozilla" / "firefox"
else: else:
common_script_utils.platform_not_supported_error() common_script_utils.platform_not_supported_error()
@ -31,15 +31,17 @@ else:
profiles_config = ConfigParser(interpolation=None) profiles_config = ConfigParser(interpolation=None)
profiles_config.read(firefox_home / "profiles.ini") profiles_config.read(firefox_home / "profiles.ini")
installs_sections = [s for s in profiles_config.sections() if s.startswith("Install")] installs_sections: list[str] = [
s for s in profiles_config.sections() if s.startswith("Install")
]
if not installs_sections: if not installs_sections:
raise Exception("no Firefox installations detected!") raise Exception("no Firefox installations detected!")
if len(installs_sections) > 1: if len(installs_sections) > 1:
raise Exception("multiple Firefox installations are not supported!") raise Exception("multiple Firefox installations are not supported!")
profile_dir = firefox_home / profiles_config.get(installs_sections[0], "Default") profile_dir: Path = firefox_home / profiles_config.get(installs_sections[0], "Default")
# should places.sqlite be used instead? # should places.sqlite be used instead?
db_path = profile_dir / "weave" / "bookmarks.sqlite" db_path: Path = profile_dir / "weave" / "bookmarks.sqlite"
if not db_path.is_file(): if not db_path.is_file():
raise Exception("'{}' is not a file".format(db_path)) raise Exception("'{}' is not a file".format(db_path))
@ -50,17 +52,22 @@ if not db_path.is_file():
db_copy_fd, db_copy_path = tempfile.mkstemp(prefix="bookmarks.", suffix=".sqlite") db_copy_fd, db_copy_path = tempfile.mkstemp(prefix="bookmarks.", suffix=".sqlite")
os.close(db_copy_fd) os.close(db_copy_fd)
chooser_entries = [] chooser_entries: list[Tuple[str, str, Optional[str]]] = []
try: try:
shutil.copyfile(db_path, db_copy_path) shutil.copyfile(db_path, db_copy_path)
db = sqlite3.connect(db_copy_path) db = sqlite3.connect(db_copy_path)
urls = {} urls: dict[int, str] = {}
url_id: int
url: str
for url_id, url in db.execute("SELECT id, url FROM urls"): for url_id, url in db.execute("SELECT id, url FROM urls"):
urls[url_id] = url urls[url_id] = url
folders = {} folders: dict[str, Tuple[Optional[str], str]] = {}
folder_id: str
parent_folder_id: str
folder_title: str
for folder_id, parent_folder_id, folder_title in db.execute( for folder_id, parent_folder_id, folder_title in db.execute(
"SELECT guid, parentGuid, title FROM items WHERE kind = 3 AND validity AND NOT isDeleted" "SELECT guid, parentGuid, title FROM items WHERE kind = 3 AND validity AND NOT isDeleted"
): ):
@ -69,24 +76,29 @@ try:
folder_title, folder_title,
) )
url_title: str
url_id: int
url_keyword: str
parent_folder_id: str
for url_title, url_id, url_keyword, parent_folder_id in db.execute( for url_title, url_id, url_keyword, parent_folder_id in db.execute(
"SELECT title, urlId, keyword, parentGuid FROM items WHERE kind = 1 AND validity AND NOT isDeleted" "SELECT title, urlId, keyword, parentGuid FROM items WHERE kind = 1 AND validity AND NOT isDeleted"
): ):
url = urls[url_id] url = urls[url_id]
folder_path = collections.deque() folder_path = list[str]()
while parent_folder_id is not None: parent_folder_id_2: Optional[str] = parent_folder_id
folder = folders.get(parent_folder_id, None) while parent_folder_id_2 is not None:
folder = folders.get(parent_folder_id_2, None)
if folder is None: if folder is None:
# broken folder structure? # broken folder structure?
folder_path.clear() folder_path.clear()
break break
parent_folder_id, folder_title = folder parent_folder_id_2, folder_title = folder
if folder_title is not None: if folder_title is not None:
folder_path.appendleft(folder_title) folder_path.append(folder_title)
folder_path_str = ( folder_path_str = (
("/" + "/".join(folder_path)) if len(folder_path) > 0 else None ("/" + "/".join(reversed(folder_path))) if len(folder_path) > 0 else None
) )
chooser_entries.append((url_title, url, folder_path_str)) chooser_entries.append((url_title, url, folder_path_str))
@ -97,7 +109,7 @@ finally:
os.remove(db_copy_path) os.remove(db_copy_path)
def chooser_entries_iter(): def chooser_entries_iter() -> Generator[str, None, None]:
for title, url, folder_path_str in chooser_entries: for title, url, folder_path_str in chooser_entries:
entry_items = [title, url] entry_items = [title, url]
if folder_path_str is not None: if folder_path_str is not None:

View file

@ -3,7 +3,7 @@
import random import random
def randbyte(): def randbyte() -> int:
return random.randrange(0, 256) return random.randrange(0, 256)