diff --git a/_dictionaries/inkscape.md b/_dictionaries/inkscape.md new file mode 100644 index 0000000..6b3c4a3 --- /dev/null +++ b/_dictionaries/inkscape.md @@ -0,0 +1,134 @@ +--- +layout: dictionary +title: Inkscape Commands +version: 1 +date: 2025-03-07 +filename: inkscape +author: user202729 +tags: commands linux inkscape +what: Convenient commands for Inkscape +formats: + - py +--- + +## Why + +Convenient shortcut for working with Inkscape. +Port of [Gilles Castel's shortcut manager](https://castel.dev/post/lecture-notes-2/) to Plover. + +## Requirements + +- Executables: `xclip`, `inkscape`, `xdotool`, `notify-send`, `rofi` +- Python packages: `getactivewindow-x` (optional), `tomlkit`, `plover_python_dictionary_lib` + +## Installation + +First install https://pypi.org/project/plover-python-dictionary/, then (restart Plover and) +add the `inkscape.py` file as a dictionary to Plover. + +**Important**: If you're not running Plover with the patch in https://github.com/openstenoproject/plover/pull/1160, +comment out the following lines from the file: + +```python +elif all(Stroke(s) in left_hand for s in strokes): + # another consequence of not using proper macro + # require https://github.com/openstenoproject/plover/pull/1160 + # just comment out if you don't have the patch (but tool will be slightly less functional) + return "{plover:deleted}" +``` + +## Documentation + +This script is a dictionary for Plover to use with Inkscape. It allows you to bind strokes to Inkscape actions, +such as selecting the pencil tool, creating a rectangle, or applying a style. + +For convenience, all functionalities are accessible from the left hand. + +It is likely that you would want to heavily customize the script to suit your needs. +As such, you need to know Python programming to use this script. +The default settings are as follows: + +1. General-purpose strokes (specified in pseudo-steno, for example `X` is `KP`): + + - `P`: pencil tool + - `X`: toggle snap + - `B`: bezier tool + - etc. + + See `adhoc_dict` in the script for more examples. + +2. To apply a style, press a stroke containing `A`. + For example: + + - `TA`: set stroke color to none (transparent) + - `#TA`: set fill color to none + - `KA`: set stroke color to black + - `#KA`: set fill color to black + - `BA`: set stroke color to blue + - `KBA`: set stroke color to light blue + - `TBA`: set stroke color to dark blue + - `SA`: set stroke width to thin + - `STPA`: set stroke width to thick + - etc. + + See `colors` and `styles` in the script for more examples. + +3. Because of the limited number of possible strokes on the left hand side, + the script overrides several commonly-used strokes. For example `S` and `T` and `R` no longer type out `is` or `it` + or `are` but instead are used for Inkscape actions. + + As such, use `SKWR` to toggle enable/disable the dictionary. + (You can also use `plover-dict-commands` project for this purpose, but it is bundled for convenience) + +4. There is a system to save and load objects. + The information is kept in `saved_object.toml` file in Plover configuration directory. + (Plover GUI → `File` → `Open config folder`) + + Select an object and press `#STPHO` to save an object, for example type in `circle [KRO]` + and press `Ctrl-Enter` to add the object. + + To load (paste) the object, type either `KRO` (as typed in above), or `STPHO` then select the object + by name. + + Objects cannot be easily deleted this way, you need to go to the TOML file and manually delete. + +5. Independently from this dictionary, I have a patch to Plover that I use internally + (see https://github.com/user202729/plover/blob/dev/plover/machine/keyboard.py#L176-L187 ) + to allow holding down/release modifier keys (Ctrl, Shift, Alt). This is crucial for Inkscape + because many of the features are accessed through holding a modifier key while moving the mouse. + + So for example I can first hold down `#BR`, then if I want to hold `Ctrl`, + I additionally hold `K`. When I want to release `Ctrl`, I release `K`. + + The way it works is follows: that part of the code converts individual events (`K` pressed, `K` released) + into never-used strokes (e.g. when `#BR` is already held and `K` is *additionally* pressed, + the machine sends `#KBR-FBLSD`, when `K` is then released, the machine sends `#KBR-RPGTZ`) + + Then, these strokes are then interpreted by another dictionary of mine to become + `{#Control_L:down}` and `{#Control_L:up}` respectively. + + Finally, another patch to Plover https://github.com/user202729/plover/blob/dev/plover/key_combo.py#L168-L174 + translates them to key events. + + Obviously this only works if you are using the keyboard instead of some steno protocol like Gemini. + + Unfortunately, neither of https://github.com/openstenoproject/plover/pull/1161 nor + https://github.com/openstenoproject/plover/pull/1161 gets merged. + + Side note: I also have a patch to Plover that only enables Plover on designated steno keyboards + https://github.com/user202729/plover/blob/dev/plover/oslayer/linux/keyboardcontrol_x11.py#L229-L230 , + as such the disadvantages of keyboard input method is not present for me. + +## Note + +1. It's not difficult to avoid using `getactivewindow` (in fact it's somewhat flaky), + just modify the source code below. See https://pypi.org/project/getactivewindow-x/0.3.0/ + for a list of replacements. + +2. I have left `*` map to `#` because Starboard. Change `StrokeH` function if you use something else. + +3. It may be possible to eliminate `xdotool` if there's some way to access the `Engine` object from Plover, + then the `KeyboardEmulation` object can be accessed that way. + One way to do that is to use https://github.com/user202729/plover-startup-py to pass `engine` + (provided as a global variable to the script) somewhere then the Python dictionary can access it + (very hacky). diff --git a/dictionaries/inkscape.py b/dictionaries/inkscape.py new file mode 100644 index 0000000..a30e693 --- /dev/null +++ b/dictionaries/inkscape.py @@ -0,0 +1,656 @@ +#!/bin/python +""" +Heavily inspired from https://github.com/gillescastel/inkscape-shortcut-manager +""" +from __future__ import annotations +from typing import Optional +import typing + +Stroke_ = typing.Any # instance of Stroke, define like this to satisfy mypy +def settings()->tuple[dict[Stroke_, str], dict[Stroke_, str], Stroke_, Stroke_, Stroke_]: + + adhoc_dict: dict[Stroke_, str] = { + Stroke("P" ): "{#p}", # pencil + Stroke("KP" ): "{^}%{^}", # snap (x instead of %) + Stroke("PW" ): "{#b}", # bezier + Stroke("S" ): "{#s}", # select + StrokeH("R" ): "{#Shift(s)}", # toggle between rotate and resize mode (in select mode) + Stroke("TPH"): "{#n}", # node + Stroke("R" ): "{#r}", # rectangle + Stroke("KR" ): "{#e}", # circle (*ellipse) + Stroke("TK" ): "{#Control(d)}",# duplicate + Stroke("T" ): "{#Control(t)}",# my internal binding for TeXtext (it's slow to spawn though) + Stroke("SKW"): "{#Control(z)}",# undo + Stroke("STK"): "{#Control(y)}",# redo + Stroke("SH"): "{#F12}", # show/hide dialogues + StrokeH("KR"): copy_object, + StrokeH("SR"): paste_object, + Stroke("TPR"): "{#Escape}", + StrokeH("K"): "{#Control(k)}", # merge paths + Stroke("KPW"): "{#BackSpace}", # shape following single-stroke-modifier + Stroke("PHR"): "{#Page_Up}", + Stroke("WHR"): "{#Page_Down}", + StrokeH("PHR"): "{#Home}", + StrokeH("WHR"): "{#End}", + Stroke("PH" ): "{^}+{^}", # zoom in + Stroke("WR" ): "{^}-{^}", # zoom out + StrokeH("S" ): "{#Control(s)}", + Stroke("WH" ): "{#Control(Slash)}", # division + Stroke("PWR"): "{#Delete}", + Stroke("TKPW"): "{#Control(G)}", # group + StrokeH("TKPW"): "{#Control(Shift(G))}", # ungroup + Stroke("TKR"): "{#Control(Shift(R))}", # resize page to selection/drawing + } + + # ↓ used to use this but is too restrictive + #colors = { # shape following my internal okular set-color script + # "T" : color_none.name, + # "R" : "red" , + # "PW" : "blue" , + # "KR" : "cyan" , + # "PH" : "magenta", + # "TKPWR": "orange" , + # "PWHR" : "black" , + # "TKPW" : "green" , + # "W" : "white" , + # "PR" : "#cccccc", # 20% gray + # } + + color_bases = { + "R" :"red", # Red + #"PWR" :"orange",# doesn't work on my side because sticky keys + "WR" :"yellow", # Yellow (truncated) + "HR" :"lime", # Lime + "H" :"green", + "PWH" :"cyan", # (mix of blue and green) + "PW" :"blue", # BLue + "PWHR":"sky", + "PH" :"fuchsia",# Magenta? + "PR" :"gray", # gRay (?) + "WHR" :"teal", + "W" :"violet", + "PHR" :"purple", # PurpLe + "P" :"pink", + "WH" :"rose", + } + + colors = dict_merge( + { + "T" : color_none.name, + "TK" : "white", + "K" : "black", + }, + {Stroke(stroke_base) | "K": tailwind_colors[color_base][2] for stroke_base, color_base in color_bases.items()}, + {stroke_base : tailwind_colors[color_base][5] for stroke_base, color_base in color_bases.items()}, + {Stroke(stroke_base) | "T": tailwind_colors[color_base][7] for stroke_base, color_base in color_bases.items()}, + ) + + # hold with A to apply color on stroke (or with A# to fill) + # the left * button on my keyboard is # so… + styles: dict[Stroke_, str] = dict_merge( + { + Stroke(str(stroke)): create_style_str(stroke=Color(color)) + for stroke, color in colors.items()}, + { + StrokeH(str(stroke)): create_style_str(fill_or_arrow=Color(color)) + for stroke, color in colors.items()}, + { + # non-color style: + # S: always pressed + # thickness: + # T P - + # S - - - + # transparency: + # - - - + # S K W - + # arrow: + # T P - + # S K W - + # (to cancel arrow press #TA = no fill) + # line style (solid/dashed/dotted): + # - - H + # S - - R + Stroke("STK") : create_style_str(fill_or_arrow=Arrow()), + Stroke("STKPW"): create_style_str(fill_or_arrow=DoubleArrow()), + Stroke("SHR"): create_style_str(stroke_style=StrokeStyle.solid), + Stroke("SR"): create_style_str(stroke_style=StrokeStyle.dashed), + Stroke("SH") : create_style_str(stroke_style=StrokeStyle.dotted), + Stroke("S") : create_style_str(thickness=Thickness.thin), + Stroke("ST") : create_style_str(thickness=Thickness.normal), + Stroke("STP"): create_style_str(thickness=Thickness.thick), + Stroke("SK") : create_style_str(fill_opacity=0.3, stroke_opacity=0.3, opacity=0.3), + Stroke("SW") : create_style_str(fill_opacity=0.6, stroke_opacity=0.6, opacity=0.6), + Stroke("SKW"): create_style_str(fill_opacity=1, stroke_opacity=1, opacity=1), + }) + + toggle_enabled_stroke = Stroke("SKWR") + + object_load_stroke = Stroke("STPHO") + object_save_stroke = StrokeH("STPHO") + + return adhoc_dict, styles, toggle_enabled_stroke, object_load_stroke, object_save_stroke + +from dataclasses import dataclass +from enum import Enum, auto +from pathlib import Path +from threading import Thread +from typing import TypeVar +import re +import subprocess +import sys +import time +import tomlkit + +try: + from typing import TypedDict +except ImportError: + from typing_extensions import TypedDict + +T = TypeVar("T") +U = TypeVar("U") + +def dict_merge(*dicts: dict[T, U])->dict[T, U]: + merged: dict[T, U] = {} + for d in dicts: + duplicate_keys = set(merged.keys()) & set(d.keys()) + if duplicate_keys: + raise ValueError(f"duplicate keys e.g. {duplicate_keys.pop()}") + merged.update(d) + return merged + +notification_id=10000 # just some random number… (it accepts nonexistent id) +def notify_send(message: str)->None: + subprocess.run(["notify-send", "-t", "2000", "-r", str(notification_id), "--", message]) + +def inkscape_window_focused()->bool: + try: + from getactivewindow import active_window_id, window_class + except ImportError: + notify_send(f"getactivewindow not installed, dictionary will always be enabled. Use {toggle_enabled_stroke} to disable") + def _inkscape_window_focused()->bool: + return True + global inkscape_window_focused + inkscape_window_focused=_inkscape_window_focused + return True + return "org.inkscape.Inkscape" in window_class(active_window_id()) + +copy_object = "{#Control(c)}" +paste_object = "{#Control(v)}" +paste_style = "{#Shift(Control(v))}" +no_op = "{#}" + +# level: 50 100 200 300 400 500 600 700 800 900 950 +# index: 0 1 2 3 4 5 6 7 8 9 10 +tailwind_colors = { # https://gist.github.com/user202729/f51f92bb5135eaafccd2ac2533b18caf +"red": "#fef2f2 #ffe2e2 #ffc9c9 #ffa2a2 #ff6467 #fb2c36 #e7000b #c10007 #9f0712 #82181a #460809".split(), +"orange": "#fff7ed #ffedd4 #ffd6a7 #ffb86a #ff8904 #ff6900 #f54900 #ca3500 #9f2d00 #7e2a0c #441306".split(), +"amber": "#fffbeb #fef3c6 #fee685 #ffd230 #ffb900 #fe9a00 #e17100 #bb4d00 #973c00 #7b3306 #461901".split(), +"yellow": "#fefce8 #fef9c2 #fff085 #ffdf20 #fdc700 #f0b100 #d08700 #a65f00 #894b00 #733e0a #432004".split(), +"lime": "#f7fee7 #ecfcca #d8f999 #bbf451 #9ae600 #7ccf00 #5ea500 #497d00 #3c6300 #35530e #192e03".split(), +"green": "#f0fdf4 #dcfce7 #b9f8cf #7bf1a8 #05df72 #00c950 #00a63e #008236 #016630 #0d542b #032e15".split(), +"emerald": "#ecfdf5 #d0fae5 #a4f4cf #5ee9b5 #00d492 #00bc7d #009966 #007a55 #006045 #004f3b #002c22".split(), +"teal": "#f0fdfa #cbfbf1 #96f7e4 #46ecd5 #00d5be #00bba7 #009689 #00786f #005f5a #0b4f4a #022f2e".split(), +"cyan": "#ecfeff #cefafe #a2f4fd #53eafd #00d3f2 #00b8db #0092b8 #007595 #005f78 #104e64 #053345".split(), +"sky": "#f0f9ff #dff2fe #b8e6fe #74d4ff #00bcff #00a6f4 #0084d1 #0069a8 #00598a #024a70 #052f4a".split(), +"blue": "#eff6ff #dbeafe #bedbff #8ec5ff #51a2ff #2b7fff #155dfc #1447e6 #193cb8 #1c398e #162456".split(), +"indigo": "#eef2ff #e0e7ff #c6d2ff #a3b3ff #7c86ff #615fff #4f39f6 #432dd7 #372aac #312c85 #1e1a4d".split(), +"violet": "#f5f3ff #ede9fe #ddd6ff #c4b4ff #a684ff #8e51ff #7f22fe #7008e7 #5d0ec0 #4d179a #2f0d68".split(), +"purple": "#faf5ff #f3e8ff #e9d4ff #dab2ff #c27aff #ad46ff #9810fa #8200db #6e11b0 #59168b #3c0366".split(), +"fuchsia": "#fdf4ff #fae8ff #f6cfff #f4a8ff #ed6aff #e12afb #c800de #a800b7 #8a0194 #721378 #4b004f".split(), +"pink": "#fdf2f8 #fce7f3 #fccee8 #fda5d5 #fb64b6 #f6339a #e60076 #c6005c #a3004c #861043 #510424".split(), +"rose": "#fff1f2 #ffe4e6 #ffccd3 #ffa1ad #ff637e #ff2056 #ec003f #c70036 #a50036 #8b0836 #4d0218".split(), +"slate": "#f8fafc #f1f5f9 #e2e8f0 #cad5e2 #90a1b9 #62748e #45556c #314158 #1d293d #0f172b #020618".split(), +"gray": "#f9fafb #f3f4f6 #e5e7eb #d1d5dc #99a1af #6a7282 #4a5565 #364153 #1e2939 #101828 #030712".split(), +"zinc": "#fafafa #f4f4f5 #e4e4e7 #d4d4d8 #9f9fa9 #71717b #52525c #3f3f46 #27272a #18181b #09090b".split(), +"neutral": "#fafafa #f5f5f5 #e5e5e5 #d4d4d4 #a1a1a1 #737373 #525252 #404040 #262626 #171717 #0a0a0a".split(), +"stone": "#fafaf9 #f5f5f4 #e7e5e4 #d6d3d1 #a6a09b #79716b #57534d #44403b #292524 #1c1917 #0c0a09".split(), +} + +from plover.system import english_stenotype as e # type: ignore +from plover_python_dictionary_lib import get_context_from_system + +context=get_context_from_system(e) +Stroke=context.stroke_type +def StrokeH(s: str)->str: + #return Stroke(s)|Stroke("*") + return Stroke("#"+s) + + +def clipboard_copy(string: str, target: Optional[str]=None)->None: + extra_args = [] + if target != None: + extra_args += ['-target', target] + + subprocess.run( + ['xclip', '-selection', 'c'] + extra_args, + universal_newlines=True, + input=string + ) + +def clipboard_get(target: Optional[str]=None)->str: + extra_args = [] + if target != None: + extra_args += ['-target', target] + + result = subprocess.run( + ['xclip', '-selection', 'c', '-o'] + extra_args, + stdout=subprocess.PIPE, + universal_newlines=True + ) + + stdout = result.stdout.strip() + return stdout + +TARGET = 'image/x-inkscape-svg' + +""" +I use an ad hoc style dict, apply one style at once, +instead of gillescastel's method of "one stroke to apply all aspects of the style". +That said, more styles can also be defined in `settings()` above. + + T P H +S K W R # + +three axes: (fill/arrow), stroke, thickness +left half for fill, right half for stroke +if omitted: no change + +fill/arrow axis: + unchanged + none + white + gray + black + one arrow head + two arrow head + +stroke axis: + unchanged + none + normal + thin + thick + +stroke style axis: + unchanged + none + solid/continuous + dashed + dotted +""" + + + +@dataclass +class Color: + name: str # could be "none" +color_none = Color("none") + +class Arrow: pass +class DoubleArrow: pass + + +# there is no "none" option below because it is tied to inkscape svg internal +# to explicitly "set stroke/thickness to none", set stroke color to color_none +class StrokeStyle(Enum): + solid = auto() + dashed = auto() + dotted = auto() + +class Thickness(Enum): + # reminds me of tailwind + thin = auto() + normal = auto() + thick = auto() + +def _style_constants()->tuple[float, float, float, float]: + # cf. inkscape-shortcut-manager + pt = 1.327 # pixels + w = 0.4 * pt + thick_width = 0.8 * pt + very_thick_width = 1.2 * pt + return pt, w, thick_width, very_thick_width + +# None: unchanged +def create_style_str( + *, + fill_or_arrow: Color|Arrow|DoubleArrow|None=None, + fill_opacity: float|None=None, + stroke: Color|None=None, + stroke_opacity: float|None=None, + stroke_style: StrokeStyle|None=None, + thickness: Thickness|None=None, + opacity: float|None=None, # for image + )->str: + """ + create a style string to be copied into inkscape + the parameters are tied to inkscape svg internal + so e.g. if you paste style of `create_style_str(stroke_style=StrokeStyle.dashed)` + onto a shape with `stroke=color_none` you will not see any change + """ + style: dict[str, str] = {} + pt, w, thick_width, very_thick_width = _style_constants() + + if isinstance(fill_or_arrow, Color): + style['fill'] = fill_or_arrow.name + style['marker-start'] = 'none' + style['marker-end'] = 'none' + elif isinstance(fill_or_arrow, Arrow): + style['fill'] = 'none' + style['marker-start'] = 'none' + style['marker-end'] = f'url(#marker-arrow-{w})' + elif isinstance(fill_or_arrow, DoubleArrow): + style['fill'] = 'none' + style['marker-start'] = f'url(#marker-arrow-{w})' + style['marker-end'] = f'url(#marker-arrow-{w})' + else: + assert fill_or_arrow is None + + if isinstance(fill_opacity, (int, float)): + style['fill-opacity'] = str(fill_opacity) + else: + assert fill_opacity is None + + if isinstance(stroke, Color): + style['stroke'] = stroke.name + else: + assert stroke is None + + if isinstance(stroke_opacity, (int, float)): + style['stroke-opacity'] = str(stroke_opacity) + else: + assert stroke_opacity is None + + if isinstance(stroke_style, StrokeStyle): + if stroke_style == StrokeStyle.solid: + style['stroke-dasharray'] = 'none' + elif stroke_style == StrokeStyle.dashed: + style['stroke-dasharray'] = f'{w},{2*pt}' + elif stroke_style == StrokeStyle.dotted: + style['stroke-dasharray'] = f'{3*pt},{3*pt}' + else: + raise ValueError(f"unknown stroke_style {stroke_style}") + else: + assert stroke_style is None + + if isinstance(thickness, Thickness): + if thickness == Thickness.thin: + style['stroke-width'] = str(w) + elif thickness == Thickness.normal: + style['stroke-width'] = str(thick_width) + elif thickness == Thickness.thick: + style['stroke-width'] = str(very_thick_width) + else: + raise ValueError(f"unknown thickness {thickness}") + else: + assert thickness is None + + if isinstance(opacity, (int, float)): + style['opacity'] = str(opacity) + else: + assert opacity is None + + style_string = ';'.join('{}:{}'.format(key, value) + for key, value in sorted(style.items(), key=lambda x: x[0]) + ) + return style_string + + +def _marker_helper()->str: + # cf. inkscape-shortcut-manager + pt, w, thick_width, very_thick_width = _style_constants() + return f''' + + + + + + + + ''' + +# to investigate the paste format can run +# xclip -o -t image/x-inkscape-svg -selection clipboard +# after copying something from inkscape + +adhoc_dict, styles, toggle_enabled_stroke, object_load_stroke, object_save_stroke = settings() +assert toggle_enabled_stroke not in adhoc_dict + +def is_style_stroke(s: Stroke_)->bool: + """ + press a style stroke will apply a style + """ + return s in StrokeH("STKPWHRA") and "A" in s + +def is_object_stroke(s: Stroke_)->bool: + """ + check whether s is an object stroke. + + press an object stroke will paste an object + (we reserve #O for saving an object) + """ + return s in StrokeH("STKPWHRO") and "O" in s + +assert is_object_stroke(object_load_stroke) +assert is_object_stroke(object_save_stroke) + + +@dataclass +class SavedObject: + # actually snippet, can also be repurposed as style by using {paste_style} instead of {paste_object} + # to be implemented later + name: str + stroke: str|None + content: str + +from plover.oslayer.config import CONFIG_DIR # type: ignore +saved_object_file_path: Path = Path(CONFIG_DIR) / "saved_object.toml" + +objects: list[SavedObject] + +saved_object_file_last_modification_time: Optional[float]=None + +def get_saved_object_file_last_modification_time()->Optional[float]: + try: + return saved_object_file_path.stat().st_mtime + except FileNotFoundError: + return None + +def reload_saved_objects()->None: + global objects + try: + d: typing.Any = tomlkit.loads(saved_object_file_path.read_text()) + objects = [ + SavedObject( + name=str(o["name"]), + stroke=str(o["stroke"]) if "stroke" in o else None, + content=str(o["content"])) + for o in d.get("objects", [])] + except FileNotFoundError: + objects = [] + except Exception as e: + notify_send(f"file {saved_object_file_path} is corrupted, consider deleting it yourself. Exception detail: {e}") + objects = [] + +def maybe_reload_saved_objects()->None: + global saved_object_file_last_modification_time + last_modification_time = get_saved_object_file_last_modification_time() + if last_modification_time != saved_object_file_last_modification_time: + saved_object_file_last_modification_time = last_modification_time + reload_saved_objects() + +maybe_reload_saved_objects() + +rofi_running: bool = False + +def rofi(prompt: str, options: list[str], rofi_args: list[str]=[], fuzzy: bool=True)->tuple[int, str]: + # cf. inkscape-shortcut-manager + global rofi_running + assert not rofi_running, "rofi is already running" + rofi_running = True + try: + assert not any("\n" in option for option in options), "newline is not allowed in options" + optionstr = '\n'.join(options) + args = ['rofi', '-sort', '-no-levenshtein-sort'] + if fuzzy: + args += ['-matching', 'fuzzy'] + args += ['-dmenu', '-p', prompt, '-format', 's', '-i'] + args += rofi_args + args = [str(arg) for arg in args] + result = subprocess.run(args, input=optionstr, stdout=subprocess.PIPE, universal_newlines=True) + return result.returncode, result.stdout.removesuffix('\n') + finally: + rofi_running = False + +def select_object(prompt: str)->tuple[Optional[str], Optional[Stroke_]]: + """ + prompt user with rofi and return (name, stroke) + """ + maybe_reload_saved_objects() + returncode, stdout = rofi(prompt, [ + f"{o.name} [{o.stroke}]" if o.stroke is not None else o.name + for o in objects]) + if returncode != 0: # probably canceled + return None, None + match = re.fullmatch(r"(.*)\[(.*)\]", stdout.strip()) + if match: + name, stroke = match.groups() + try: + return name.strip(), Stroke(stroke) + except ValueError as e: + notify_send(str(e)) + return None, None + else: + return stdout.strip(), None + +def do_load_object()->None: + """ + open a dialog to prompt the user which object to load + should be spawned in a separate thread. + """ + name, stroke = select_object("select object to load") + if name is None: return + try: + o=next(o for o in objects if o.name==name) + except StopIteration: + notify_send(f"no object with name {name!r}") + return + clipboard_copy(o.content, TARGET) + time.sleep(0.2) + subprocess.run(["xdotool", "key", "Control_L+v"]) + +def do_save_object()->None: + """ + open a dialog to prompt the user where to save the object + should be spawned in a separate thread. + """ + global objects + time.sleep(0.2) # wait until the key is processed and the object is copied + content = clipboard_get(TARGET) + if "svg" not in content: + notify_send("no svg content copied") + return + name, stroke = select_object("save object as (format: 'name' or 'name [stroke]')") + if name is None: + return + if stroke is not None and not is_object_stroke(Stroke(stroke)): + notify_send(f"stroke {stroke} is not a valid stroke for object") + return + if any(o.name==name for o in objects): + returncode, stdout = rofi( + f"overwrite {name!r}? (note Ctrl-Enter avoids selecting partial match)", + ["y", "n"]) + if not (returncode == 0 and stdout == "y"): + return + objects = [o for o in objects if o.name!=name] + objects.append(SavedObject(name=name, stroke=stroke, content=content)) + saved_object_file_path.write_text(tomlkit.dumps({"objects": [ + dict( + name=o.name, + **({"stroke": str(o.stroke)} if o.stroke is not None else {}), + content=tomlkit.string(o.content, multiline=True), + ) for o in objects]})) + notify_send(f"object {name!r} saved" + + (f" with stroke {stroke}" if stroke else "")) + +for stroke in adhoc_dict.keys(): + assert not is_style_stroke(stroke), f"ad hoc stroke {stroke} can be misrecognized as style" + assert not is_object_stroke(stroke), f"ad hoc stroke {stroke} can be misrecognized as object" +enabled = True + +def copy_style(style_string: str)->None: + # style_string should be output of create_style_str() + clipboard_copy( + '' + '' + + _marker_helper() + + f'' + '', TARGET) + +left_hand = StrokeH("STKPWHRAO") + +def lookup(strokes: tuple[str, ...])->Optional[str]: + # NOTE it is very wrong to make `lookup` not a pure function + # should use command plugin or https://github.com/user202729/plover-python-dictionary-cmd instead + # (probably the former, more flexible) + # (in practice it works anyway) + if not inkscape_window_focused(): + return None + if rofi_running: + return None + global enabled + if len(strokes)==1: + stroke = Stroke(strokes[0]) + if stroke == toggle_enabled_stroke: + enabled = not enabled + if enabled: + notify_send("inkscape dict enabled") + else: + notify_send("inkscape dict disabled") + return no_op + if not enabled: + # `toggle_enabled_stroke` must be checked before this + return None + if stroke in StrokeH("TKPWRAO") and StrokeH("PWR") in stroke: + # overlap with a certain other dictionary I use, hide the warning + return None + if stroke in adhoc_dict: + return adhoc_dict[stroke] + if is_style_stroke(stroke): + style_string = styles.get(stroke-Stroke("A"), None) + if style_string is not None: + copy_style(style_string) + return paste_style + if is_object_stroke(stroke): + if stroke==object_load_stroke: + Thread(target=do_load_object).start() + return no_op + if stroke==object_save_stroke: + Thread(target=do_save_object).start() + return copy_object + maybe_reload_saved_objects() + try: + o = next(o for o in objects if o.stroke is not None and Stroke(o.stroke)==stroke) + except StopIteration: + notify_send(f"no object with stroke {stroke}") + return no_op + clipboard_copy(o.content, TARGET) + return paste_object + if stroke in left_hand: + notify_send(f"invalid stroke {stroke}") + return no_op + elif all(Stroke(s) in left_hand for s in strokes): + # another consequence of not using proper macro + # require https://github.com/openstenoproject/plover/pull/1160 + # just comment out if you don't have the patch (but tool will be slightly less functional) + return "{plover:deleted}" + return None + +LONGEST_KEY = 3