diff options
| -rw-r--r-- | accounts/gkleen@sif/default.nix | 2 | ||||
| -rw-r--r-- | accounts/gkleen@sif/niri/default.nix | 30 | ||||
| -rw-r--r-- | overlays/worktime/pyproject.toml | 2 | ||||
| -rw-r--r-- | overlays/worktime/uv.lock | 13 | ||||
| -rwxr-xr-x | overlays/worktime/worktime/__main__.py | 159 |
5 files changed, 192 insertions, 14 deletions
diff --git a/accounts/gkleen@sif/default.nix b/accounts/gkleen@sif/default.nix index ac520249..041ffa9e 100644 --- a/accounts/gkleen@sif/default.nix +++ b/accounts/gkleen@sif/default.nix | |||
| @@ -344,7 +344,7 @@ in { | |||
| 344 | font = "Fira Sans"; | 344 | font = "Fira Sans"; |
| 345 | }; | 345 | }; |
| 346 | colors = { | 346 | colors = { |
| 347 | background = "000000aa"; | 347 | background = "000000cc"; |
| 348 | text = "cdd6f4ff"; | 348 | text = "cdd6f4ff"; |
| 349 | match = "94e2d5ff"; | 349 | match = "94e2d5ff"; |
| 350 | selection = "585b70ff"; | 350 | selection = "585b70ff"; |
diff --git a/accounts/gkleen@sif/niri/default.nix b/accounts/gkleen@sif/niri/default.nix index bc4e24c8..9bd6eafd 100644 --- a/accounts/gkleen@sif/niri/default.nix +++ b/accounts/gkleen@sif/niri/default.nix | |||
| @@ -134,7 +134,7 @@ let | |||
| 134 | 134 | ||
| 135 | windows_json="$(niri msg -j windows)" | 135 | windows_json="$(niri msg -j windows)" |
| 136 | active_workspace="$(jq -r '.[] | select(.is_focused) | .workspace_id' <<<"$windows_json")" | 136 | active_workspace="$(jq -r '.[] | select(.is_focused) | .workspace_id' <<<"$windows_json")" |
| 137 | window_ix="$(gojq -r --arg active_workspace "$active_workspace" '.[] | select('"$window_select"') | "\(.title)\u0000icon\u001f\(.app_id)"' <<<"$windows_json" | fuzzel --log-level=warning --dmenu --index)" | 137 | window_ix="$(gojq -r --arg active_workspace "$active_workspace" '.[] | select('"$window_select"') | "\(.title)\u0000icon\u001f\(.app_id)"' <<<"$windows_json" | fuzzel --width=60 --log-level=warning --dmenu --index)" |
| 138 | # shellcheck disable=SC2016 | 138 | # shellcheck disable=SC2016 |
| 139 | window_json="$(gojq -rc --arg active_workspace "$active_workspace" --arg window_ix "$window_ix" 'map(select('"$window_select"')) | .[($window_ix | tonumber)]' <<<"$windows_json")" | 139 | window_json="$(gojq -rc --arg active_workspace "$active_workspace" --arg window_ix "$window_ix" 'map(select('"$window_select"')) | .[($window_ix | tonumber)]' <<<"$windows_json")" |
| 140 | 140 | ||
| @@ -449,7 +449,7 @@ in { | |||
| 449 | { title = "^Access Request.*"; } | 449 | { title = "^Access Request.*"; } |
| 450 | { title = ".*Passkey credentials$"; } | 450 | { title = ".*Passkey credentials$"; } |
| 451 | ]; | 451 | ]; |
| 452 | windowRuleExtra = [ | 452 | windowRuleExtra = with kdl; [ |
| 453 | (kdl.leaf "open-focused" false) | 453 | (kdl.leaf "open-focused" false) |
| 454 | ]; | 454 | ]; |
| 455 | key = "Mod+Control+P"; | 455 | key = "Mod+Control+P"; |
| @@ -477,6 +477,20 @@ in { | |||
| 477 | app-id = "com.github.wwmm.easyeffects"; | 477 | app-id = "com.github.wwmm.easyeffects"; |
| 478 | spawn = [ "easyeffects" ]; | 478 | spawn = [ "easyeffects" ]; |
| 479 | } | 479 | } |
| 480 | { name = "time"; | ||
| 481 | key = "Mod+Control+K"; | ||
| 482 | app-id = "chrome-kimai.yggdrasil.li__-Default"; | ||
| 483 | spawn = [ (toString (pkgs.resholve.writeScript "kimai" { | ||
| 484 | interpreter = pkgs.runtimeShell; | ||
| 485 | inputs = [ pkgs.dex ]; | ||
| 486 | execer = [ "cannot:${lib.getExe pkgs.dex}" ]; | ||
| 487 | } '' | ||
| 488 | exec dex $HOME/.local/state/nix/profile/share/applications/kimai.desktop | ||
| 489 | '')) ]; | ||
| 490 | windowRuleExtra = with kdl; [ | ||
| 491 | (leaf "block-out-from" "screencast") | ||
| 492 | ]; | ||
| 493 | } | ||
| 480 | ]; | 494 | ]; |
| 481 | programs.niri.config = | 495 | programs.niri.config = |
| 482 | let | 496 | let |
| @@ -683,12 +697,6 @@ in { | |||
| 683 | (leaf "open-on-workspace" "comm") | 697 | (leaf "open-on-workspace" "comm") |
| 684 | ]) | 698 | ]) |
| 685 | (plain "window-rule" [ | 699 | (plain "window-rule" [ |
| 686 | (leaf "match" { app-id = "^chrome-kimai\.yggdrasil\.li__-Default$"; }) | ||
| 687 | (leaf "open-on-workspace" "comm") | ||
| 688 | (leaf "open-fullscreen" false) | ||
| 689 | (plain "default-column-width" [(leaf "proportion" (2. / 3.))]) | ||
| 690 | ]) | ||
| 691 | (plain "window-rule" [ | ||
| 692 | (leaf "match" { app-id = "^firefox$"; }) | 700 | (leaf "match" { app-id = "^firefox$"; }) |
| 693 | (leaf "open-on-workspace" "web") | 701 | (leaf "open-on-workspace" "web") |
| 694 | (leaf "open-maximized" true) | 702 | (leaf "open-maximized" true) |
| @@ -803,7 +811,7 @@ in { | |||
| 803 | done < <(export LC_ALL=C.UTF-8; echo; find "$RESULTS_DIR" -type f -printf $'%T@ %p\n' | sort -n | cut -d' ' -f2- | xargs -r cat) | 811 | done < <(export LC_ALL=C.UTF-8; echo; find "$RESULTS_DIR" -type f -printf $'%T@ %p\n' | sort -n | cut -d' ' -f2- | xargs -r cat) |
| 804 | $FOUND || echo | 812 | $FOUND || echo |
| 805 | } | 813 | } |
| 806 | FUZZEL_RES=$(prev | fuzzel --dmenu --prompt "qalc> ") || exit $? | 814 | FUZZEL_RES=$(prev | fuzzel --dmenu --prompt "qalc> " --width=60) || exit $? |
| 807 | if [[ "$FUZZEL_RES" =~ .*\ =\ .* ]]; then | 815 | if [[ "$FUZZEL_RES" =~ .*\ =\ .* ]]; then |
| 808 | QALC_RES="$FUZZEL_RES" | 816 | QALC_RES="$FUZZEL_RES" |
| 809 | QALC_RET=0 | 817 | QALC_RET=0 |
| @@ -832,7 +840,7 @@ in { | |||
| 832 | name = "emoji-fuzzel"; | 840 | name = "emoji-fuzzel"; |
| 833 | runtimeInputs = with pkgs; [ config.programs.fuzzel.package wtype wl-clipboard-rs ]; | 841 | runtimeInputs = with pkgs; [ config.programs.fuzzel.package wtype wl-clipboard-rs ]; |
| 834 | text = '' | 842 | text = '' |
| 835 | FUZZEL_RES=$(fuzzel --dmenu --prompt "emoji> " <"$HOME"/.local/share/emoji-data/list.txt) || exit $? | 843 | FUZZEL_RES=$(fuzzel --dmenu --prompt "emoji> " --cache "$HOME"/.cache/fuzzel-emoji --width=60 <"$HOME"/.local/share/emoji-data/list.txt) || exit $? |
| 836 | [[ -n "$FUZZEL_RES" ]] || exit 1 | 844 | [[ -n "$FUZZEL_RES" ]] || exit 1 |
| 837 | wl-copy "$(cut -d ':' -f 1 <<<"$FUZZEL_RES" | tr -d '\n')" && wtype -k XF86Paste | 845 | wl-copy "$(cut -d ':' -f 1 <<<"$FUZZEL_RES" | tr -d '\n')" && wtype -k XF86Paste |
| 838 | ''; | 846 | ''; |
| @@ -973,6 +981,8 @@ in { | |||
| 973 | 981 | ||
| 974 | "Mod+D".action = with-urgent-window-action "{\"Action\":{\"FocusWindow\":{\"id\": .id}}}"; | 982 | "Mod+D".action = with-urgent-window-action "{\"Action\":{\"FocusWindow\":{\"id\": .id}}}"; |
| 975 | "Mod+Shift+D".action = with-focused-window-action "{\"Action\":{\"UnsetUrgent\":{\"id\": .id}}}"; | 983 | "Mod+Shift+D".action = with-focused-window-action "{\"Action\":{\"UnsetUrgent\":{\"id\": .id}}}"; |
| 984 | |||
| 985 | "Mod+K".action = spawn (lib.getExe' pkgs.worktime "worktime-ui"); | ||
| 976 | })) | 986 | })) |
| 977 | (map ({ name, selector, spawn, key, ...}: if key != null && selector != null && spawn != null then bind key { action = focus-or-spawn-action selector name spawn; } else null) cfg.scratchspaces) | 987 | (map ({ name, selector, spawn, key, ...}: if key != null && selector != null && spawn != null then bind key { action = focus-or-spawn-action selector name spawn; } else null) cfg.scratchspaces) |
| 978 | (map ({ name, moveKey, ...}: if moveKey != null then bind moveKey { action = kdl.magic-leaf "move-column-to-workspace" name; } else null) cfg.scratchspaces) | 988 | (map ({ name, moveKey, ...}: if moveKey != null then bind moveKey { action = kdl.magic-leaf "move-column-to-workspace" name; } else null) cfg.scratchspaces) |
diff --git a/overlays/worktime/pyproject.toml b/overlays/worktime/pyproject.toml index bb6934ac..ccc0ff62 100644 --- a/overlays/worktime/pyproject.toml +++ b/overlays/worktime/pyproject.toml | |||
| @@ -10,10 +10,12 @@ dependencies = [ | |||
| 10 | "tabulate>=0.9.0,<0.10", | 10 | "tabulate>=0.9.0,<0.10", |
| 11 | "toml>=0.10.2,<0.11", | 11 | "toml>=0.10.2,<0.11", |
| 12 | "jsonpickle>=4.0.5,<5", | 12 | "jsonpickle>=4.0.5,<5", |
| 13 | "frozendict>=2.4.6", | ||
| 13 | ] | 14 | ] |
| 14 | 15 | ||
| 15 | [project.scripts] | 16 | [project.scripts] |
| 16 | worktime = "worktime.__main__:main" | 17 | worktime = "worktime.__main__:main" |
| 18 | worktime-ui = "worktime.__main__:ui" | ||
| 17 | 19 | ||
| 18 | [build-system] | 20 | [build-system] |
| 19 | requires = ["hatchling"] | 21 | requires = ["hatchling"] |
diff --git a/overlays/worktime/uv.lock b/overlays/worktime/uv.lock index 8f91b44b..cffa0301 100644 --- a/overlays/worktime/uv.lock +++ b/overlays/worktime/uv.lock | |||
| @@ -47,6 +47,17 @@ wheels = [ | |||
| 47 | ] | 47 | ] |
| 48 | 48 | ||
| 49 | [[package]] | 49 | [[package]] |
| 50 | name = "frozendict" | ||
| 51 | version = "2.4.6" | ||
| 52 | source = { registry = "https://pypi.org/simple" } | ||
| 53 | sdist = { url = "https://files.pythonhosted.org/packages/bb/59/19eb300ba28e7547538bdf603f1c6c34793240a90e1a7b61b65d8517e35e/frozendict-2.4.6.tar.gz", hash = "sha256:df7cd16470fbd26fc4969a208efadc46319334eb97def1ddf48919b351192b8e", size = 316416, upload-time = "2024-10-13T12:15:32.449Z" } | ||
| 54 | wheels = [ | ||
| 55 | { url = "https://files.pythonhosted.org/packages/04/13/d9839089b900fa7b479cce495d62110cddc4bd5630a04d8469916c0e79c5/frozendict-2.4.6-py311-none-any.whl", hash = "sha256:d065db6a44db2e2375c23eac816f1a022feb2fa98cbb50df44a9e83700accbea", size = 16148, upload-time = "2024-10-13T12:15:26.839Z" }, | ||
| 56 | { url = "https://files.pythonhosted.org/packages/ba/d0/d482c39cee2ab2978a892558cf130681d4574ea208e162da8958b31e9250/frozendict-2.4.6-py312-none-any.whl", hash = "sha256:49344abe90fb75f0f9fdefe6d4ef6d4894e640fadab71f11009d52ad97f370b9", size = 16146, upload-time = "2024-10-13T12:15:28.16Z" }, | ||
| 57 | { url = "https://files.pythonhosted.org/packages/a5/8e/b6bf6a0de482d7d7d7a2aaac8fdc4a4d0bb24a809f5ddd422aa7060eb3d2/frozendict-2.4.6-py313-none-any.whl", hash = "sha256:7134a2bb95d4a16556bb5f2b9736dceb6ea848fa5b6f3f6c2d6dba93b44b4757", size = 16146, upload-time = "2024-10-13T12:15:29.495Z" }, | ||
| 58 | ] | ||
| 59 | |||
| 60 | [[package]] | ||
| 50 | name = "idna" | 61 | name = "idna" |
| 51 | version = "3.10" | 62 | version = "3.10" |
| 52 | source = { registry = "https://pypi.org/simple" } | 63 | source = { registry = "https://pypi.org/simple" } |
| @@ -150,6 +161,7 @@ name = "worktime" | |||
| 150 | version = "1.0.0" | 161 | version = "1.0.0" |
| 151 | source = { editable = "." } | 162 | source = { editable = "." } |
| 152 | dependencies = [ | 163 | dependencies = [ |
| 164 | { name = "frozendict" }, | ||
| 153 | { name = "jsonpickle" }, | 165 | { name = "jsonpickle" }, |
| 154 | { name = "python-dateutil" }, | 166 | { name = "python-dateutil" }, |
| 155 | { name = "pyxdg" }, | 167 | { name = "pyxdg" }, |
| @@ -161,6 +173,7 @@ dependencies = [ | |||
| 161 | 173 | ||
| 162 | [package.metadata] | 174 | [package.metadata] |
| 163 | requires-dist = [ | 175 | requires-dist = [ |
| 176 | { name = "frozendict", specifier = ">=2.4.6" }, | ||
| 164 | { name = "jsonpickle", specifier = ">=4.0.5,<5" }, | 177 | { name = "jsonpickle", specifier = ">=4.0.5,<5" }, |
| 165 | { name = "python-dateutil", specifier = ">=2.9.0.post0,<3" }, | 178 | { name = "python-dateutil", specifier = ">=2.9.0.post0,<3" }, |
| 166 | { name = "pyxdg", specifier = ">=0.28,<0.29" }, | 179 | { name = "pyxdg", specifier = ">=0.28,<0.29" }, |
diff --git a/overlays/worktime/worktime/__main__.py b/overlays/worktime/worktime/__main__.py index dbe7502e..0bad8ac8 100755 --- a/overlays/worktime/worktime/__main__.py +++ b/overlays/worktime/worktime/__main__.py | |||
| @@ -29,7 +29,7 @@ from sys import stderr, stdout | |||
| 29 | 29 | ||
| 30 | from tabulate import tabulate | 30 | from tabulate import tabulate |
| 31 | 31 | ||
| 32 | from itertools import groupby, count | 32 | from itertools import groupby, count, islice |
| 33 | from functools import cache, partial | 33 | from functools import cache, partial |
| 34 | 34 | ||
| 35 | from pathlib import Path | 35 | from pathlib import Path |
| @@ -42,6 +42,12 @@ import jsonpickle | |||
| 42 | from hashlib import blake2s | 42 | from hashlib import blake2s |
| 43 | import json | 43 | import json |
| 44 | 44 | ||
| 45 | import asyncio | ||
| 46 | |||
| 47 | from frozendict import frozendict | ||
| 48 | from contextlib import closing | ||
| 49 | import os | ||
| 50 | |||
| 45 | class BearerAuth(requests.auth.AuthBase): | 51 | class BearerAuth(requests.auth.AuthBase): |
| 46 | def __init__(self, token): | 52 | def __init__(self, token): |
| 47 | self.token = token | 53 | self.token = token |
| @@ -149,7 +155,7 @@ class KimaiAPI(object): | |||
| 149 | def get_billable_hours(self, start_date: datetime, end_date: datetime = datetime.now(timezone.utc)) -> timedelta: | 155 | def get_billable_hours(self, start_date: datetime, end_date: datetime = datetime.now(timezone.utc)) -> timedelta: |
| 150 | return sum(self.entry_durations(start_date, end_date=end_date), start=timedelta(milliseconds=0)) | 156 | return sum(self.entry_durations(start_date, end_date=end_date), start=timedelta(milliseconds=0)) |
| 151 | 157 | ||
| 152 | def get_running_clock(self, now: datetime = datetime.now(timezone.utc)) -> timedelta | None: | 158 | def get_running_entry(self) -> Any | None: |
| 153 | kimai_entries = self._session.get('/api/timesheets/active').json() | 159 | kimai_entries = self._session.get('/api/timesheets/active').json() |
| 154 | if not kimai_entries: | 160 | if not kimai_entries: |
| 155 | return None | 161 | return None |
| @@ -158,8 +164,44 @@ class KimaiAPI(object): | |||
| 158 | if entry['project']['customer']['id'] not in self._client_ids: | 164 | if entry['project']['customer']['id'] not in self._client_ids: |
| 159 | return None | 165 | return None |
| 160 | 166 | ||
| 167 | return entry | ||
| 168 | |||
| 169 | def get_running_clock(self, now: datetime = datetime.now(timezone.utc)) -> timedelta | None: | ||
| 170 | entry = self.get_running_entry() | ||
| 171 | if not entry: | ||
| 172 | return None | ||
| 161 | start = isoparse(entry['begin']) | 173 | start = isoparse(entry['begin']) |
| 162 | return now - start if start <= now else None | 174 | return now - start if start <= now else None, entry |
| 175 | |||
| 176 | def get_recent_entries(self) -> Generator[Any]: | ||
| 177 | step = timedelta(days = 7) | ||
| 178 | now = datetime.now().astimezone(timezone.utc) | ||
| 179 | ids = set() | ||
| 180 | for req_end in (now - step * i for i in count()): | ||
| 181 | params = { | ||
| 182 | 'begin': self.render_datetime(req_end - step), | ||
| 183 | 'end': self.render_datetime(req_end), | ||
| 184 | 'full': 'true', | ||
| 185 | } | ||
| 186 | for entry in self.get_timesheets(params): | ||
| 187 | if entry['id'] in ids: | ||
| 188 | continue | ||
| 189 | ids.add(entry['id']) | ||
| 190 | yield entry | ||
| 191 | |||
| 192 | def start_clock(self, project_id: int, activity_id: int, description: str | None = None, tags: Iterable[str] | None = None, billable: bool = True): | ||
| 193 | self._session.post('/api/timesheets', json={ | ||
| 194 | 'begin': self.render_datetime(datetime.now()), | ||
| 195 | 'project': project_id, | ||
| 196 | 'activity': activity_id, | ||
| 197 | 'description': description if description else '', | ||
| 198 | 'tags': (','.join(tags)) if tags else '', | ||
| 199 | 'billable': billable, | ||
| 200 | }).raise_for_status() | ||
| 201 | |||
| 202 | def stop_clock(self, running_id: int): | ||
| 203 | self._session.patch(f'/api/timesheets/{running_id}/stop').raise_for_status() | ||
| 204 | |||
| 163 | 205 | ||
| 164 | class Worktime(object): | 206 | class Worktime(object): |
| 165 | time_worked = timedelta() | 207 | time_worked = timedelta() |
| @@ -874,5 +916,116 @@ def main(): | |||
| 874 | 916 | ||
| 875 | args.cmd(**vars(args)) | 917 | args.cmd(**vars(args)) |
| 876 | 918 | ||
| 919 | async def ui_update_options(api, cache_path): | ||
| 920 | options = set() | ||
| 921 | sort_order = dict() | ||
| 922 | entry_iter = enumerate(api.get_recent_entries()) | ||
| 923 | loop = asyncio.get_event_loop() | ||
| 924 | while item := await loop.run_in_executor(None, next, entry_iter): | ||
| 925 | ix, entry = item | ||
| 926 | if len(options) >= 20 or ix >= 100: | ||
| 927 | break | ||
| 928 | |||
| 929 | option = frozendict({ | ||
| 930 | 'tags': frozenset(entry['tags']), | ||
| 931 | 'activity': frozendict({'id': entry['activity']['id'], 'name': entry['activity']['name']}), | ||
| 932 | 'project': frozendict({'id': entry['project']['id'], 'customer': entry['project']['customer']['name'], 'name': entry['project']['name']}), | ||
| 933 | 'description': entry['description'] if entry['description'] else None, | ||
| 934 | 'billable': entry['billable'], | ||
| 935 | }) | ||
| 936 | sort_value = isoparse(entry['begin']) | ||
| 937 | if option in sort_order: | ||
| 938 | sort_value = max(sort_value, sort_order[option]) | ||
| 939 | sort_order[option] = sort_value | ||
| 940 | options.add(option) | ||
| 941 | |||
| 942 | options = list(sorted(options, key = lambda o: sort_order[o], reverse = True)) | ||
| 943 | |||
| 944 | with cache_path.open('w', encoding='utf-8') as ch: | ||
| 945 | ch.write(jsonpickle.encode(options)) | ||
| 946 | |||
| 947 | return options | ||
| 948 | |||
| 949 | def ui_render_option(option): | ||
| 950 | res = '' | ||
| 951 | if option['description']: | ||
| 952 | res += '„{}“, '.format(option['description']) | ||
| 953 | res += option['activity']['name'] + ', ' | ||
| 954 | res += option['project']['name'] | ||
| 955 | if option['project']['customer'] not in option['project']['name']: | ||
| 956 | res += ' ({})'.format(option['project']['customer']) | ||
| 957 | if option['tags']: | ||
| 958 | res += ', {}'.format(' '.join(map(lambda t: '#{}'.format(t), option['tags']))) | ||
| 959 | if not option['billable']: | ||
| 960 | res += ', not billable' | ||
| 961 | return res | ||
| 962 | |||
| 963 | async def ui_main(): | ||
| 964 | cache_path = Path(BaseDirectory.save_cache_path('worktime-ui')) / 'options.json' | ||
| 965 | options = None | ||
| 966 | try: | ||
| 967 | with cache_path.open('r', encoding='utf-8') as ch: | ||
| 968 | options = jsonpickle.decode(ch.read()) | ||
| 969 | except FileNotFoundError: | ||
| 970 | pass | ||
| 971 | |||
| 972 | config = Worktime.config() | ||
| 973 | api = KimaiAPI( | ||
| 974 | base_url=config.get("KIMAI", {}).get("BaseUrl", None), | ||
| 975 | api_token=config.get("KIMAI", {}).get("ApiToken", None), | ||
| 976 | clients=config.get("KIMAI", {}).get("Clients", None) | ||
| 977 | ) | ||
| 978 | running_entry = api.get_running_entry() | ||
| 979 | |||
| 980 | async with asyncio.TaskGroup() as tg: | ||
| 981 | update_options = tg.create_task(ui_update_options(api, cache_path)) | ||
| 982 | if not options: | ||
| 983 | options = await update_options | ||
| 984 | |||
| 985 | read_fd, write_fd = os.pipe() | ||
| 986 | w_pipe = open(write_fd, 'wb', 0) | ||
| 987 | loop = asyncio.get_event_loop() | ||
| 988 | w_transport, _ = await loop.connect_write_pipe( | ||
| 989 | asyncio.Protocol, | ||
| 990 | w_pipe, | ||
| 991 | ) | ||
| 992 | r_pipe = open(read_fd, 'rb', 0) | ||
| 993 | |||
| 994 | proc = await asyncio.create_subprocess_exec( | ||
| 995 | "fuzzel", "--dmenu", "--index", "--width=60", | ||
| 996 | stdout = asyncio.subprocess.PIPE, | ||
| 997 | stdin = r_pipe, | ||
| 998 | ) | ||
| 999 | |||
| 1000 | with closing(w_transport) as t: | ||
| 1001 | if running_entry: | ||
| 1002 | t.write(b'Stop running timesheet\n') | ||
| 1003 | for option in options: | ||
| 1004 | t.write(ui_render_option(option).encode('utf-8') + b'\n') | ||
| 1005 | |||
| 1006 | stdout, _ = await proc.communicate() | ||
| 1007 | if proc.returncode != 0: | ||
| 1008 | return | ||
| 1009 | fuzzel_out = int(stdout.decode('utf-8')) | ||
| 1010 | if fuzzel_out < 0 or fuzzel_out >= len(options): | ||
| 1011 | return | ||
| 1012 | elif running_entry and fuzzel_out == 0: | ||
| 1013 | api.stop_clock(running_entry['id']) | ||
| 1014 | else: | ||
| 1015 | if running_entry: | ||
| 1016 | fuzzel_out -= 1 | ||
| 1017 | option = options[fuzzel_out] | ||
| 1018 | api.start_clock( | ||
| 1019 | project_id = option['project']['id'], | ||
| 1020 | activity_id = option['activity']['id'], | ||
| 1021 | description = option['description'], | ||
| 1022 | tags = option['tags'], | ||
| 1023 | billable = option['billable'], | ||
| 1024 | ) | ||
| 1025 | |||
| 1026 | |||
| 1027 | def ui(): | ||
| 1028 | asyncio.run(ui_main()) | ||
| 1029 | |||
| 877 | if __name__ == "__main__": | 1030 | if __name__ == "__main__": |
| 878 | sys.exit(main()) | 1031 | sys.exit(main()) |
