Skip to content

Commit

Permalink
Merge branch 'main' into share-magic-wormhole
Browse files Browse the repository at this point in the history
  • Loading branch information
Mustaballer authored Jun 16, 2023
2 parents b4b7879 + 0cd5c54 commit 395fa00
Show file tree
Hide file tree
Showing 23 changed files with 830 additions and 40 deletions.
42 changes: 42 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: Python CI

on:
push:
branches:
- '**'

jobs:
run-ci:
runs-on: windows-latest

steps:
- name: Checkout code
uses: actions/checkout@v3

- name: Set up Python
uses: actions/setup-python@v3
with:
python-version: '3.10'

- name: Install dependencies
run: |
pip install wheel
pip install -r requirements.txt
pip install -e .
- name: Check formatting with Black
run: |
black --check --exclude "(src|alembic)/" .
- name: Run headless tests
uses: coactions/setup-xvfb@v1
with:
run: python -m pytest
working-directory: ./ # Optional: Specify the working directory if needed
options: # Optional: Add any additional options or arguments for pytest

- name: flake8 Lint
uses: py-actions/flake8@v2
with:
plugins: "flake8-docstrings"
extra-args: "--docstring-convention=google --exclude=alembic/versions"
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ cache
.VSCode
.vsCode

#Idea
.idea

# Generated performance charts
performance

Expand Down
3 changes: 3 additions & 0 deletions openadapt/app/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from openadapt.app.main import run_app

run_app()
Binary file added openadapt/app/assets/logo.ico
Binary file not shown.
Binary file added openadapt/app/assets/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
30 changes: 30 additions & 0 deletions openadapt/app/build.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import os
import subprocess
from pathlib import Path

import nicegui

spec = [
"pyi-makespec",
f"{Path(__file__).parent}/main.py",
f"--icon={Path(__file__).parent}/assets/logo.ico",
"--name",
"OpenAdapt", # name
# "--onefile", # trade startup speed for smaller file size
"--onedir",
"--windowed", # prevent console appearing, only use with ui.run(native=True, ...)
"--add-data",
f"{Path(nicegui.__file__).parent}{os.pathsep}nicegui",
]

subprocess.call(spec)

# add import sys ; sys.setrecursionlimit(sys.getrecursionlimit() * 5) to line 2 of OpenAdapt.spec
with open("OpenAdapt.spec", "r+") as f:
lines = f.readlines()
lines[1] = "import sys ; sys.setrecursionlimit(sys.getrecursionlimit() * 5)\n"
f.seek(0)
f.truncate()
f.writelines(lines)

subprocess.call(["pyinstaller", "OpenAdapt.spec"])
88 changes: 88 additions & 0 deletions openadapt/app/cards.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import signal
from nicegui import ui
from subprocess import Popen
from openadapt.app.objects.local_file_picker import LocalFilePicker
from openadapt.app.util import set_dark, sync_switch

PROC = None


def settings(dark_mode):
with ui.dialog() as settings, ui.card():
s = ui.switch("Dark mode", on_change=lambda: set_dark(dark_mode, s.value))
sync_switch(s, dark_mode)
ui.button("Close", on_click=lambda: settings.close())

settings.open()


def select_import(f):
async def pick_file():
result = await LocalFilePicker(".")
ui.notify(f"Selected {result[0]}" if result else "No file selected.")
selected_file.text = result[0] if result else ""
import_button.enabled = True if result else False

with ui.dialog() as import_dialog, ui.card():
with ui.column():
ui.button("Select File", on_click=pick_file).props("icon=folder")
selected_file = ui.label("")
selected_file.visible = False
import_button = ui.button(
"Import", on_click=lambda: f(selected_file.text, delete.value)
)
import_button.enabled = False
delete = ui.checkbox("Delete file after import")

import_dialog.open()


def recording_prompt(options, record_button):
if PROC is None:
with ui.dialog() as dialog, ui.card():
ui.label("Enter a name for the recording: ")
ui.input(
label="Name",
placeholder="test",
autocomplete=options,
on_change=lambda e: result.set_text(e),
)
result = ui.label()

with ui.row():
ui.button("Close", on_click=dialog.close)
ui.button("Enter", on_click=lambda: on_record())

dialog.open()

def terminate():
global PROC
PROC.send_signal(signal.SIGINT)

# wait for process to terminate
PROC.wait()
ui.notify("Stopped recording")
record_button._props["name"] = "radio_button_checked"
record_button.on("click", lambda: recording_prompt(options, record_button))

PROC = None

def begin():
name = result.text.__getattribute__("value")

ui.notify(
f"Recording {name}... Press CTRL + C in terminal window to cancel",
)
PROC = Popen(
"python3 -m openadapt.record " + name,
shell=True,
)
record_button._props["name"] = "stop"
record_button.on("click", lambda: terminate())
record_button.update()
return PROC

def on_record():
global PROC
dialog.close()
PROC = begin()
82 changes: 82 additions & 0 deletions openadapt/app/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import threading
import base64
import os

from nicegui import app, ui

from openadapt import replay, visualize
from openadapt.app.cards import recording_prompt, select_import, settings
from openadapt.app.util import clear_db, on_export, on_import
from openadapt.app.objects.console import Console

SERVER = "127.0.0.1:8000/upload"


def run_app():
file = os.path.dirname(__file__)
app.native.window_args["resizable"] = False # too many issues with resizing
app.native.start_args["debug"] = False

dark = ui.dark_mode()
logger = None

# Add logo
# right align icon
with ui.row().classes("w-full justify-right"):
# settings

# alignment trick
with ui.avatar(color="white" if dark else "black", size=128):
logo_base64 = base64.b64encode(open(f"{file}/assets/logo.png", "rb").read())
img = bytes(
f"data:image/png;base64,{(logo_base64.decode('utf-8'))}",
encoding="utf-8",
)
ui.image(img.decode("utf-8"))
ui.icon("settings").tooltip("Settings").on("click", lambda: settings(dark))
ui.icon("delete").on("click", lambda: clear_db(log=logger)).tooltip(
"Clear all recorded data"
)
ui.icon("upload").tooltip("Export Data").on("click", lambda: on_export(SERVER))
ui.icon("download").tooltip("Import Data").on(
"click", lambda: select_import(on_import)
)
ui.icon("share").tooltip("Share").on(
"click", lambda: (_ for _ in ()).throw(Exception(NotImplementedError))
)

# Recording description autocomplete
options = ["test"]

with ui.splitter(value=20) as splitter:
splitter.classes("w-full h-full")
with splitter.before:
with ui.column().classes("w-full h-full"):
record_button = (
ui.icon("radio_button_checked", size="64px")
.on("click", lambda: recording_prompt(options, record_button))
.tooltip("Record a new replay / Stop recording")
)
ui.icon("visibility", size="64px").on(
"click", lambda: threading.Thread(target=visualize.main).start()
).tooltip("Visualize the latest replay")

ui.icon("play_arrow", size="64px").on(
"click", lambda: replay.replay("NaiveReplayStrategy")
).tooltip("Play the latest replay")
with splitter.after:
logger = Console()
logger.log.style("height: 250px;, width: 300px;")
splitter.enabled = False

ui.run(
title="OpenAdapt Client",
native=True,
window_size=(400, 400),
fullscreen=False,
reload=False,
)


if __name__ == "__main__":
run_app()
21 changes: 21 additions & 0 deletions openadapt/app/objects/console.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import sys

from nicegui import ui


class Console(object):
def __init__(self):
self.log = ui.log().classes("w-full h-20")
self.old_stderr = sys.stderr
sys.stderr = self

def write(self, data):
self.log.push(data[:-1])
self.log.update()

def flush(self):
self.log.update()

def reset(self):
self.log.clear()
sys.stderr = self.old_stderr
108 changes: 108 additions & 0 deletions openadapt/app/objects/local_file_picker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# retrieved from https://github.com/zauberzeug/nicegui/tree/main/examples/local_file_picker

from pathlib import Path
from typing import Dict, Optional

from nicegui import ui


class LocalFilePicker(ui.dialog):
def __init__(
self,
directory: str,
*,
upper_limit: Optional[str] = ...,
multiple: bool = False,
show_hidden_files: bool = False,
dark_mode: bool = False,
) -> None:
"""Local File Picker
This is a simple file picker that allows you to select a file from the local filesystem where NiceGUI is running.
:param directory: The directory to start in.
:param upper_limit: The directory to stop at (None: no limit, default: same as the starting directory).
:param multiple: Whether to allow multiple files to be selected.
:param show_hidden_files: Whether to show hidden files.
"""
super().__init__()

self.path = Path(directory).expanduser()
if upper_limit is None:
self.upper_limit = None
else:
self.upper_limit = Path(
directory if upper_limit == ... else upper_limit
).expanduser()
self.show_hidden_files = show_hidden_files

with self, ui.card():
self.grid = (
ui.aggrid(
{
"columnDefs": [{"field": "name", "headerName": "File"}],
"rowSelection": "multiple" if multiple else "single",
},
html_columns=[0],
)
.classes("w-96")
.on("cellDoubleClicked", self.handle_double_click)
)
with ui.row().classes("w-full justify-end"):
ui.button("Cancel", on_click=self.close).props("outline")
ui.button("Ok", on_click=self._handle_ok)
self.update_grid()

def update_grid(self) -> None:
paths = list(self.path.glob("*"))
if not self.show_hidden_files:
paths = [p for p in paths if not p.name.startswith(".")]
paths.sort(key=lambda p: p.name.lower())
paths.sort(key=lambda p: not p.is_dir())

self.grid.options["rowData"] = [
{
"name": f"📁 <strong>{p.name}</strong>" if p.is_dir() else p.name,
"path": str(p),
}
for p in paths
]
if (
self.upper_limit is None
and self.path != self.path.parent
or self.upper_limit is not None
and self.path != self.upper_limit
):
self.grid.options["rowData"].insert(
0,
{
"name": "📁 <strong>..</strong>",
"path": str(self.path.parent),
},
)

self.grid.update()

async def handle_double_click(self, msg: Dict) -> None:
self.path = Path(msg["args"]["data"]["path"])
if self.path.is_dir():
self.update_grid()
else:
self.submit([str(self.path)])

async def _handle_ok(self):
rows = await ui.run_javascript(
f"getElement({self.grid.id}).gridOptions.api.getSelectedRows()"
)
self.submit([r["path"] for r in rows])


async def pick_file():
result = await LocalFilePicker("~", multiple=True)
ui.notify(f"You chose {result}")


if __name__ in {"__main__", "__mp_main__"}:
ui.button("Choose file", on_click=pick_file).props("icon=folder")

ui.run(native=True)
Loading

0 comments on commit 395fa00

Please sign in to comment.