├── .gitignore ├── .vscode ├── launch.json └── settings.json ├── app ├── electron │ ├── .gitignore │ ├── base-requirements.txt │ ├── forge.config.js │ ├── icon.icns │ ├── package.json │ ├── scripts │ │ ├── generate-html.js │ │ └── package.json │ ├── src │ │ ├── file-manager.ts │ │ ├── host │ │ │ ├── app-backend.ts │ │ │ ├── index.ts │ │ │ ├── navigation.ts │ │ │ └── renderer.tsx │ │ ├── index.ts │ │ ├── interfaces.ts │ │ ├── logger.ts │ │ ├── shared │ │ │ ├── context-menu.tsx │ │ │ ├── preload.ts │ │ │ └── renderer-types.d.ts │ │ ├── startup │ │ │ ├── host-creator.tsx │ │ │ ├── index.ts │ │ │ ├── renderer.tsx │ │ │ └── steps │ │ │ │ ├── s0.tsx │ │ │ │ ├── s1.tsx │ │ │ │ ├── s2.tsx │ │ │ │ ├── s4.tsx │ │ │ │ ├── s5.tsx │ │ │ │ ├── s6.tsx │ │ │ │ └── s7.tsx │ │ ├── types.ts │ │ └── util.ts │ ├── tsconfig.app.json │ ├── tsconfig.base.json │ ├── tsconfig.json │ ├── tsconfig.preload.json │ ├── tsconfig.renderer.json │ └── tsconfig.types.json ├── library │ ├── .gitignore │ ├── package.json │ ├── src │ │ ├── host.ts │ │ ├── index.ts │ │ ├── scan.ts │ │ ├── search.ts │ │ ├── socket-client.ts │ │ ├── types │ │ │ └── app-data.ts │ │ ├── util.ts │ │ └── util │ │ │ └── fs.ts │ └── tsconfig.json ├── server │ ├── .gitignore │ ├── pr1_server │ │ ├── __init__.py │ │ ├── __main__.py │ │ ├── agent.py │ │ ├── auth.py │ │ ├── bridges │ │ │ ├── __init__.py │ │ │ ├── protocol.py │ │ │ ├── socket.py │ │ │ ├── stdio.py │ │ │ └── websocket.py │ │ ├── certificate.py │ │ ├── conf.py │ │ ├── session.py │ │ ├── static.py │ │ ├── trash.py │ │ └── util.py │ └── pyproject.toml ├── shared │ ├── .gitignore │ ├── package.json │ ├── src │ │ ├── client-utils.ts │ │ ├── client.ts │ │ ├── defer.ts │ │ ├── error.ts │ │ ├── index.ts │ │ ├── lock.ts │ │ ├── misc.ts │ │ ├── term.ts │ │ └── types │ │ │ ├── communication.ts │ │ │ ├── compilation.ts │ │ │ ├── diagnostic.ts │ │ │ ├── draft.ts │ │ │ ├── effect.ts │ │ │ ├── experiment.ts │ │ │ ├── host.ts │ │ │ ├── master.ts │ │ │ ├── plugin.ts │ │ │ ├── protocol.ts │ │ │ ├── rich-text.ts │ │ │ ├── unit.ts │ │ │ └── util.ts │ └── tsconfig.json ├── standalone-client │ ├── .gitignore │ ├── index.html │ ├── index.js │ ├── package.json │ └── service-worker.js └── vscode-extension │ ├── .gitignore │ ├── language-configuration.json │ ├── package.json │ ├── src │ ├── index.ts │ └── util.ts │ ├── syntaxes │ └── prl.tmLanguage.json │ └── tsconfig.json ├── client ├── .gitignore ├── .madgerc ├── .npmignore ├── package.json ├── scripts │ ├── build.js │ ├── external.js │ ├── generate-html.js │ ├── libraries │ │ ├── react-dom-client.js │ │ ├── react-dom.js │ │ ├── react-jsx-runtime.js │ │ └── react.js │ └── package.json ├── service-worker.js ├── src │ ├── analysis.ts │ ├── app-backends │ │ ├── base.ts │ │ └── browser.ts │ ├── application.tsx │ ├── backends │ │ └── common.ts │ ├── browser-app.tsx │ ├── components │ │ ├── bar-nav.tsx │ │ ├── blank-state.tsx │ │ ├── block-inspector.tsx │ │ ├── button.tsx │ │ ├── chip-protocol.tsx │ │ ├── chip-settings.tsx │ │ ├── context-menu-area.tsx │ │ ├── context-menu.tsx │ │ ├── description.tsx │ │ ├── diagram.tsx │ │ ├── document-editor.tsx │ │ ├── draft-overview.tsx │ │ ├── draft-summary.tsx │ │ ├── error-boundary.tsx │ │ ├── execution-inspector.tsx │ │ ├── expandable-text.tsx │ │ ├── expression.tsx │ │ ├── features.tsx │ │ ├── file-tab-nav.tsx │ │ ├── graph-editor.tsx │ │ ├── icon.tsx │ │ ├── info-bar.tsx │ │ ├── item-list.tsx │ │ ├── large-icon.tsx │ │ ├── modal.tsx │ │ ├── modals │ │ │ ├── edit-protocol.tsx │ │ │ ├── start-protocol.tsx │ │ │ └── unsaved-document.tsx │ │ ├── overflowable-text.tsx │ │ ├── possible-link.tsx │ │ ├── progress-bar.tsx │ │ ├── protocol-overview.tsx │ │ ├── protocol-timeline.tsx │ │ ├── report-inspector.tsx │ │ ├── report-panel.tsx │ │ ├── report.tsx │ │ ├── selector.tsx │ │ ├── shadow-scrollable.tsx │ │ ├── shortcut-guide.tsx │ │ ├── sidebar.tsx │ │ ├── split-panels.tsx │ │ ├── standard-form.tsx │ │ ├── static-select.tsx │ │ ├── tab-nav.tsx │ │ ├── text-editor.tsx │ │ ├── time-sensitive.tsx │ │ ├── timed-progress-bar.tsx │ │ ├── title-bar.tsx │ │ └── visual-editor.tsx │ ├── constants.ts │ ├── contexts.ts │ ├── draft.ts │ ├── dynamic-value.tsx │ ├── format.ts │ ├── geometry.ts │ ├── global-interfaces.d.ts │ ├── host.ts │ ├── index.tsx │ ├── interfaces │ │ ├── draft.ts │ │ ├── feature.ts │ │ ├── graph.ts │ │ ├── host.ts │ │ ├── misc.ts │ │ ├── plugin.ts │ │ ├── protocol.ts │ │ ├── unit.ts │ │ └── view.ts │ ├── language-service.ts │ ├── libraries │ │ └── panel.tsx │ ├── plugin.ts │ ├── process.tsx │ ├── protocol.ts │ ├── rich-text.tsx │ ├── serialize-immutable.ts │ ├── shortcuts.ts │ ├── snapshot.ts │ ├── sort.ts │ ├── startup.tsx │ ├── store │ │ ├── application.tsx │ │ ├── base.ts │ │ ├── browser-storage.ts │ │ ├── idb.ts │ │ ├── memory-store.ts │ │ ├── store-manager.ts │ │ └── types.ts │ ├── term.ts │ ├── types.d.ts │ ├── unit.ts │ ├── units │ │ └── index.ts │ ├── ureg.ts │ ├── util.ts │ ├── views │ │ ├── chip.tsx │ │ ├── conf.tsx │ │ ├── draft.tsx │ │ ├── execution.tsx │ │ ├── experiment-wrapper.tsx │ │ ├── experiment.tsx │ │ ├── experiments.tsx │ │ ├── graph.tsx │ │ ├── plugin-view.tsx │ │ ├── protocols.tsx │ │ └── test │ │ │ └── design.tsx │ └── websocket.ts ├── static │ └── logo.jpeg ├── styles │ ├── _constants.scss │ ├── _mcontrol.scss │ ├── _mixins.scss │ ├── _protocol.scss │ ├── _reset.scss │ ├── _run.scss │ ├── _sidebar.scss │ ├── _startup.scss │ ├── common │ │ ├── _buttons.scss │ │ ├── _header.scss │ │ ├── _misc.scss │ │ └── _mixins.scss │ ├── components │ │ ├── _barnav.scss │ │ ├── _chip-list.scss │ │ ├── _context-menu.scss │ │ ├── _protocol-config.scss │ │ ├── _protocol-overview.scss │ │ ├── _protocol-status.scss │ │ ├── _protocol-timeline.scss │ │ ├── _selector.scss │ │ ├── _standard-form.scss │ │ ├── _text-editor.scss │ │ ├── _visual-editor.scss │ │ ├── application.module.scss │ │ ├── description.module.scss │ │ ├── diagnostics-report.module.scss │ │ ├── diagnostics-summary.module.scss │ │ ├── editor.module.scss │ │ ├── error-boundary.module.scss │ │ ├── expression.module.scss │ │ ├── features.module.scss │ │ ├── file-tab-nav.module.scss │ │ ├── form.module.scss │ │ ├── graph-editor.module.scss │ │ ├── info-bar.module.scss │ │ ├── modal.module.scss │ │ ├── progress-bar.module.scss │ │ ├── shadow-scrollable.module.scss │ │ ├── shortcut-guide.module.scss │ │ ├── sidebar.module.scss │ │ ├── split-panels.module.scss │ │ ├── spotlight.module.scss │ │ ├── static-select.module.scss │ │ ├── tab-nav.module.scss │ │ ├── text-editor.module.scss │ │ ├── title-bar.module.scss │ │ └── view.module.scss │ ├── libraries │ │ └── panel.module.scss │ ├── main.scss │ └── views │ │ ├── _protocols.scss │ │ └── conf.module.scss ├── superstatic.json └── tsconfig.json ├── host ├── .gitignore ├── automancer │ └── __init__.py ├── pr1 │ ├── __init__.py │ ├── analysis.py │ ├── devices │ │ ├── __init__.py │ │ ├── adapter.py │ │ ├── claim.py │ │ ├── node.py │ │ └── nodes │ │ │ ├── __init__.py │ │ │ ├── collection.py │ │ │ ├── common.py │ │ │ ├── numeric.py │ │ │ ├── primitive.py │ │ │ ├── readable.py │ │ │ ├── value.py │ │ │ └── watcher.py │ ├── document.py │ ├── draft.py │ ├── error.py │ ├── eta.py │ ├── experiment.py │ ├── fiber.py │ ├── fiber │ │ ├── __init__.py │ │ ├── binding.py │ │ ├── eval.py │ │ ├── expr.py │ │ ├── master.py │ │ ├── master2.py │ │ ├── parser.py │ │ ├── process.py │ │ ├── segment.py │ │ ├── staticanalysis.py │ │ ├── staticanalysis2.py │ │ ├── staticeval.py │ │ └── transparent.py │ ├── history.py │ ├── host.py │ ├── input │ │ ├── __init__.py │ │ ├── __main__.py │ │ ├── dynamic.py │ │ └── file.py │ ├── langservice.py │ ├── master.py │ ├── master │ │ ├── __init__.py │ │ └── analysis.py │ ├── plugin │ │ └── manager.py │ ├── procedure.py │ ├── protocol.py │ ├── reader.py │ ├── report.py │ ├── rich_text.py │ ├── state.py │ ├── staticanalysis │ │ ├── __init__.py │ │ ├── __main__.py │ │ ├── context.py │ │ ├── expr.py │ │ ├── expression.py │ │ ├── function.py │ │ ├── module.py │ │ ├── overloads.py │ │ ├── special.py │ │ ├── support.py │ │ ├── type.py │ │ └── types.py │ ├── test.py │ ├── units │ │ ├── __init__.py │ │ └── base.py │ ├── ureg.py │ └── util │ │ ├── __init__.py │ │ ├── asyncio.py │ │ ├── batch.py │ │ ├── blob.py │ │ ├── decorators.py │ │ ├── encoder.py │ │ ├── iterators.py │ │ ├── misc.py │ │ ├── parser.py │ │ ├── pool.py │ │ ├── ref.py │ │ ├── schema.py │ │ └── types.py ├── pyproject.toml └── test.yml ├── pyrightconfig.json ├── readme.md ├── scripts └── build.sh ├── shared └── tsconfig.json └── units ├── adaptyv_nikon ├── .gitignore ├── client │ ├── package.json │ ├── src │ │ └── index.tsx │ └── tsconfig.json ├── pyproject.toml ├── readme.md └── src │ └── pr1_adaptyv_nikon │ ├── __init__.py │ ├── client │ └── __init__.py │ ├── executor.py │ ├── macros │ ├── __init__.py │ ├── capture.mac │ ├── inspect.mac │ └── query.mac │ ├── parser.py │ ├── process.py │ └── runner.py ├── amf ├── .gitignore ├── client │ ├── package.json │ ├── src │ │ ├── index.ts │ │ └── types.d.ts │ ├── styles │ │ └── index.scss │ └── tsconfig.json ├── pyproject.toml └── src │ └── pr1_amf │ ├── __init__.py │ ├── client │ └── __init__.py │ ├── data │ ├── __init__.py │ └── logo.png │ ├── device.py │ └── executor.py ├── core ├── .gitignore ├── client │ ├── build.mjs │ ├── package.json │ ├── src │ │ ├── devices │ │ │ ├── blocks │ │ │ │ ├── applier.ts │ │ │ │ └── publisher.ts │ │ │ ├── components │ │ │ │ ├── node-detail.tsx │ │ │ │ ├── node-hierarchy.module.scss │ │ │ │ └── node-hierarchy.tsx │ │ │ ├── index.tsx │ │ │ ├── styles.module.scss │ │ │ ├── types.ts │ │ │ ├── util.ts │ │ │ └── view.tsx │ │ ├── name │ │ │ └── index.tsx │ │ ├── repeat │ │ │ └── index.tsx │ │ ├── segment │ │ │ └── index.tsx │ │ ├── sequence │ │ │ └── index.tsx │ │ ├── shorthands │ │ │ └── index.tsx │ │ ├── state │ │ │ └── index.tsx │ │ └── timer │ │ │ └── index.tsx │ └── tsconfig.json ├── pyproject.toml └── src │ ├── pr1_devices │ ├── __init__.py │ ├── client │ ├── executor.py │ ├── mock.py │ ├── parser.py │ ├── program.py │ └── runner.py │ ├── pr1_do │ ├── __init__.py │ └── parser.py │ ├── pr1_name │ ├── __init__.py │ ├── client │ └── parser.py │ ├── pr1_repeat │ ├── __init__.py │ ├── client │ ├── parser.py │ └── program.py │ ├── pr1_segment │ ├── __init__.py │ └── client │ ├── pr1_sequence │ ├── __init__.py │ ├── client │ ├── parser.py │ └── program.py │ ├── pr1_shorthands │ ├── __init__.py │ ├── client │ └── parser.py │ ├── pr1_state │ ├── __init__.py │ ├── client │ ├── parser.py │ └── parser2.py │ └── pr1_timer │ ├── __init__.py │ ├── client │ ├── parser.py │ └── process.py ├── debug ├── .gitignore ├── client │ ├── package.json │ ├── src │ │ └── index.ts │ └── tsconfig.json ├── pyproject.toml └── src │ └── pr1_debug │ ├── __init__.py │ ├── client │ └── __init__.py │ └── executor.py ├── expect ├── .gitignore ├── client │ ├── package.json │ ├── src │ │ └── index.tsx │ └── tsconfig.json ├── pyproject.toml ├── setup.py └── src │ └── pr1_expect │ ├── __init__.py │ ├── client │ └── __init__.py │ ├── parser.py │ └── runner.py ├── mfcontrol ├── .gitignore ├── client │ ├── package.json │ ├── src │ │ ├── code-editor.tsx │ │ ├── diagram.tsx │ │ ├── index.ts │ │ ├── manual-control.tsx │ │ ├── matrix-editor.tsx │ │ ├── types.d.ts │ │ └── util.ts │ ├── styles │ │ └── index.scss │ └── tsconfig.json ├── pyproject.toml ├── setup.py └── src │ └── pr1_mfcontrol │ ├── __init__.py │ ├── client │ └── __init__.py │ ├── data │ ├── __init__.py │ └── repr.json │ ├── executor.py │ ├── model.py │ ├── parser.py │ └── runner.py ├── numato ├── .gitignore ├── pyproject.toml ├── setup.py └── src │ └── pr1_numato │ ├── __init__.py │ ├── data │ ├── __init__.py │ └── logo.svg │ ├── devices │ ├── __init__.py │ ├── numato.py │ └── relay_board.py │ └── executor.py ├── okolab ├── .gitignore ├── pyproject.toml └── src │ └── pr1_okolab │ ├── __init__.py │ ├── device.py │ └── executor.py ├── opcua ├── .gitignore ├── client │ ├── package.json │ ├── src │ │ └── index.tsx │ ├── styles │ │ └── index.scss │ └── tsconfig.json ├── pyproject.toml └── src │ └── pr1_opcua │ ├── __init__.py │ ├── client │ └── __init__.py │ ├── device.py │ └── executor.py ├── record ├── .gitignore ├── client │ ├── package.json │ ├── src │ │ └── index.tsx │ └── tsconfig.json ├── pyproject.toml └── src │ └── pr1_record │ ├── __init__.py │ ├── client │ └── __init__.py │ ├── parser.py │ └── program.py ├── s3 ├── .gitignore ├── client │ ├── package.json │ ├── src │ │ └── index.tsx │ └── tsconfig.json ├── pyproject.toml └── src │ └── pr1_s3 │ ├── __init__.py │ ├── client │ └── __init__.py │ ├── parser.py │ └── process.py ├── simple ├── .gitignore ├── pyproject.toml ├── setup.py └── src │ └── pr1_simple │ ├── __init__.py │ └── client │ ├── __init__.py │ ├── index.css │ └── index.js ├── slack ├── .gitignore ├── client │ ├── package.json │ ├── src │ │ └── index.tsx │ └── tsconfig.json ├── pyproject.toml └── src │ └── pr1_slack │ ├── __init__.py │ ├── client │ └── __init__.py │ ├── parser.py │ └── process.py ├── template ├── .gitignore ├── client │ ├── package.json │ ├── src │ │ ├── index.ts │ │ └── types.d.ts │ ├── styles │ │ └── index.scss │ └── tsconfig.json ├── pyproject.toml ├── setup.py └── src │ └── pr1_template │ ├── __init__.py │ └── client │ └── __init__.py ├── tsconfig.json └── utils ├── .gitignore ├── client ├── package.json ├── src │ └── index.tsx └── tsconfig.json ├── pyproject.toml ├── setup.py └── src └── pr1_utils ├── __init__.py ├── client └── __init__.py ├── executor.py ├── parser.py └── runner.py /.gitignore: -------------------------------------------------------------------------------- 1 | *copy* 2 | tmp 3 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Extension", 6 | "type": "extensionHost", 7 | "request": "launch", 8 | "args": [ 9 | "--extensionDevelopmentPath=${workspaceFolder}/app/vscode-extension" 10 | ] 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.analysis.extraPaths": [ 3 | "./host" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /app/electron/.gitignore: -------------------------------------------------------------------------------- 1 | lib 2 | node_modules 3 | out 4 | package-lock.json 5 | packages 6 | tmp 7 | -------------------------------------------------------------------------------- /app/electron/base-requirements.txt: -------------------------------------------------------------------------------- 1 | --extra-index-url https://gitlab.com/api/v4/projects/45232449/packages/pypi/simple 2 | 3 | pip-tools~=6.13.0 4 | 5 | pr1 6 | pr1-server 7 | 8 | pr1-amf 9 | pr1-core 10 | -------------------------------------------------------------------------------- /app/electron/forge.config.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const setLanguages = require('electron-packager-languages'); 3 | 4 | 5 | console.log(); 6 | 7 | module.exports = { 8 | packagerConfig: { 9 | name: 'Automancer', 10 | icon: 'icon.icns', 11 | ignore: [ 12 | /^\/forge\.config\.js$/, 13 | /^\/icon\.icns$/, 14 | /^\/lib\/types(\/|$)/, 15 | /^\/node_modules(\/|$)/, 16 | /^\/scripts(\/|$)/, 17 | /^\/src(\/|$)/, 18 | /^\/tsconfig(?:\.[^\/]*)?\.json$/, 19 | /^\/tmp(\/|$)/ 20 | ], 21 | afterCopy: [ 22 | setLanguages(['en', 'en_US']) 23 | ] 24 | }, 25 | makers: [ 26 | { name: '@electron-forge/maker-zip' }, 27 | { name: '@electron-forge/maker-squirrel', 28 | config: { 29 | authors: 'AdaptyvBio', 30 | description: 'Protocol Runner 1' 31 | } } 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /app/electron/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adaptyvbio/automancer/d577cc1775767b5d4839f908410ca5c4a0dadb9f/app/electron/icon.icns -------------------------------------------------------------------------------- /app/electron/scripts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module" 3 | } 4 | -------------------------------------------------------------------------------- /app/electron/src/host/navigation.ts: -------------------------------------------------------------------------------- 1 | let oldNavigation = window.navigation; 2 | let newNavigation = { 3 | currentEntry: { 4 | getState: () => undefined, 5 | url: 'file:///' 6 | }, 7 | addEventListener(type: 'navigate', listener: ((event: any) => void)) { 8 | oldNavigation.addEventListener('navigate', (event: any) => { 9 | event.preventDefault(); 10 | 11 | let defaultPrevented = false; 12 | 13 | listener({ 14 | destination: event.destination, 15 | canIntercept: true, 16 | info: event.info?.info, 17 | intercept({ handler }: any) { 18 | handler(); 19 | }, 20 | preventDefault: () => { 21 | defaultPrevented = true; 22 | } 23 | }); 24 | 25 | if (!defaultPrevented) { 26 | newNavigation.currentEntry = { 27 | getState: () => event.info?.state, 28 | url: event.destination.url 29 | }; 30 | 31 | } 32 | }); 33 | }, 34 | navigate(url: string, options: any) { 35 | oldNavigation.navigate(url, { 36 | info: { 37 | info: options?.info, 38 | state: options?.state 39 | } 40 | }).committed.catch(() => {}); 41 | } 42 | }; 43 | 44 | window.navigation = newNavigation; 45 | -------------------------------------------------------------------------------- /app/electron/src/interfaces.ts: -------------------------------------------------------------------------------- 1 | import type { FSWatcher } from 'chokidar'; 2 | import type { IpcMainInvokeEvent } from 'electron'; 3 | import type { DraftEntryId, PythonInstallationRecord } from 'pr1-library'; 4 | import type { UnionToIntersection } from 'pr1-shared'; 5 | 6 | 7 | export type IPC any)>> = UnionToIntersection<{ 8 | [S in keyof T]: T[S] extends ((...args: infer U) => Promise) 9 | ? { handle(channel: S, callback: ((event: IpcMainInvokeEvent, ...args: U) => Promise)): void; } 10 | : T[S] extends ((...args: infer U) => void) 11 | ? { on(channel: S, callback: ((event: IpcMainInvokeEvent, ...args: U) => void)): void; } 12 | : never; 13 | }[keyof T]>; 14 | 15 | export type IPC2d> = UnionToIntersection<{ 16 | [S in keyof T]: T[S] extends Record any)> 17 | ? IPC<{ [U in keyof T[S] as (`${S & string}.${U & string}`)]: T[S][U]; }> 18 | : never; 19 | }[keyof T]>; 20 | 21 | 22 | export interface HostCreatorContext { 23 | computerName: string; 24 | pythonInstallations: PythonInstallationRecord; 25 | } 26 | 27 | export interface Disposable { 28 | dispose(): Promise | void; 29 | } 30 | 31 | export interface DraftSkeleton { 32 | id: DraftEntryId; 33 | entryPath: string; 34 | name: string | null; 35 | } 36 | 37 | export interface DocumentChange { 38 | instance: { 39 | contents: string | null; // null = contents didn't change 40 | lastExternalModificationDate: number; 41 | lastModificationDate: number; 42 | } | null; 43 | status: 'ok' | 'error' | 'missing'; 44 | } 45 | -------------------------------------------------------------------------------- /app/electron/src/logger.ts: -------------------------------------------------------------------------------- 1 | import * as uol from 'uol'; 2 | 3 | 4 | export const rootLogger = new uol.Logger({ levels: uol.StdLevels.Python }).init(); 5 | 6 | export type Logger = typeof rootLogger; 7 | -------------------------------------------------------------------------------- /app/electron/src/shared/context-menu.tsx: -------------------------------------------------------------------------------- 1 | import { ContextMenuContext, ContextMenuProps, React } from 'pr1'; 2 | 3 | 4 | export class NativeContextMenu extends React.Component { 5 | constructor(props: ContextMenuProps) { 6 | super(props); 7 | 8 | this.props.triggerRef.current = (event) => { 9 | let menu = this.props.createMenu(event); 10 | let position = { 11 | x: event.clientX, 12 | y: event.clientY 13 | }; 14 | 15 | window.api.main.triggerContextMenu(menu, position).then((selectedPath) => { 16 | if (selectedPath !== null) { 17 | this.props.onSelect(selectedPath); 18 | } 19 | 20 | this.props.onClose(selectedPath !== null); 21 | }); 22 | }; 23 | } 24 | 25 | override render() { 26 | return null; 27 | } 28 | } 29 | 30 | export function NativeContextMenuProvider(props: React.PropsWithChildren<{}>) { 31 | return ( 32 | 33 | {props.children} 34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /app/electron/src/shared/renderer-types.d.ts: -------------------------------------------------------------------------------- 1 | import { IPCEndpoint } from './preload'; 2 | 3 | 4 | declare global { 5 | interface Window { 6 | navigation: any; 7 | readonly api: IPCEndpoint; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /app/electron/src/startup/index.ts: -------------------------------------------------------------------------------- 1 | import { BrowserWindow } from 'electron'; 2 | import path from 'path'; 3 | 4 | import { CoreApplication } from '..'; 5 | import { rootLogger } from '../logger'; 6 | 7 | 8 | export class StartupWindow { 9 | closed: Promise; 10 | window: BrowserWindow; 11 | 12 | private logger = rootLogger.getChild('startupWindow'); 13 | 14 | constructor(private app: CoreApplication) { 15 | this.logger.debug('Constructed and created'); 16 | 17 | this.window = new BrowserWindow({ 18 | width: 800, 19 | height: Math.round(800 / 1.7), 20 | backgroundColor: '#000000', 21 | fullscreenable: false, 22 | resizable: false, 23 | show: this.app.debug, 24 | webPreferences: { 25 | preload: path.join(__dirname, '../preload/index.js') 26 | }, 27 | ...((process.platform === 'darwin') 28 | ? { 29 | titleBarStyle: 'hiddenInset' 30 | } 31 | : { 32 | titleBarOverlay: { color: '#f6f6f6' }, 33 | titleBarStyle: 'hidden' 34 | }) 35 | }); 36 | 37 | this.closed = new Promise((resolve) => { 38 | this.window.on('close', () => { 39 | this.logger.debug('Closed'); 40 | resolve(); 41 | }); 42 | }); 43 | 44 | this.window.loadFile(path.join(__dirname, '../static/startup/index.html')); 45 | } 46 | 47 | focus() { 48 | if (this.window.isMinimized()) { 49 | this.window.restore(); 50 | } 51 | 52 | this.window.focus(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/electron/src/startup/steps/s2.tsx: -------------------------------------------------------------------------------- 1 | //* Success 2 | 3 | import { LargeIcon, React } from 'pr1'; 4 | import { HostSettingsId } from 'pr1-library'; 5 | 6 | import { HostCreatorStepData, HostCreatorStepProps } from '../host-creator'; 7 | 8 | 9 | export interface Data extends HostCreatorStepData { 10 | stepIndex: 2; 11 | 12 | hostSettingsId: HostSettingsId; 13 | label: string; 14 | } 15 | 16 | export function Component(props: HostCreatorStepProps) { 17 | return ( 18 |
19 |
20 |
21 |
New setup
22 |

Set connection parameters

23 |
24 |
25 | 26 |

Succesfully added "{props.data.label}"

27 |
28 |
29 |
30 |
31 | 34 |
35 |
36 | 39 |
40 |
41 |
42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /app/electron/src/types.ts: -------------------------------------------------------------------------------- 1 | export * from './interfaces'; 2 | -------------------------------------------------------------------------------- /app/electron/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | 4 | "compilerOptions": { 5 | "esModuleInterop": true, 6 | "resolveJsonModule": true 7 | }, 8 | "files": [ 9 | "src/index.ts" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /app/electron/tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../shared/tsconfig.json", 3 | 4 | "compilerOptions": { 5 | "allowJs": true, 6 | "checkJs": true, 7 | "moduleResolution": "node", 8 | "noEmit": true 9 | }, 10 | "references": [ 11 | { "path": "../../client" } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /app/electron/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.preload.json" }, 6 | { "path": "./tsconfig.renderer.json" } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /app/electron/tsconfig.preload.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | 4 | "files": [ 5 | "src/shared/preload.ts" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /app/electron/tsconfig.renderer.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | 4 | "compilerOptions": { 5 | "jsx": "react-jsx" 6 | }, 7 | "files": [ 8 | "src/host/renderer.tsx", 9 | "src/startup/renderer.tsx", 10 | 11 | "src/shared/renderer-types.d.ts" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /app/electron/tsconfig.types.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | 4 | "compilerOptions": { 5 | "composite": true, 6 | "emitDeclarationOnly": true, 7 | "jsx": "react", 8 | "noEmit": false, 9 | "outDir": "lib/types", 10 | "rootDir": "src", 11 | "resolveJsonModule": true 12 | }, 13 | "files": [ 14 | "src/types.ts" 15 | ], 16 | "include": [ 17 | "src/**/*" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /app/library/.gitignore: -------------------------------------------------------------------------------- 1 | *.tsbuildinfo 2 | lib 3 | node_modules 4 | package-lock.json 5 | /types 6 | -------------------------------------------------------------------------------- /app/library/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pr1-library", 3 | "version": "0.0.0", 4 | "main": "lib/index.js", 5 | "types": "types/index.d.ts", 6 | "scripts": { 7 | "build": "tsc", 8 | "build:watch": "tsc --sourceMap --watch" 9 | }, 10 | "dependencies": { 11 | "multicast-dns": "^7.2.5", 12 | "pr1": "file:../../client", 13 | "pr1-shared": "file:../shared" 14 | }, 15 | "devDependencies": { 16 | "@types/multicast-dns": "^7.2.1", 17 | "@types/node": "^18.11.18", 18 | "typescript": "^4.9.4" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/library/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './host'; 2 | export * from './scan'; 3 | export * from './search'; 4 | export * from './socket-client'; 5 | export * from './types/app-data'; 6 | export * from './util'; 7 | export * from './util/fs'; 8 | -------------------------------------------------------------------------------- /app/library/src/util.ts: -------------------------------------------------------------------------------- 1 | import childProcess from 'node:child_process'; 2 | import fs from 'node:fs/promises'; 3 | 4 | 5 | export async function fsMkdir(dirPath: string) { 6 | await fs.mkdir(dirPath, { recursive: true }); 7 | } 8 | 9 | 10 | export interface RunCommandOptions { 11 | architecture?: string | null; 12 | cwd?: string; 13 | ignoreErrors?: unknown; 14 | timeout?: number; 15 | } 16 | 17 | export async function runCommand(args: string[] | string, options: RunCommandOptions & { ignoreErrors: true; }): Promise<[string, string] | null>; 18 | export async function runCommand(args: string[] | string, options?: RunCommandOptions): Promise<[string, string]>; 19 | export async function runCommand(args: string[] | string, options?: RunCommandOptions) { 20 | if (typeof args === 'string') { 21 | args = args.split(' '); 22 | } 23 | 24 | if (options?.architecture && (process.platform === 'darwin')) { 25 | args = ['arch', '-arch', options.architecture, ...args]; 26 | } 27 | 28 | let [execPath, ...otherArgs] = args; 29 | 30 | return await new Promise<[string, string] | null>((resolve, reject) => { 31 | childProcess.execFile(execPath, otherArgs, { 32 | cwd: options?.cwd, 33 | timeout: (options?.timeout ?? 1000) 34 | }, (err, stdout, stderr) => { 35 | if (err) { 36 | if (options?.ignoreErrors) { 37 | resolve(null); 38 | } else { 39 | reject(err); 40 | } 41 | } else { 42 | resolve([stdout, stderr]); 43 | } 44 | }); 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /app/library/src/util/fs.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises'; 2 | 3 | 4 | /** 5 | * Tests whether a file exists. 6 | * 7 | * @param path The path to the potential file. 8 | */ 9 | export async function fsExists(path: string) { 10 | try { 11 | await fs.stat(path) 12 | } catch (err) { 13 | if ((err as { code: string; }).code === 'ENOENT') { 14 | return false; 15 | } 16 | 17 | throw err; 18 | } 19 | 20 | return true; 21 | } 22 | -------------------------------------------------------------------------------- /app/library/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../shared/tsconfig.json", 3 | 4 | "compilerOptions": { 5 | "composite": true, 6 | "declarationDir": "types", 7 | "module": "es2022", 8 | "moduleResolution": "node", 9 | "outDir": "lib", 10 | "rootDir": "src" 11 | }, 12 | "files": [ 13 | "src/index.ts" 14 | ], 15 | "include": [ 16 | "src/**/*" 17 | ], 18 | "references": [ 19 | { "path": "../electron/tsconfig.types.json" } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /app/server/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | __pycache__ 4 | *.egg-info 5 | -------------------------------------------------------------------------------- /app/server/pr1_server/__main__.py: -------------------------------------------------------------------------------- 1 | from logging import Formatter 2 | import logging 3 | import sys 4 | 5 | 6 | class ColoredFormatter(Formatter): 7 | def format(self, record): 8 | reset = "\x1b[0m" 9 | color = { 10 | logging.CRITICAL: "\x1b[31;1m", 11 | logging.INFO: "\x1b[34;20m", 12 | logging.ERROR: "\x1b[31;20m", 13 | logging.WARNING: "\x1b[33;20m" 14 | }.get(record.levelno, str()) 15 | 16 | formatter = logging.Formatter(f"{color}%(levelname)-8s{reset} :: %(name)-18s :: %(message)s") 17 | return formatter.format(record) 18 | 19 | class DefaultFormatter(Formatter): 20 | def format(self, record): 21 | formatter = logging.Formatter(f"%(levelname)-8s :: %(name)-18s :: %(message)s") 22 | return formatter.format(record) 23 | 24 | 25 | ch = logging.StreamHandler() 26 | ch.setFormatter(ColoredFormatter() if sys.stderr.isatty() else DefaultFormatter()) 27 | 28 | logging.getLogger().addHandler(ch) 29 | logging.getLogger().setLevel(logging.INFO) 30 | logging.getLogger("aiohttp.access").setLevel(logging.DEBUG) 31 | logging.getLogger("pr1").setLevel(logging.DEBUG) 32 | 33 | 34 | from . import main 35 | 36 | main() 37 | -------------------------------------------------------------------------------- /app/server/pr1_server/auth.py: -------------------------------------------------------------------------------- 1 | import bcrypt 2 | import json 3 | 4 | from pr1.util import schema as sc 5 | 6 | 7 | # password_message_schema = sc.Schema({ 8 | # 'password': str 9 | # }) 10 | 11 | class PasswordAgent: 12 | name = 'password' 13 | conf_schema = sc.Schema({ 14 | 'type': name, 15 | 'password': str, 16 | 'hashed': sc.Optional(sc.ParseType(bool)) 17 | }) 18 | 19 | def __init__(self, conf): 20 | self._conf = conf 21 | 22 | def test(self, message): 23 | return bcrypt.checkpw(message['password'].encode("utf-8"), self._conf['password'].encode("utf-8")) 24 | 25 | def export(self): 26 | return { 27 | "type": self.name, 28 | "description": self._conf.get('description') or "Password authentication" 29 | } 30 | 31 | def update_conf(conf): 32 | if not conf.get('hashed'): 33 | return { 34 | **conf, 35 | 'password': bcrypt.hashpw(conf['password'].encode("utf-8"), bcrypt.gensalt()).decode("utf-8"), 36 | 'hashed': True 37 | } 38 | 39 | 40 | agents = { 41 | Agent.name: Agent 42 | for Agent in [PasswordAgent] 43 | } 44 | -------------------------------------------------------------------------------- /app/server/pr1_server/bridges/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adaptyvbio/automancer/d577cc1775767b5d4839f908410ca5c4a0dadb9f/app/server/pr1_server/bridges/__init__.py -------------------------------------------------------------------------------- /app/server/pr1_server/bridges/protocol.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from dataclasses import dataclass 3 | from ipaddress import IPv4Address 4 | from typing import Any, AsyncGenerator, Callable, Coroutine, Protocol 5 | 6 | 7 | class ClientClosed(Exception): 8 | pass 9 | 10 | class BaseClient(ABC): 11 | def __init__(self): 12 | self.id: str 13 | self.privileged: bool 14 | self.remote: bool 15 | 16 | @abstractmethod 17 | async def recv(self) -> Any: 18 | ... 19 | 20 | @abstractmethod 21 | async def send(self, message: object): 22 | ... 23 | 24 | def __aiter__(self): 25 | return self 26 | 27 | async def __anext__(self): 28 | return await self.recv() 29 | 30 | 31 | @dataclass 32 | class BridgeAdvertisementInfo: 33 | address: IPv4Address 34 | port: int 35 | type: str 36 | 37 | class BridgeProtocol(Protocol): 38 | def advertise(self) -> list[BridgeAdvertisementInfo]: 39 | return list() 40 | 41 | def export_info(self) -> list: 42 | return list() 43 | 44 | async def start(self, handle_client: Callable[[BaseClient], Coroutine]) -> AsyncGenerator[Any, None]: 45 | ... 46 | -------------------------------------------------------------------------------- /app/server/pr1_server/bridges/stdio.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | import sys 4 | import threading 5 | from typing import Optional 6 | 7 | from .protocol import BridgeProtocol, BaseClient 8 | 9 | 10 | class Client(BaseClient): 11 | privileged = False 12 | remote = False 13 | 14 | def __init__(self): 15 | super().__init__() 16 | self.id = "0" 17 | 18 | async def recv(self): 19 | loop = asyncio.get_event_loop() 20 | fut = loop.create_future() 21 | 22 | def _run(): 23 | line = sys.stdin.readline() 24 | loop.call_soon_threadsafe(fut.set_result, line) 25 | 26 | threading.Thread(target=_run, daemon=True).start() 27 | return json.loads(await fut) 28 | 29 | async def send(self, message): 30 | sys.stdout.write(json.dumps(message, allow_nan=False) + "\n") 31 | sys.stdout.flush() 32 | 33 | 34 | class StdioBridge(BridgeProtocol): 35 | def __init__(self, *, app): 36 | self.app = app 37 | self.client: Client 38 | 39 | async def initialize(self): 40 | self.client = Client() 41 | 42 | async def start(self, handle_client): 43 | try: 44 | await handle_client(self.client) 45 | except asyncio.CancelledError: 46 | pass 47 | -------------------------------------------------------------------------------- /app/server/pr1_server/session.py: -------------------------------------------------------------------------------- 1 | import array 2 | import asyncio 3 | import os 4 | import subprocess 5 | 6 | 7 | class Session: 8 | def __init__(self, size): 9 | self._master = None 10 | self._proc = None 11 | self._size = size # (columns, rows) 12 | 13 | @property 14 | def status(self): 15 | return self._proc.poll() 16 | 17 | def resize(self, new_size = None): 18 | import fcntl 19 | import termios 20 | 21 | if new_size: 22 | self._size = new_size 23 | 24 | buf = array.array('H', [self._size[1], self._size[0], 0, 0]) 25 | fcntl.ioctl(self._master, termios.TIOCSWINSZ, buf) 26 | 27 | async def start(self): 28 | import pty 29 | 30 | master, slave = pty.openpty() 31 | self._master = master 32 | 33 | self._proc = subprocess.Popen(["fish"], stdout=slave, stderr=slave, stdin=slave, universal_newlines=True, preexec_fn=os.setsid, shell=True, close_fds=True, cwd=os.environ["HOME"]) 34 | 35 | os.close(slave) 36 | 37 | self.resize() 38 | 39 | loop = asyncio.get_event_loop() 40 | reader = asyncio.StreamReader() 41 | protocol = asyncio.StreamReaderProtocol(reader) 42 | await loop.connect_read_pipe(lambda: protocol, os.fdopen(master, mode="rb")) 43 | 44 | while self._proc.poll() is None: 45 | data = await reader.read(100) 46 | 47 | # 'data' is an empty bytes object when the process terminates. 48 | if len(data) > 0: 49 | yield data 50 | 51 | # print(res.decode("utf-8"), end="") 52 | 53 | def close(self): 54 | self._proc.kill() 55 | 56 | def write(self, data): 57 | os.write(self._master, data) 58 | -------------------------------------------------------------------------------- /app/server/pr1_server/trash.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import functools 3 | import tempfile 4 | 5 | from . import logger 6 | 7 | 8 | @functools.cache 9 | def find_trash(): 10 | home = Path.home() 11 | 12 | # TODO: Add support for Windows 13 | locations = [ 14 | home / ".local/share/Trash", 15 | home / ".Trash" 16 | ] 17 | 18 | for location in locations: 19 | if location.exists(): 20 | return location 21 | 22 | return None 23 | 24 | 25 | def trash(source: Path): 26 | trash_location = find_trash() 27 | 28 | if trash_location is None: 29 | trash_location = Path(tempfile.mkdtemp()) 30 | logger.warning("No trash location found, using temporary directory instead") 31 | 32 | index = 0 33 | target = trash_location / source.name 34 | 35 | while target.exists(): 36 | index += 1 37 | target = trash_location / f"{source.name} {index}" 38 | 39 | logger.debug(f"Moving '{source}' to '{target}'") 40 | source.rename(target) 41 | -------------------------------------------------------------------------------- /app/server/pr1_server/util.py: -------------------------------------------------------------------------------- 1 | from ipaddress import IPv4Address, IPv6Address 2 | from typing import Iterator, Sequence 3 | 4 | 5 | IPAddress = IPv4Address | IPv6Address 6 | 7 | 8 | def format_list(items: Iterator[str], /): 9 | *head, tail = items 10 | return ", ".join(head) + (" and " if head else str()) + tail 11 | -------------------------------------------------------------------------------- /app/server/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=66", "setuptools_scm[toml]>=6.2"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "pr1-server" 7 | dynamic = ["version"] 8 | requires-python = ">3.11" 9 | 10 | dependencies=[ 11 | "aiohttp~=3.7.4", 12 | "bcrypt~=3.2.2", 13 | "pyOpenSSL~=23.0", 14 | "websockets~=10.2.0", 15 | "zeroconf~=0.47.1" 16 | ] 17 | 18 | [project.entry-points.console_scripts] 19 | automancer = "pr1_server:main" 20 | 21 | [tool.setuptools.packages.find] 22 | include = ["pr1_server"] 23 | 24 | [tool.setuptools_scm] 25 | root = "../.." 26 | -------------------------------------------------------------------------------- /app/shared/.gitignore: -------------------------------------------------------------------------------- 1 | *.tsbuildinfo 2 | lib 3 | node_modules 4 | package-lock.json 5 | /types 6 | -------------------------------------------------------------------------------- /app/shared/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pr1-shared", 3 | "version": "0.0.0", 4 | "main": "lib/index.js", 5 | "types": "types/index.d.ts", 6 | "scripts": { 7 | "build": "tsc", 8 | "build:watch": "tsc --sourceMap --watch" 9 | }, 10 | "dependencies": { 11 | "multicast-dns": "^7.2.5" 12 | }, 13 | "devDependencies": { 14 | "typescript": "^4.9.5" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/shared/src/client-utils.ts: -------------------------------------------------------------------------------- 1 | import { createErrorWithCode } from './error'; 2 | import { ClientProtocol, ServerProtocol } from './types/communication'; 3 | 4 | 5 | export async function* splitMessagesOfIterator(iterable: AsyncIterable<{ toString(): string; }>) { 6 | let contents = ''; 7 | 8 | for await (let chunk of iterable) { 9 | contents += chunk.toString(); 10 | 11 | let msgs = contents.split('\n'); 12 | contents = msgs.at(-1)!; 13 | 14 | yield* msgs.slice(0, -1); 15 | } 16 | } 17 | 18 | 19 | export function serializeMessage(message: ClientProtocol.Message) { 20 | return (JSON.stringify(message) + '\n'); 21 | } 22 | 23 | export async function* deserializeMessagesOfIterator(iterable: AsyncIterable) { 24 | for await (let msg of iterable) { 25 | let message; 26 | 27 | try { 28 | message = JSON.parse(msg) as ServerProtocol.Message; 29 | } catch (err) { 30 | if (err instanceof SyntaxError) { 31 | throw createErrorWithCode('Malformed message', 'APP_MALFORMED'); 32 | } 33 | 34 | throw err; 35 | } 36 | 37 | yield message; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/shared/src/defer.ts: -------------------------------------------------------------------------------- 1 | export function defer(): Deferred { 2 | let resolve!: Deferred['resolve']; 3 | let reject!: Deferred['reject']; 4 | 5 | let promise = new Promise((_resolve, _reject) => { 6 | resolve = _resolve; 7 | reject = _reject; 8 | }); 9 | 10 | return { promise, resolve, reject }; 11 | } 12 | 13 | export interface Deferred { 14 | promise: Promise; 15 | resolve(value: PromiseLike | T): void; 16 | reject(err: any): void; 17 | } 18 | -------------------------------------------------------------------------------- /app/shared/src/error.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates an `Error` instance with a `code` property. 3 | * 4 | * @param message The error's message. 5 | * @param code The error's code, such as `APP_FINGERPRINT_MISMATCH`. 6 | */ 7 | export function createErrorWithCode(message: string, code: string) { 8 | let err = new Error(message); 9 | 10 | // @ts-expect-error 11 | err.code = code; 12 | 13 | return err; 14 | } 15 | -------------------------------------------------------------------------------- /app/shared/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './client-utils'; 2 | export * from './client'; 3 | export * from './defer'; 4 | export * from './error'; 5 | export * from './lock'; 6 | export * from './misc'; 7 | export * from './term'; 8 | export * from './types/communication'; 9 | export * from './types/compilation'; 10 | export * from './types/diagnostic'; 11 | export * from './types/effect'; 12 | export * from './types/experiment'; 13 | export * from './types/host'; 14 | export * from './types/master'; 15 | export * from './types/plugin'; 16 | export * from './types/protocol'; 17 | export * from './types/rich-text'; 18 | export * from './types/unit'; 19 | export * from './types/util'; 20 | -------------------------------------------------------------------------------- /app/shared/src/lock.ts: -------------------------------------------------------------------------------- 1 | import { Deferred, defer } from './defer'; 2 | 3 | 4 | export class Lock { 5 | #candidates: Deferred[] = []; 6 | #locked = false; 7 | 8 | constructor(options?: { signal?: AbortSignal; }) { 9 | options?.signal?.addEventListener('abort', () => { 10 | for (let deferred of this.#candidates) { 11 | deferred.reject(new Error('Aborted')); 12 | } 13 | }); 14 | } 15 | 16 | async acquire(options?: { signal?: AbortSignal; }) { 17 | if (this.#locked) { 18 | let deferred = defer(); 19 | this.#candidates.push(deferred); 20 | 21 | options?.signal?.addEventListener('abort', () => { 22 | this.#candidates.splice(this.#candidates.indexOf(deferred), 1); 23 | }); 24 | 25 | await deferred.promise; 26 | } 27 | 28 | return () => { 29 | let deferred = this.#candidates.shift()!; 30 | 31 | if (deferred) { 32 | deferred.resolve(); 33 | } else { 34 | this.#locked = false; 35 | } 36 | }; 37 | } 38 | 39 | async acquireWith(fn: (() => Promise | T)) { 40 | let controller = new AbortController(); 41 | 42 | await this.acquire(); 43 | 44 | try { 45 | return await fn(); 46 | } finally { 47 | controller.abort(); 48 | } 49 | } 50 | 51 | get locked() { 52 | return this.#locked; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/shared/src/misc.ts: -------------------------------------------------------------------------------- 1 | import type { CompilationAnalysis } from './types/compilation'; 2 | import type { MasterAnalysis } from './types/master'; 3 | 4 | 5 | export function createReport(analysis: CompilationAnalysis | MasterAnalysis | null) { 6 | return [ 7 | ...(analysis && ('effects' in analysis) ? analysis.effects : []).map((effect) => [effect, 'effect'] as const), 8 | ...(analysis?.errors ?? []).map((diagnostic) => [diagnostic, 'error'] as const), 9 | ...(analysis?.warnings ?? []).map((diagnostic) => [diagnostic, 'warning'] as const) 10 | ]; 11 | } 12 | -------------------------------------------------------------------------------- /app/shared/src/term.ts: -------------------------------------------------------------------------------- 1 | import type { AnyDurationTerm, DurationTerm, Term } from './types/protocol'; 2 | 3 | 4 | export function addTerms(target: AnyDurationTerm, other: AnyDurationTerm): AnyDurationTerm; 5 | export function addTerms(target: Term, other: Term): Term; 6 | 7 | export function addTerms(target: Term, other: Term): Term { 8 | if ((target.type === 'forever') || (other.type === 'forever')) { 9 | return { type: 'forever' }; 10 | } 11 | 12 | if ((target.type === 'unknown') || (other.type === 'unknown')) { 13 | return { type: 'unknown' }; 14 | } 15 | 16 | if ((target.type === 'duration') && (other.type === 'duration')) { 17 | return { 18 | type: 'duration', 19 | resolution: (target.resolution + other.resolution), 20 | value: (target.value + other.value) 21 | }; 22 | } 23 | 24 | if ((target.type === 'datetime') && (other.type === 'duration')) { 25 | return { 26 | type: 'datetime', 27 | resolution: (target.resolution + other.resolution), 28 | value: (target.value + other.value) 29 | }; 30 | } 31 | 32 | throw new Error('Invalid operation'); 33 | } 34 | 35 | 36 | export function createZeroTerm(): DurationTerm { 37 | return { 38 | type: 'duration', 39 | resolution: 0, 40 | value: 0 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /app/shared/src/types/compilation.ts: -------------------------------------------------------------------------------- 1 | import type { Diagnostic } from './diagnostic'; 2 | 3 | 4 | export interface CompilationAnalysis { 5 | errors: Diagnostic[]; 6 | warnings: Diagnostic[]; 7 | } 8 | -------------------------------------------------------------------------------- /app/shared/src/types/diagnostic.ts: -------------------------------------------------------------------------------- 1 | import type { DocumentId } from './draft'; 2 | import type { RichText } from './rich-text'; 3 | import type { Brand } from './util'; 4 | 5 | 6 | export type DiagnosticId = Brand; 7 | 8 | export type Diagnostic = { 9 | id: DiagnosticId | null; 10 | description: RichText | null; 11 | message: string; 12 | name: string; 13 | references: DiagnosticReference[]; 14 | runtimeInfo: null; 15 | trace: DiagnosticReference[] | null; 16 | } & ({ 17 | type: 'default'; 18 | } | { 19 | type: 'timed'; 20 | date: number; 21 | }); 22 | 23 | export type DiagnosticReference = DiagnosticDocumentReference | DiagnosticFileReference; 24 | 25 | export interface DiagnosticBaseReference { 26 | type: string; 27 | id: string | null; 28 | label: string | null; 29 | } 30 | 31 | export type DiagnosticDocumentReference = DiagnosticBaseReference & { 32 | type: 'document'; 33 | documentId: DocumentId; 34 | ranges: [number, number][]; 35 | } 36 | 37 | export type DiagnosticFileReference = DiagnosticBaseReference & { 38 | type: 'file'; 39 | path: string; 40 | } 41 | -------------------------------------------------------------------------------- /app/shared/src/types/draft.ts: -------------------------------------------------------------------------------- 1 | import type { Brand } from './util'; 2 | 3 | 4 | export type DocumentId = Brand; 5 | export type DraftId = Brand; 6 | -------------------------------------------------------------------------------- /app/shared/src/types/effect.ts: -------------------------------------------------------------------------------- 1 | import type { DiagnosticBaseReference } from './diagnostic'; 2 | import type { MasterItem } from './master'; 3 | import { RichText } from './rich-text'; 4 | 5 | 6 | export type Effect = MasterItem<{ 7 | type: string; 8 | references: DiagnosticBaseReference[]; 9 | }>; 10 | 11 | export interface GenericEffect extends Effect { 12 | type: 'generic'; 13 | description: RichText | null; 14 | icon: string | null; 15 | message: string; 16 | } 17 | -------------------------------------------------------------------------------- /app/shared/src/types/experiment.ts: -------------------------------------------------------------------------------- 1 | import type { CompilationAnalysis } from './compilation'; 2 | import type { Master, MasterAnalysis, MasterBlockLocation } from './master'; 3 | import type { PluginName } from './plugin'; 4 | import type { ProtocolBlock } from './protocol'; 5 | import type { Brand } from './util'; 6 | 7 | 8 | export type ExperimentId = Brand; 9 | 10 | 11 | export interface Experiment { 12 | id: ExperimentId; 13 | creationDate: number; 14 | hasReport: boolean; 15 | master: Master | null; 16 | runners: Record; 17 | title: string; 18 | } 19 | 20 | export interface ExperimentReportInfo { 21 | draft: any; 22 | endDate: number; 23 | initialAnalysis: CompilationAnalysis; 24 | masterAnalysis: MasterAnalysis; 25 | name: string; 26 | root: ProtocolBlock; 27 | rootStaticEntry: ExperimentReportStaticEntry; 28 | startDate: number; 29 | } 30 | 31 | export interface ExperimentReportStaticEntry { 32 | occurenceCount: number; 33 | occurences: [ExperimentReportEventIndex, ExperimentReportEventIndex][]; 34 | children: Record; 35 | } 36 | 37 | export type ExperimentReportEventIndex = Brand; 38 | 39 | export interface ExperimentReportEvent { 40 | date: number; 41 | location: MasterBlockLocation | null; 42 | } 43 | 44 | export type ExperimentReportEvents = Record; 45 | -------------------------------------------------------------------------------- /app/shared/src/types/host.ts: -------------------------------------------------------------------------------- 1 | import { Experiment, ExperimentId } from './experiment'; 2 | import type { PluginName } from './plugin'; 3 | import type { PluginInfo } from './unit'; 4 | import type { Brand } from './util'; 5 | 6 | 7 | export type HostIdentifier = Brand; 8 | export type HostId = Brand; 9 | 10 | export interface HostState { 11 | info: { 12 | id: HostId; 13 | instanceRevision: number; 14 | name: string; 15 | startTime: number; 16 | units: Record; 17 | }; 18 | 19 | executors: Record; 20 | experiments: Record; 21 | } 22 | -------------------------------------------------------------------------------- /app/shared/src/types/master.ts: -------------------------------------------------------------------------------- 1 | import type { CompilationAnalysis } from './compilation'; 2 | import type { Diagnostic } from './diagnostic'; 3 | import { Effect } from './effect'; 4 | import type { Protocol, Term } from './protocol'; 5 | import type { Brand } from './util'; 6 | 7 | 8 | export type MasterId = Brand; 9 | 10 | export type MasterBlockId = number; 11 | 12 | export interface MasterBlockLocation { 13 | children: Record; 14 | childrenTerms: Record; 15 | startDate: number; 16 | term: Term; 17 | } 18 | 19 | 20 | export interface Master { 21 | id: MasterId; 22 | initialAnalysis: CompilationAnalysis; 23 | location: MasterBlockLocation; 24 | masterAnalysis: MasterAnalysis; 25 | protocol: Protocol; 26 | startDate: number; 27 | } 28 | 29 | export interface MasterAnalysis { 30 | effects: Effect[]; 31 | errors: MasterDiagnostic[]; 32 | warnings: MasterDiagnostic[]; 33 | } 34 | 35 | export type MasterItem = Omit & { 36 | runtimeInfo: { 37 | authorPath: number[]; 38 | eventIndex: number; 39 | }; 40 | } 41 | 42 | export type MasterDiagnostic = MasterItem; 43 | export type AnyDiagnostic = Diagnostic | MasterDiagnostic; 44 | -------------------------------------------------------------------------------- /app/shared/src/types/plugin.ts: -------------------------------------------------------------------------------- 1 | import type { Brand } from './util'; 2 | 3 | 4 | export type PluginName = Brand; 5 | -------------------------------------------------------------------------------- /app/shared/src/types/protocol.ts: -------------------------------------------------------------------------------- 1 | import type { DocumentId, DraftId } from './draft'; 2 | import type { PluginName } from './plugin'; 3 | import type { Brand } from './util'; 4 | 5 | 6 | export interface Protocol { 7 | draft: { 8 | id: DraftId; 9 | documents: { 10 | id: DocumentId; 11 | }[]; 12 | entryDocumentId: DocumentId; 13 | }; 14 | name: string | null; 15 | root: ProtocolBlock; 16 | } 17 | 18 | export interface ProtocolBlock { 19 | duration: AnyDurationTerm; 20 | name: ProtocolBlockName; 21 | namespace: PluginName; 22 | 23 | [key: string]: unknown; 24 | } 25 | 26 | export type ProtocolBlockName = Brand; 27 | export type ProtocolBlockPath = number[]; 28 | 29 | export interface ProtocolProcess { 30 | data: unknown; 31 | namespace: PluginName; 32 | } 33 | 34 | 35 | export interface DatetimeTerm { 36 | type: 'datetime'; 37 | resolution: number; 38 | value: number; 39 | } 40 | 41 | export interface DurationTerm { 42 | type: 'duration'; 43 | resolution: number; 44 | value: number; 45 | } 46 | 47 | export interface ForeverTerm { 48 | type: 'forever'; 49 | } 50 | 51 | export interface UnknownTerm { 52 | type: 'unknown'; 53 | } 54 | 55 | export type AnyDurationTerm = DurationTerm | ForeverTerm | UnknownTerm; 56 | export type Term = DatetimeTerm | DurationTerm | ForeverTerm | UnknownTerm; 57 | -------------------------------------------------------------------------------- /app/shared/src/types/rich-text.ts: -------------------------------------------------------------------------------- 1 | export type RichText = RichTextComponent[]; 2 | 3 | export type RichTextComponent = string | { 4 | type: 'code'; 5 | value: RichText; 6 | } | { 7 | type: 'link'; 8 | url: string; 9 | value: RichText; 10 | } | { 11 | type: 'strong'; 12 | value: RichText; 13 | }; 14 | -------------------------------------------------------------------------------- /app/shared/src/types/unit.ts: -------------------------------------------------------------------------------- 1 | import type { PluginName } from './plugin'; 2 | 3 | 4 | export interface PluginInfo { 5 | development: boolean; 6 | enabled: boolean; 7 | hasClient: boolean; 8 | namespace: PluginName; 9 | version: number; 10 | 11 | metadata: { 12 | author: string | null; 13 | description: string | null; 14 | license: string | null; 15 | title: string | null; 16 | url: string | null; 17 | version: string | null; 18 | 19 | icon: { 20 | kind: 'bitmap' | 'icon' | 'svg'; 21 | value: string; 22 | } | null; 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /app/shared/src/types/util.ts: -------------------------------------------------------------------------------- 1 | declare const brand: unique symbol; 2 | 3 | export type Brand = T & { 4 | [brand]: TBrand; 5 | }; 6 | 7 | 8 | export type OrdinaryId = number | string; 9 | 10 | 11 | export type Depromisify = T extends Promise ? U : never; 12 | export type UnionToIntersection = (T extends any ? ((k: T) => void) : never) extends ((k: infer S) => void) ? S : never; 13 | 14 | 15 | type Join = Tuple extends [infer Head] 16 | ? Head & string 17 | : Tuple extends [infer Head, ...(infer Tail)] 18 | ? Tail extends string[] // Not sure why this is needed 19 | ? `${Head & string}${Glue}${Join}` 20 | : never 21 | : never; 22 | -------------------------------------------------------------------------------- /app/shared/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../shared/tsconfig.json", 3 | 4 | "compilerOptions": { 5 | "composite": true, 6 | "declarationDir": "types", 7 | "lib": ["dom", "es2022"], 8 | "outDir": "lib", 9 | "rootDir": "src" 10 | }, 11 | "files": [ 12 | "src/index.ts" 13 | ], 14 | "include": [ 15 | "src/**/*" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /app/standalone-client/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | package-lock.json 4 | -------------------------------------------------------------------------------- /app/standalone-client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/standalone-client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "build": "esbuild index.js --bundle --outfile=dist/index.js --format=iife --loader:.ttf=file --loader:.woff2=file", 4 | "build-other": "zsh -c 'esbuild node_modules/pr1-client/dist/*/**/*.js --outbase=node_modules/pr1-client/dist --outdir=dist'", 5 | "download-host": "mkdir -p dist && cd dist && pip3 download ../../../host" 6 | }, 7 | "dependencies": { 8 | "idb-keyval": "^6.1.0", 9 | "pr1-client": "file:../../client" 10 | }, 11 | "devDependencies": { 12 | "esbuild": "~0.15.16" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/standalone-client/service-worker.js: -------------------------------------------------------------------------------- 1 | self.addEventListener('fetch', function (event) { 2 | let url = new URL(event.request.url); 3 | 4 | if (url.origin === 'https://cdn.jsdelivr.net') { 5 | event.respondWith( 6 | caches.open('cdn').then(function (cache) { 7 | return cache.match(event.request).then(function (response) { 8 | return ( 9 | response || 10 | fetch(event.request).then(function (response) { 11 | cache.put(event.request, response.clone()); 12 | return response; 13 | }) 14 | ); 15 | }); 16 | }), 17 | ); 18 | } 19 | }); 20 | -------------------------------------------------------------------------------- /app/vscode-extension/.gitignore: -------------------------------------------------------------------------------- 1 | lib 2 | node_modules 3 | package-lock.json 4 | -------------------------------------------------------------------------------- /app/vscode-extension/language-configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "autoClosingPairs": [ 3 | { 4 | "open": "{{", 5 | "close": "}}" 6 | } 7 | ], 8 | "brackets": [ 9 | [ 10 | "{{", 11 | "}}" 12 | ] 13 | ], 14 | "comments": { 15 | "lineComment": "#" 16 | }, 17 | "surroundingPairs": [ 18 | { 19 | "open": "{", 20 | "close": "}" 21 | } 22 | ], 23 | "onEnterRules": [ 24 | { 25 | "beforeText": "^ *- *[^:]+: *$", 26 | "action": { 27 | "appendText": " ", 28 | "indent": "indent" 29 | } 30 | }, 31 | { 32 | "beforeText": "^ *[^:]+: *$", 33 | "action": { 34 | "indent": "indent" 35 | } 36 | }, 37 | { 38 | "beforeText": "^ *\\| ", 39 | "action": { 40 | "indent": "none", 41 | "appendText": "| " 42 | } 43 | } 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /app/vscode-extension/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prl", 3 | "displayName": "PRL language support", 4 | "version": "0.1.0", 5 | "main": "lib/index.js", 6 | "engines": { 7 | "vscode": "^1.74.0" 8 | }, 9 | "scripts": { 10 | "build": "tsc" 11 | }, 12 | "categories": [ 13 | "Programming Languages" 14 | ], 15 | "activationEvents": [ 16 | "onLanguage:prl" 17 | ], 18 | "contributes": { 19 | "commands": [ 20 | { 21 | "command": "pr1.setHostIdentifier", 22 | "title": "Select setup" 23 | } 24 | ], 25 | "grammars": [ 26 | { 27 | "language": "prl", 28 | "scopeName": "source.prl", 29 | "path": "./syntaxes/prl.tmLanguage.json", 30 | "embeddedLanguages": { 31 | "meta.embedded.block.python": "python" 32 | } 33 | } 34 | ], 35 | "languages": [ 36 | { 37 | "id": "prl", 38 | "aliases": [ 39 | "PRL", 40 | "prl" 41 | ], 42 | "extensions": [ 43 | ".prl" 44 | ], 45 | "configuration": "./language-configuration.json" 46 | } 47 | ], 48 | "menus": { 49 | "commandPalette": [ 50 | { 51 | "command": "pr1.setHostIdentifier", 52 | "when": "editorLangId == prl" 53 | } 54 | ] 55 | } 56 | }, 57 | "dependencies": { 58 | "node-machine-id": "^1.1.12", 59 | "pr1": "file:../../client", 60 | "pr1-library": "file:../library" 61 | }, 62 | "devDependencies": { 63 | "@types/node": "^18.11.18", 64 | "@types/vscode": "^1.74.0", 65 | "typescript": "^4.9.4" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /app/vscode-extension/src/util.ts: -------------------------------------------------------------------------------- 1 | export function defer(): Deferred { 2 | let resolve!: Deferred['resolve']; 3 | let reject!: Deferred['reject']; 4 | 5 | let promise = new Promise((_resolve, _reject) => { 6 | resolve = _resolve; 7 | reject = _reject; 8 | }); 9 | 10 | return { promise, resolve, reject }; 11 | } 12 | 13 | export interface Deferred { 14 | promise: Promise; 15 | resolve(value: T): void; 16 | reject(err: any): void; 17 | } 18 | 19 | 20 | export function findMap(arr: T[], fn: (item: T, index: number, arr: T[]) => S | null) : S | undefined { 21 | for (let [index, item] of arr.entries()) { 22 | let value = fn(item, index, arr); 23 | 24 | if (value) { 25 | return value; 26 | } 27 | } 28 | 29 | return undefined; 30 | } 31 | -------------------------------------------------------------------------------- /app/vscode-extension/syntaxes/prl.tmLanguage.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", 3 | "name": "PRL", 4 | "patterns": [ 5 | { 6 | "name": "comment.line.number-sign", 7 | "match": "#.*$" 8 | }, 9 | { 10 | "match": "^[[:space:]]*(?:(-)[[:space:]])?([^/]+/)?([^:]+:)", 11 | "captures": { 12 | "1": { "name": "punctuation" }, 13 | "2": { "name": "entity.name.section" }, 14 | "3": { "name": "entity.name.constant" } 15 | } 16 | }, 17 | { 18 | "match": "^[[:space:]]*(-)", 19 | "captures": { 20 | "1": { "name": "punctuation" } 21 | } 22 | }, 23 | { 24 | "match": "^[[:space:]]*(|)(?:[^#\\\\]|\\\\#)*$", 25 | "captures": { 26 | "1": { "name": "punctuation" }, 27 | "2": { "name": "string" } 28 | } 29 | }, 30 | { 31 | "contentName": "meta.embedded.block.python", 32 | "begin": "[%@\\$]?{{", 33 | "end": "}}", 34 | "beginCaptures": { 35 | "0": { "name": "variable" } 36 | }, 37 | "endCaptures": { 38 | "0": { "name": "variable" } 39 | }, 40 | "patterns": [ 41 | { 42 | "include": "source.python" 43 | } 44 | ] 45 | }, 46 | { 47 | "name": "variable.other", 48 | "match": "(?:(?![%@\\$]?{{)[^#\\\\]|\\\\#)*" 49 | } 50 | ], 51 | "scopeName": "source.prl" 52 | } 53 | -------------------------------------------------------------------------------- /app/vscode-extension/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../shared/tsconfig.json", 3 | 4 | "compilerOptions": { 5 | "module": "CommonJS", 6 | "outDir": "lib" 7 | }, 8 | "files": [ 9 | "src/index.ts" 10 | ], 11 | "references": [ 12 | { "path": "../library" }, 13 | { "path": "../../client" } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | lib 3 | node_modules 4 | package-lock.json 5 | -------------------------------------------------------------------------------- /client/.madgerc: -------------------------------------------------------------------------------- 1 | { 2 | "detectiveOptions": { 3 | "ts": { 4 | "skipTypeImports": true 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /client/.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | lib 3 | styles 4 | tsconfig.json 5 | .madgerc 6 | 7 | build.js 8 | index.html 9 | service-worker.js 10 | -------------------------------------------------------------------------------- /client/scripts/external.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'immutable': 'libraries/immutable.js', 3 | 'pr1': 'index.js', 4 | 'react': 'libraries/react.js', 5 | 'react/jsx-runtime': 'libraries/react-jsx-runtime.js', 6 | 'react-dom': 'libraries/react-dom.js', 7 | 'react-dom/client': 'libraries/react-dom-client.js' 8 | } 9 | -------------------------------------------------------------------------------- /client/scripts/generate-html.js: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises'; 2 | import path from 'node:path'; 3 | import { fileURLToPath } from 'node:url'; 4 | 5 | import externalLibraries from './external.js'; 6 | 7 | 8 | let workingDirPath = path.dirname(path.dirname(fileURLToPath(import.meta.url))); 9 | 10 | let html = ` 11 | 12 | 13 | 14 | 15 | 16 | 23 | 24 | 25 |
26 | 30 | 31 | 32 | `; 33 | 34 | await fs.writeFile(path.join(workingDirPath, 'dist/index.html'), html); 35 | -------------------------------------------------------------------------------- /client/scripts/libraries/react-dom-client.js: -------------------------------------------------------------------------------- 1 | export { 2 | createRoot 3 | } from 'react-dom/client'; 4 | -------------------------------------------------------------------------------- /client/scripts/libraries/react-dom.js: -------------------------------------------------------------------------------- 1 | export { 2 | createPortal 3 | } from 'react-dom'; 4 | -------------------------------------------------------------------------------- /client/scripts/libraries/react-jsx-runtime.js: -------------------------------------------------------------------------------- 1 | export { 2 | Fragment, 3 | jsx, 4 | jsxs 5 | } from 'react/jsx-runtime'; 6 | -------------------------------------------------------------------------------- /client/scripts/libraries/react.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export { 4 | Children, 5 | cloneElement, 6 | Component, 7 | createContext, 8 | createElement, 9 | createFactory, 10 | createRef, 11 | forwardRef, 12 | Fragment, 13 | isValidElement, 14 | lazy, 15 | memo, 16 | Profiler, 17 | PureComponent, 18 | startTransition, 19 | StrictMode, 20 | Suspense, 21 | useCallback, 22 | useContext, 23 | useDebugValue, 24 | useDeferredValue, 25 | useEffect, 26 | useId, 27 | useImperativeHandle, 28 | useInsertionEffect, 29 | useLayoutEffect, 30 | useMemo, 31 | useReducer, 32 | useRef, 33 | useState, 34 | useSyncExternalStore, 35 | useTransition, 36 | version 37 | } from 'react'; 38 | 39 | export default React; 40 | -------------------------------------------------------------------------------- /client/scripts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module" 3 | } 4 | -------------------------------------------------------------------------------- /client/service-worker.js: -------------------------------------------------------------------------------- 1 | self.addEventListener('fetch', function (event) { 2 | let url = new URL(event.request.url); 3 | 4 | if (url.origin === 'https://cdn.jsdelivr.net') { 5 | event.respondWith( 6 | caches.open('cdn').then(function (cache) { 7 | return cache.match(event.request).then(function (response) { 8 | return ( 9 | response || 10 | fetch(event.request).then(function (response) { 11 | cache.put(event.request, response.clone()); 12 | return response; 13 | }) 14 | ); 15 | }); 16 | }), 17 | ); 18 | } 19 | }); 20 | -------------------------------------------------------------------------------- /client/src/backends/common.ts: -------------------------------------------------------------------------------- 1 | import type { UnitNamespace } from 'pr1-shared'; 2 | import type { Master } from '../interfaces/protocol'; 3 | 4 | 5 | /** @deprecated */ 6 | export type ChipId = string; 7 | 8 | /** @deprecated */ 9 | export type HostId = string; 10 | 11 | 12 | /** @deprecated */ 13 | export interface Chip { 14 | id: ChipId; 15 | condition: ChipCondition.Ok | ChipCondition.Partial | ChipCondition.Unrunnable; 16 | issues: ChipIssue[]; 17 | master: Master | null; 18 | readable: true; 19 | runners: Record; 20 | unitList: UnitNamespace[]; 21 | } 22 | 23 | /** @deprecated */ 24 | export interface UnreadableChip { 25 | id: ChipId; 26 | condition: ChipCondition.Unsupported | ChipCondition.Corrupted; 27 | issues: ChipIssue[]; 28 | readable: false; 29 | } 30 | 31 | /** @deprecated */ 32 | export type GeneralChip = Chip | UnreadableChip; 33 | 34 | 35 | /** @deprecated */ 36 | export enum ChipCondition { 37 | Ok = 0, 38 | Partial = 1, 39 | Unrunnable = 2, 40 | Unsupported = 3, 41 | Corrupted = 4 42 | } 43 | 44 | /** @deprecated */ 45 | export interface ChipIssue { 46 | message: string; 47 | } 48 | 49 | 50 | /** @deprecated */ 51 | export interface ProtocolLocation { 52 | segmentIndex: number; 53 | state: any; 54 | } 55 | -------------------------------------------------------------------------------- /client/src/components/bar-nav.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { Icon } from './icon'; 4 | import * as util from '../util'; 5 | 6 | 7 | export function BarNav(props: { 8 | entries: { 9 | id: Id; 10 | href: string; 11 | label: string; 12 | icon: string; 13 | disabled?: unknown; 14 | }[]; 15 | selectedEntryId: Id | null; 16 | }) { 17 | return ( 18 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /client/src/components/blank-state.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | 4 | export function BlankState(props: { 5 | action?: { 6 | label: string; 7 | onTrigger(): void; 8 | }; 9 | message: string; 10 | }) { 11 | return ( 12 |
13 |
14 |
{props.message}
15 | {props.action && ( 16 | 19 | )} 20 |
21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /client/src/components/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import formStyles from '../../styles/components/form.module.scss'; 4 | 5 | import * as util from '../util'; 6 | import { ShortcutGuide } from './shortcut-guide'; 7 | 8 | 9 | export function Button(props: React.PropsWithChildren<{ 10 | className?: string; 11 | disabled?: unknown; 12 | onClick(): void; 13 | shortcut?: string | null; 14 | }>) { 15 | return ( 16 | 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /client/src/components/chip-settings.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import type { Host } from '../host'; 4 | import { ErrorBoundary } from './error-boundary'; 5 | import { Chip, ChipId } from '../backends/common'; 6 | import { sortUnits } from '../sort'; 7 | import { Pool } from '../util'; 8 | 9 | 10 | export interface ChipSettingsProps { 11 | chipId: ChipId; 12 | host: Host; 13 | } 14 | 15 | export interface ChipSettingsState { 16 | 17 | } 18 | 19 | export class ChipSettings extends React.Component { 20 | pool = new Pool(); 21 | 22 | constructor(props: ChipSettingsProps) { 23 | super(props); 24 | } 25 | 26 | get chip(): Chip { 27 | return this.props.host.state.chips[this.props.chipId] as Chip; 28 | } 29 | 30 | render() { 31 | return ( 32 |
33 |
34 | {Object.values(this.props.host.units) 35 | .filter((unit) => this.chip.unitList.includes(unit.namespace) && unit.MatrixEditor) 36 | .sort(sortUnits) 37 | .map((unit) => { 38 | let Component = unit.MatrixEditor!; 39 | 40 | return ( 41 | <>Failed to render the settings editor of unit {unit.namespace}.} 43 | key={unit.namespace}> 44 | 47 | 48 | ); 49 | })} 50 |
51 |
52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /client/src/components/description.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import descriptionStyles from '../../styles/components/description.module.scss'; 4 | 5 | 6 | export function Description(props: React.PropsWithChildren<{}>) { 7 | return ( 8 |
9 | {props.children} 10 |
11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /client/src/components/draft-summary.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | 3 | import styles from '../../styles/components/diagnostics-summary.module.scss'; 4 | 5 | import { Icon } from '../components/icon'; 6 | import { formatClass } from '../util'; 7 | import { ShortcutGuide } from './shortcut-guide'; 8 | 9 | 10 | export function DraftSummary(props: { 11 | description?: ReactNode | null; 12 | message?: ReactNode | null; 13 | onTrigger?: (() => void) | null; 14 | status: 'default' | 'error' | 'success' | 'warning'; 15 | title: ReactNode; 16 | }) { 17 | return ( 18 |
24 | 30 |
{props.title}
31 | {props.description &&

{props.description}

} 32 | {props.onTrigger && ( 33 | 39 | )} 40 |
41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /client/src/components/error-boundary.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import * as util from '../util'; 4 | 5 | import styles from '../../styles/components/error-boundary.module.scss'; 6 | 7 | 8 | export type ErrorBoundaryProps = React.PropsWithChildren<{ 9 | getErrorMessage?(): JSX.Element; 10 | }>; 11 | 12 | export interface ErrorBoundaryState { 13 | hasError: boolean; 14 | } 15 | 16 | export class ErrorBoundary extends React.Component { 17 | override state: ErrorBoundaryState = { 18 | hasError: false 19 | }; 20 | 21 | override render() { 22 | if (this.state.hasError) { 23 | return ( 24 |
25 |

{this.props.getErrorMessage?.() ?? 'An error has occured.'}

28 |
29 | ); 30 | } 31 | 32 | return this.props.children; 33 | } 34 | 35 | static getDerivedStateFromError() { 36 | return { hasError: true }; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /client/src/components/expandable-text.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { PropsWithChildren, ReactNode, useEffect, useRef, useState } from 'react'; 3 | 4 | 5 | export function ExpandableText(props: PropsWithChildren<{ 6 | expandedValue?: ReactNode; 7 | }>) { 8 | let [width, setWidth] = useState(null); 9 | let ref = useRef(null); 10 | 11 | useEffect(() => { 12 | let rect = ref.current!.getBoundingClientRect(); 13 | setWidth(rect.width); 14 | }, []); 15 | 16 | return !width 17 | ? ( 18 |
{props.expandedValue ?? props.children}
19 | ) 20 | : ( 21 |
{props.children}
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /client/src/components/expression.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import styles from '../../styles/components/expression.module.scss'; 4 | 5 | 6 | export function Expression(props: { contents: string; }) { 7 | return ( 8 | {props.contents} 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /client/src/components/icon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import * as util from '../util'; 4 | 5 | 6 | export const Icon = React.memo(function Icon(props: { 7 | className?: string; 8 | name: string; 9 | style?: string; 10 | title?: string; 11 | }) { 12 | let disabled = props.name[0] === '-'; 13 | let name = disabled 14 | ? props.name.substring(1) 15 | : props.name; 16 | 17 | return ( 18 | 26 | {name} 27 | 28 | ); 29 | }); 30 | -------------------------------------------------------------------------------- /client/src/components/info-bar.tsx: -------------------------------------------------------------------------------- 1 | import { PropsWithChildren, ReactNode } from 'react'; 2 | 3 | import { formatClass } from '../util'; 4 | 5 | import styles from '../../styles/components/info-bar.module.scss'; 6 | 7 | 8 | export function InfoBar(props: PropsWithChildren<{ 9 | className?: string; 10 | left?: ReactNode; 11 | right?: ReactNode; 12 | mode?: 'default' | 'edit'; 13 | }>) { 14 | return ( 15 |
16 | {props.children} 17 |
18 |
{props.left}
19 |
{props.right}
20 |
21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /client/src/components/modal.tsx: -------------------------------------------------------------------------------- 1 | import { createPortal } from 'react-dom'; 2 | 3 | import styles from '../../styles/components/modal.module.scss'; 4 | 5 | import { Component, createRef } from 'react'; 6 | 7 | 8 | export type ModalProps = React.PropsWithChildren<{ 9 | onCancel(): void; 10 | }>; 11 | 12 | export interface ModalState { 13 | 14 | } 15 | 16 | export class Modal extends Component { 17 | private refDialog = createRef(); 18 | 19 | constructor(props: ModalProps) { 20 | super(props); 21 | 22 | this.state = {}; 23 | } 24 | 25 | override componentDidMount() { 26 | this.refDialog.current!.showModal(); 27 | } 28 | 29 | override render() { 30 | return ( 31 | createPortal(( 32 | { 37 | if (event.currentTarget === event.target) { 38 | this.props.onCancel(); 39 | } 40 | }} 41 | onKeyDown={(event) => { 42 | if (event.key === 'Escape') { 43 | event.stopPropagation(); 44 | } 45 | }}> 46 |
47 | {this.props.children} 48 |
49 |
50 | ), document.body) 51 | ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /client/src/components/modals/edit-protocol.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import descriptionStyles from '../../../styles/components/description.module.scss'; 3 | 4 | import { Modal } from '../modal'; 5 | import * as Form from '../standard-form'; 6 | 7 | 8 | export function EditProtocolModal(props: { 9 | onCancel(): void; 10 | onSubmit(mode: 'copy' | 'original'): void; 11 | originalAvailable: unknown; 12 | }) { 13 | return ( 14 | 15 |
{ 16 | event.preventDefault(); 17 | props.onSubmit('original'); 18 | }}> 19 |

Edit protocol

20 | 21 |

Do you wish to edit the original files used in this protocol or to edit a copy?

22 | 23 | 24 |
25 | 26 |
27 |
28 | void props.onSubmit('copy')} /> 29 | {!!props.originalAvailable && ( 30 | 31 | )} 32 |
33 |
34 |
35 |
36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /client/src/components/modals/unsaved-document.tsx: -------------------------------------------------------------------------------- 1 | import { FormEvent, useCallback } from 'react'; 2 | 3 | import descriptionStyles from '../../../styles/components/description.module.scss'; 4 | 5 | import { Modal } from '../modal'; 6 | import * as Form from '../standard-form'; 7 | 8 | 9 | export function UnsavedDocumentModal(props: { 10 | onFinish(result: 'cancel' | 'ignore' | 'save'): void; 11 | }) { 12 | let onCancel = useCallback(() => void props.onFinish('cancel'), [props.onFinish]); 13 | 14 | return ( 15 | 16 |
{ 17 | event.preventDefault(); 18 | props.onFinish('save'); 19 | }}> 20 |

Unsaved changes

21 | 22 |

Some files have unsaved changes.

23 | 24 | 25 |
26 | 27 |
28 |
29 | void props.onFinish('ignore')} /> 30 | 31 |
32 |
33 |
34 |
35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /client/src/components/overflowable-text.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | 4 | export function OverflowableText(props: React.PropsWithChildren<{}>) { 5 | let refTarget = React.useRef(); 6 | let child = props.children as any; 7 | 8 | React.useEffect(() => { 9 | let el = refTarget.current!; 10 | el.classList.toggle('_ellipsis', el.offsetWidth < el.scrollWidth); 11 | }, [child]); 12 | 13 | return React.cloneElement(child, { 14 | ref: (ref: HTMLElement) => { 15 | refTarget.current = ref; 16 | 17 | if (typeof child.ref === 'function') { 18 | child.ref(ref); 19 | } 20 | } 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /client/src/components/possible-link.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | 4 | export function PossibleLink(props: React.PropsWithChildren< 5 | ({ kind: 'anchor'; } & React.AnchorHTMLAttributes) | 6 | ({ kind: 'button'; } & React.ButtonHTMLAttributes) | 7 | ({ kind: 'div'; } & React.HTMLAttributes) 8 | >) { 9 | switch (props.kind) { 10 | case 'anchor': { 11 | let { children, kind, ...other } = props; 12 | return {children}; 13 | } 14 | 15 | case 'button': { 16 | let { children, kind, ...other } = props; 17 | return ; 18 | } 19 | 20 | case 'div': { 21 | let { children, kind, ...other } = props; 22 | return
{children}
; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /client/src/components/report-panel.tsx: -------------------------------------------------------------------------------- 1 | import { CompilationAnalysis, MasterAnalysis, createReport } from 'pr1-shared'; 2 | 3 | import styles from '../../styles/components/diagnostics-report.module.scss'; 4 | 5 | import { PanelPlaceholder } from '../libraries/panel'; 6 | import { Report } from './report'; 7 | 8 | 9 | export function ReportPanel(props: { 10 | compilationAnalysis: CompilationAnalysis | null; 11 | masterAnalysis?: MasterAnalysis; 12 | }) { 13 | if (!props.compilationAnalysis || ( 14 | props.compilationAnalysis.errors.length + 15 | props.compilationAnalysis.warnings.length + 16 | (props.masterAnalysis?.errors.length ?? 0) + 17 | (props.masterAnalysis?.warnings.length ?? 0) + 18 | (props.masterAnalysis?.effects.length ?? 0) 19 | ) < 1) { 20 | return ( 21 | 22 | ); 23 | } 24 | 25 | return ( 26 |
27 | 28 | {props.masterAnalysis && } 29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /client/src/components/selector.tsx: -------------------------------------------------------------------------------- 1 | import { OrdinaryId } from 'pr1-shared'; 2 | import * as React from 'react'; 3 | 4 | import { Icon } from './icon'; 5 | 6 | 7 | export function Selector(props: { 15 | entries: Entry[]; 16 | onSelect(id: EntryId): void; 17 | selectedEntryId: EntryId | null; 18 | }) { 19 | return ( 20 |
21 | {props.entries.map((entry) => ( 22 | 37 | ))} 38 |
39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /client/src/components/time-sensitive.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, useEffect } from 'react'; 2 | 3 | import { useForceUpdate } from '../util'; 4 | 5 | 6 | export function TimeSensitive(props: { 7 | contents(): ReactNode; 8 | interval: number | null; 9 | }) { 10 | let forceUpdate = useForceUpdate(); 11 | 12 | useEffect(() => { 13 | if (props.interval !== null) { 14 | let interval = setInterval(() => void forceUpdate(), Math.max(props.interval, 30)); 15 | 16 | return () => void clearInterval(interval); 17 | } 18 | 19 | return () => {}; 20 | }, [props.interval]); 21 | 22 | return ( 23 | <>{props.contents()} 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /client/src/constants.ts: -------------------------------------------------------------------------------- 1 | export const BaseUrlPathname = ''; 2 | export const BaseUrl = location.origin + BaseUrlPathname; 3 | -------------------------------------------------------------------------------- /client/src/contexts.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | import type { ContextMenuComponent } from './components/context-menu-area'; 4 | import type { StoreConsumer } from './store/types'; 5 | import type { ApplicationPersistentStoreEntries, ApplicationSessionStoreEntries } from './store/application'; 6 | 7 | 8 | export const ApplicationStoreContext = createContext>(null as any); 9 | export const ContextMenuContext = createContext(null); 10 | -------------------------------------------------------------------------------- /client/src/geometry.ts: -------------------------------------------------------------------------------- 1 | export interface Point { 2 | x: number; 3 | y: number; 4 | } 5 | 6 | export interface SideFlags { 7 | bottom: boolean; 8 | left: boolean; 9 | right: boolean; 10 | top: boolean; 11 | } 12 | 13 | export interface SideValues { 14 | bottom: number; 15 | left: number; 16 | right: number; 17 | top: number; 18 | } 19 | 20 | export interface Size { 21 | width: number; 22 | height: number; 23 | } 24 | 25 | 26 | export function squareDistance(a: Point, b: Point) { 27 | return (a.x - b.x) ** 2 + (a.y - b.y) ** 2; 28 | } 29 | 30 | export function squareLength(point: Point) { 31 | return point.x ** 2 + point.y ** 2; 32 | } 33 | 34 | 35 | export class RectSurface { 36 | readonly position: Point; 37 | readonly size: Size; 38 | 39 | constructor(position: Point, size: Size) { 40 | this.position = position; 41 | this.size = size; 42 | } 43 | 44 | get center(): Point { 45 | return { 46 | x: this.position.x + (this.size.width / 2), 47 | y: this.position.y + (this.size.height / 2) 48 | }; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /client/src/global-interfaces.d.ts: -------------------------------------------------------------------------------- 1 | import * as monaco from 'monaco-editor'; 2 | 3 | 4 | declare global { 5 | const navigation: any; 6 | 7 | interface Crypto { 8 | randomUUID(): string; 9 | } 10 | 11 | interface Document { 12 | adoptedStyleSheets: CSSStyleSheet[]; 13 | } 14 | 15 | interface Element { 16 | computedStyleMap(): { 17 | get(property: string): CSSUnparsedValue; 18 | }; 19 | } 20 | 21 | interface Keyboard { 22 | getLayoutMap(): Promise; 23 | } 24 | 25 | interface KeyboardLayoutMap { 26 | get(code: string): string | undefined; 27 | } 28 | 29 | interface Navigator { 30 | keyboard: Keyboard; 31 | } 32 | 33 | interface Window { 34 | MonacoEnvironment?: monaco.Environment | undefined; 35 | } 36 | 37 | namespace CSS { 38 | function number(value: number): any; 39 | } 40 | 41 | class CSSNumericValue { 42 | unit: string; 43 | value: number; 44 | 45 | static parse(cssText: CSSUnparsedValue | string): CSSNumericValue; 46 | } 47 | 48 | class CSSUnparsedValue { 49 | toString(): string; 50 | } 51 | 52 | class URLPattern { 53 | constructor(options: any): URLPattern; 54 | exec(input: any): any; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /client/src/host.ts: -------------------------------------------------------------------------------- 1 | import type { Client, ClientId, HostId, HostState } from 'pr1-shared'; 2 | 3 | import type { Plugins } from './interfaces/plugin'; 4 | import type { Units } from './interfaces/unit'; 5 | 6 | 7 | export interface Host { 8 | client: Client; 9 | clientId: ClientId; 10 | plugins: Plugins; 11 | state: HostState; 12 | staticUrl: string | null; 13 | 14 | /** @deprecated */ 15 | units: Units; 16 | } 17 | -------------------------------------------------------------------------------- /client/src/interfaces/draft.ts: -------------------------------------------------------------------------------- 1 | import type { Diagnostic, Protocol, Term } from 'pr1-shared'; 2 | import type { DraftCompletion, DraftFold, DraftHover, DraftMarker, DraftRelation, DraftRename, DraftSelection, DraftToken } from '../draft'; 3 | import type { DocumentPath } from '../app-backends/base'; 4 | 5 | 6 | export type HostDraftId = string; 7 | 8 | export interface HostDraft { 9 | id: HostDraftId; 10 | documents: HostDraftDocument[]; 11 | } 12 | 13 | export type HostDraftDocumentId = string; 14 | 15 | export interface HostDraftDocument { 16 | id: HostDraftDocumentId; 17 | contents: string | null; 18 | path: string[] | null; 19 | } 20 | 21 | export interface HostDraftCompilerOptions { 22 | trusted: boolean; 23 | } 24 | 25 | 26 | export interface HostDraftCompilerResult { 27 | analysis: DraftLanguageAnalysis; 28 | missingDocumentPaths: DocumentPath[]; 29 | protocol: Protocol | null; 30 | valid: boolean; 31 | 32 | study: { 33 | mark: HostDraftMark; 34 | point: unknown; 35 | } | null; 36 | } 37 | 38 | export interface HostDraftMark { 39 | childrenMarks: Record; 40 | childrenOffsets: Record; 41 | term: Term; 42 | } 43 | 44 | 45 | export interface DraftLanguageAnalysis { 46 | completions: DraftCompletion[]; 47 | errors: Diagnostic[]; 48 | folds: DraftFold[]; 49 | hovers: DraftHover[]; 50 | markers: DraftMarker[]; 51 | relations: DraftRelation[]; 52 | renames: DraftRename[]; 53 | selections: DraftSelection[]; 54 | tokens: DraftToken[]; 55 | warnings: Diagnostic[]; 56 | } 57 | -------------------------------------------------------------------------------- /client/src/interfaces/feature.ts: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from 'react'; 2 | 3 | 4 | /** @deprecated */ 5 | export interface Feature { 6 | accent?: unknown; 7 | description?: string | null; 8 | disabled?: unknown; 9 | error?: { 10 | kind: 'emergency' | 'error' | 'power' | 'shield' | 'warning'; 11 | message: string; 12 | } | null; 13 | icon: string; 14 | label: ReactNode; 15 | } 16 | -------------------------------------------------------------------------------- /client/src/interfaces/host.ts: -------------------------------------------------------------------------------- 1 | import type { Brand } from 'pr1-shared'; 2 | 3 | 4 | export interface HostInfo { 5 | id: HostInfoId; 6 | description: string | null; 7 | imageUrl: string | null; 8 | label: string; 9 | local: boolean; 10 | } 11 | 12 | export type HostInfoId = Brand; 13 | 14 | 15 | /* export type HostSettingsId = string; 16 | 17 | export interface HostSettings { 18 | id: HostSettingsId; 19 | label: string; 20 | options: HostSettingsOptions; 21 | } 22 | 23 | export interface HostSettingsOptionsLocal { 24 | type: 'local'; 25 | architecture: string | null; 26 | conf: any; 27 | dirPath: string; 28 | identifier: string; 29 | pythonPath: string; // | null; // null -> use embedded 30 | } 31 | 32 | export interface HostSettingsOptionsRemote { 33 | type: 'remote'; 34 | address: string; 35 | port: number; 36 | auth: null; 37 | } 38 | 39 | export type HostSettingsOptions = HostSettingsOptionsLocal | HostSettingsOptionsRemote; 40 | 41 | 42 | export type HostSettingsCollection = Record; 43 | 44 | export interface HostSettingsData { 45 | defaultHostSettingsId: HostSettingsId | null; 46 | hosts: HostSettingsCollection; 47 | } 48 | */ 49 | -------------------------------------------------------------------------------- /client/src/interfaces/misc.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | 3 | 4 | export interface AccessibleText { 5 | node: ReactNode; 6 | text: string; 7 | } 8 | -------------------------------------------------------------------------------- /client/src/interfaces/view.ts: -------------------------------------------------------------------------------- 1 | import { ComponentType } from 'react'; 2 | 3 | import type { Application } from '../application'; 4 | import type { Host } from '../host'; 5 | 6 | 7 | // Returns true => we can continue navigation 8 | export type UnsavedDataCallback = (newRoute: T | null) => Promise | boolean; 9 | 10 | export interface ViewProps { 11 | app: Application; 12 | host: Host; 13 | 14 | route: T; 15 | setUnsavedDataCallback(callback: UnsavedDataCallback | null): void; 16 | } 17 | 18 | export interface ViewHashOptions { 19 | app: Application; 20 | host: Host; 21 | route: T; 22 | } 23 | 24 | export type ViewType = ComponentType & { 25 | hash?(options: ViewHashOptions): string; 26 | routes: ViewRouteDef[]; 27 | } 28 | 29 | export interface ViewRouteDef { 30 | id: string; 31 | pattern: string; 32 | } 33 | 34 | export interface ViewRouteMatch { 35 | id: string; 36 | params: {}; 37 | state: unknown; 38 | } 39 | 40 | export interface ViewRouteMatchDefault { 41 | id: any; 42 | params: any; 43 | state: any; 44 | } 45 | -------------------------------------------------------------------------------- /client/src/plugin.ts: -------------------------------------------------------------------------------- 1 | import { ExperimentId, PluginName } from 'pr1-shared'; 2 | import { Application } from './application'; 3 | import { Host } from './host'; 4 | import { PluginContext } from './interfaces/plugin'; 5 | import { StoreConsumer, StoreEntries } from './store/types'; 6 | 7 | 8 | export function createPluginContext(app: Application, host: Host, namespace: PluginName): PluginContext { 9 | return { 10 | app, 11 | host: host, 12 | pool: app.pool, 13 | async requestToExecutor(request) { 14 | return await host.client.request({ 15 | type: 'requestToExecutor', 16 | data: request, 17 | namespace 18 | }); 19 | }, 20 | async requestToRunner(request, experimentId: ExperimentId) { 21 | return await host.client.request({ 22 | type: 'requestToRunner', 23 | experimentId, 24 | data: request, 25 | namespace 26 | }); 27 | }, 28 | store: { 29 | usePersistent: app.pluginStores[namespace]?.persistent.useEntry, 30 | useSession: app.pluginStores[namespace]?.session.useEntry 31 | } as StoreConsumer 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /client/src/rich-text.tsx: -------------------------------------------------------------------------------- 1 | import { RichText } from 'pr1-shared'; 2 | 3 | 4 | export function formatRichText(richText: RichText) { 5 | return richText.map((component, index) => { 6 | if (typeof component === 'string') { 7 | return component; 8 | } 9 | 10 | switch (component.type) { 11 | case 'code': 12 | return {formatRichText(component.value)} 13 | case 'link': 14 | return {formatRichText(component.value)} 15 | case 'strong': 16 | return {formatRichText(component.value)} 17 | } 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /client/src/sort.ts: -------------------------------------------------------------------------------- 1 | import seqOrd from 'seq-ord'; 2 | 3 | import { Unit } from './units'; 4 | 5 | 6 | export const sortUnits = seqOrd>(function* (a, b, rules) { 7 | if (a.namespace !== b.namespace) { 8 | if (a.namespace === 'metadata') { 9 | yield -1; 10 | } 11 | 12 | if (b.namespace === 'metadata') { 13 | yield 1; 14 | } 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /client/src/store/application.tsx: -------------------------------------------------------------------------------- 1 | import { StoreConsumer } from './types'; 2 | 3 | 4 | export enum GraphDirection { 5 | Horizontal = 0, 6 | Vertical = 1 7 | } 8 | 9 | export enum ShortcutDisplayMode { 10 | Disabled = 0, 11 | Normal = 1, 12 | Symbols = 2 13 | } 14 | 15 | export interface ApplicationPersistentStoreEntries { 16 | 'general.shortcutDisplayMode': ShortcutDisplayMode, 17 | 'graph.direction': GraphDirection, 18 | 'editor.automaticSave': boolean 19 | } 20 | 21 | export const ApplicationPersistentStoreDefaults: ApplicationPersistentStoreEntries = { 22 | 'general.shortcutDisplayMode': ShortcutDisplayMode.Disabled, 23 | 'graph.direction': GraphDirection.Vertical, 24 | 'editor.automaticSave': false 25 | }; 26 | 27 | 28 | export interface ApplicationSessionStoreEntries { 29 | 30 | } 31 | 32 | export const ApplicationSessionStoreDefaults: ApplicationSessionStoreEntries = { 33 | 34 | }; 35 | 36 | 37 | export type ApplicationStoreConsumer = StoreConsumer; 38 | -------------------------------------------------------------------------------- /client/src/store/base.ts: -------------------------------------------------------------------------------- 1 | export interface Store { 2 | /** 3 | * Read a value from the store. 4 | * 5 | * @param key The key to read. 6 | * @returns A promise that resolves to the value, or `undefined` if the key does not exist. 7 | */ 8 | read(key: string): Promise; 9 | 10 | /** 11 | * Read all values from the store. 12 | * 13 | * @returns An async iterable that yields key-value pairs. 14 | */ 15 | readAll(): AsyncIterable; 16 | 17 | /** 18 | * Write a value to the store. 19 | * 20 | * @param key The key to write. 21 | * @param value The value to write. 22 | */ 23 | write(key: string, value: unknown): Promise; 24 | } 25 | -------------------------------------------------------------------------------- /client/src/store/browser-storage.ts: -------------------------------------------------------------------------------- 1 | import { deserialize, serialize } from '../serialize-immutable'; 2 | import { Store } from './base'; 3 | 4 | 5 | export class BrowserStorageStore implements Store { 6 | constructor(private storage: Storage, private name: string) { 7 | 8 | } 9 | 10 | private prefixKey(key: string) { 11 | return `${this.name}/${key}`; 12 | } 13 | 14 | private unprefixKey(prefixedKey: string) { 15 | let prefix = `${this.name}/`; 16 | 17 | return prefixedKey.startsWith(prefix) 18 | ? prefixedKey.substring(prefix.length) 19 | : null; 20 | } 21 | 22 | async read(key: string) { 23 | let rawValue = this.storage.getItem(this.prefixKey(key)); 24 | 25 | return (rawValue !== null) 26 | ? deserialize(JSON.parse(rawValue)) 27 | : undefined; 28 | } 29 | 30 | async * readAll() { 31 | for (let index = 0; index < this.storage.length; index += 1) { 32 | let prefixedKey = this.storage.key(index)!; 33 | let key = this.unprefixKey(prefixedKey); 34 | 35 | if (key !== null) { 36 | let value = await this.read(key); 37 | yield [key, value] as const; 38 | } 39 | } 40 | } 41 | 42 | async write(key: string, value: unknown) { 43 | this.storage.setItem(this.prefixKey(key), JSON.stringify(serialize(value))); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /client/src/store/idb.ts: -------------------------------------------------------------------------------- 1 | import { createStore, entries, get, set } from 'idb-keyval'; 2 | 3 | import { Store } from './base'; 4 | import { deserialize, serialize } from '../serialize-immutable'; 5 | 6 | 7 | export class IDBStore implements Store { 8 | private store: ReturnType; 9 | 10 | constructor(options: { 11 | dbName: string; 12 | storeName: string; 13 | } = { 14 | dbName: 'pr1-store', 15 | storeName: 'store' 16 | }) { 17 | this.store = createStore(options.dbName, options.storeName); 18 | } 19 | 20 | async read(key: string) { 21 | return deserialize(await get(key, this.store)); 22 | } 23 | 24 | async * readAll() { 25 | for (let [key, value] of await entries(this.store)) { 26 | yield [key as string, deserialize(value)] as const; 27 | } 28 | } 29 | 30 | async write(key: string, value: unknown) { 31 | await set(key, serialize(value), this.store); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /client/src/store/memory-store.ts: -------------------------------------------------------------------------------- 1 | import { Store } from './base'; 2 | 3 | 4 | export class MemoryStore implements Store { 5 | private _entries = new Map(); 6 | 7 | async read(key: string) { 8 | return this._entries.get(key); 9 | } 10 | 11 | async * readAll() { 12 | for (let [key, value] of this._entries) { 13 | yield [key, value] as const; 14 | } 15 | } 16 | 17 | async write(key: string, value: unknown) { 18 | this._entries.set(key, value); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /client/src/store/types.ts: -------------------------------------------------------------------------------- 1 | import { UnionToIntersection } from 'pr1-shared'; 2 | 3 | import { SpecializedStoreManagerHook } from './store-manager'; 4 | 5 | 6 | export type StoreEntries = object; 7 | 8 | export interface StoreConsumer { 9 | usePersistent: StoreManagerHookFromEntries; 10 | useSession: StoreManagerHookFromEntries; 11 | } 12 | 13 | export type StoreManagerHookFromEntries = UnionToIntersection<{ 14 | [S in keyof T]: SpecializedStoreManagerHook; 15 | }[keyof T]>; 16 | 17 | export type StoreManagerReadFromEntries = UnionToIntersection<{ 18 | [S in keyof T]: (key: S) => T[S]; 19 | }[keyof T]>; 20 | -------------------------------------------------------------------------------- /client/src/term.ts: -------------------------------------------------------------------------------- 1 | import type { Term } from 'pr1-shared'; 2 | 3 | 4 | export function getDateFromTerm(term: Term, refDate: number) { 5 | switch (term.type) { 6 | case 'datetime': 7 | return term.value; 8 | case 'duration': 9 | return refDate + term.value; 10 | default: 11 | return null; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /client/src/types.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.module.scss' { 2 | const content: any; 3 | export default content; 4 | } 5 | 6 | declare module '*.jpeg' { 7 | const content: string; 8 | export default content; 9 | } 10 | -------------------------------------------------------------------------------- /client/src/ureg.ts: -------------------------------------------------------------------------------- 1 | import { UnitRegistry } from 'quantops'; 2 | 3 | 4 | export const ureg = new UnitRegistry(); 5 | -------------------------------------------------------------------------------- /client/src/views/graph.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { GraphEditor } from '../components/graph-editor'; 4 | 5 | 6 | export interface ViewGraphProps { 7 | 8 | } 9 | 10 | export interface ViewGraphState { 11 | 12 | } 13 | 14 | export class ViewGraph extends React.Component { 15 | constructor(props: ViewGraphProps) { 16 | super(props); 17 | 18 | this.state = {}; 19 | } 20 | 21 | render() { 22 | return ( 23 |
24 |
25 |

Graph

26 |
27 | 28 |
29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /client/src/views/plugin-view.tsx: -------------------------------------------------------------------------------- 1 | import viewStyles from '../../styles/components/view.module.scss'; 2 | 3 | import { ViewExperiments } from './experiments'; 4 | import { ViewProps } from '../interfaces/view'; 5 | import { PluginName } from 'pr1-shared'; 6 | import { useEffect } from 'react'; 7 | import { createPluginContext } from '../plugin'; 8 | 9 | 10 | export function ViewPluginView(props: ViewProps) { 11 | let namespace = (props.route.params.namespace as PluginName); 12 | let plugin = props.host.plugins[namespace]; 13 | let viewEntry = plugin?.views?.find((viewEntry) => (viewEntry.id === props.route.params.id)); 14 | 15 | useEffect(() => { 16 | if (!viewEntry) { 17 | ViewExperiments.navigate(); 18 | } 19 | }, []); 20 | 21 | if (!viewEntry) { 22 | return null; 23 | } 24 | 25 | let Component = viewEntry.Component; 26 | 27 | return ( 28 |
29 | 31 |
32 | ); 33 | } 34 | 35 | 36 | ViewPluginView.routes = [ 37 | { id: '_', pattern: `/unit/:namespace/:id` } 38 | ]; 39 | -------------------------------------------------------------------------------- /client/src/websocket.ts: -------------------------------------------------------------------------------- 1 | import ModernWebsocket from 'modern-websocket'; 2 | import { ClientBackend, ClientProtocol, deserializeMessagesOfIterator, serializeMessage, ServerProtocol } from 'pr1-shared'; 3 | 4 | 5 | export class WebsocketBackend implements ClientBackend { 6 | closed: Promise; 7 | messages: AsyncIterator; 8 | ready: Promise; 9 | 10 | private socket: ModernWebsocket; 11 | 12 | constructor(url: string) { 13 | this.socket = new ModernWebsocket(url); 14 | 15 | this.closed = this.socket.closed.then(() => {}); 16 | this.ready = this.socket.ready; 17 | 18 | this.messages = deserializeMessagesOfIterator(this.socket.iter()); 19 | } 20 | 21 | close() { 22 | this.socket.close(); 23 | } 24 | 25 | send(message: ClientProtocol.Message) { 26 | this.socket.send(serializeMessage(message)); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /client/static/logo.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adaptyvbio/automancer/d577cc1775767b5d4839f908410ca5c4a0dadb9f/client/static/logo.jpeg -------------------------------------------------------------------------------- /client/styles/_constants.scss: -------------------------------------------------------------------------------- 1 | $app-blue: #0074d9; 2 | $app-green: darken(#2ecc40, 4%); 3 | $app-orange: #d96500; 4 | $app-purple: #85144b; 5 | $app-red: #ff4136; 6 | $app-turquoise: #07e7c4; 7 | 8 | $main-font: ui-sans-serif, system-ui, "Helvetica Neue", Helvetica, sans-serif; 9 | $monospace-font: Menlo, "Cascadia Mono", "Courier New", ui-monospace, monospace; 10 | $line-height: 1.3; 11 | $icon-gap: 0.6rem; 12 | $icon-grey: #666; 13 | $icon-size: 24px; 14 | 15 | $highlight-blue: #0366d6; 16 | $highlight-shadow: 0 0 0 3px rgba($highlight-blue, 40%); 17 | 18 | $dur-short: 150ms; 19 | $dur-medium: 250ms; 20 | $dur-long: 350ms; 21 | 22 | $titlebar-height: 40px; 23 | $separator-border-color: #ddd; 24 | -------------------------------------------------------------------------------- /client/styles/_mixins.scss: -------------------------------------------------------------------------------- 1 | @use "sass:math"; 2 | 3 | 4 | @mixin border-radius($top: -1, $right: -1, $bottom: -1, $left: -1) { 5 | @if $top >= 0 and $bottom >= 0 { 6 | border-radius: $top $top $bottom $bottom; 7 | } @else if $top >= 0 { 8 | border-top-left-radius: $top; 9 | border-top-right-radius: $top; 10 | } @else if $bottom >= 0 { 11 | border-bottom-left-radius: $bottom; 12 | border-bottom-right-radius: $bottom; 13 | } 14 | 15 | @if $left >= 0 and $right >= 0 { 16 | border-radius: $left $right $right $left; 17 | } @else if $left >= 0 { 18 | border-top-left-radius: $left; 19 | border-bottom-left-radius: $left; 20 | } @else if $right >= 0 { 21 | border-top-right-radius: $right; 22 | border-bottom-right-radius: $right; 23 | } 24 | } 25 | 26 | @mixin icon($fill: false, $size: 24px, $style: "sharp", $weight: 200) { 27 | font-family: map-get(( 28 | "rounded": "Material Symbols Rounded", 29 | "sharp": "Material Symbols Sharp", 30 | ), $style); 31 | font-size: $size; 32 | font-variation-settings: 33 | "FILL" if($fill, 1, 0), 34 | "wght" $weight, 35 | "GRAD" 200, 36 | "opsz" math.div($size, 1px); 37 | } 38 | 39 | @mixin no-scrollbar() { 40 | & { 41 | scrollbar-width: none; 42 | } 43 | 44 | &::-webkit-scrollbar { 45 | display: none; 46 | } 47 | } 48 | 49 | @mixin superimposed-children() { 50 | display: grid; 51 | grid: [track] 100% / [track] 100%; 52 | 53 | > * { 54 | grid-area: track; 55 | } 56 | } 57 | 58 | @mixin text-ellipsis { 59 | overflow-x: hidden; 60 | text-overflow: ellipsis; 61 | white-space: nowrap; 62 | } 63 | -------------------------------------------------------------------------------- /client/styles/common/_buttons.scss: -------------------------------------------------------------------------------- 1 | @use "../constants" as *; 2 | 3 | 4 | .btn { 5 | display: flex; 6 | align-items: center; 7 | column-gap: $icon-gap; 8 | 9 | width: auto; 10 | height: 2.6rem; 11 | 12 | padding: 0 1.4rem; 13 | border-radius: 5px; 14 | 15 | background-color: #222; 16 | background-image: linear-gradient(transparent, #fff1); 17 | 18 | color: #fff; 19 | font-weight: 500; 20 | 21 | transition: background-color $dur-short, box-shadow $dur-short; 22 | 23 | &:hover { 24 | background-color: #111; 25 | box-shadow: 2px 2px 6px #0003; 26 | } 27 | 28 | .btn-icon { 29 | display: flex; 30 | width: $icon-size; 31 | 32 | font-variation-settings: 33 | 'FILL' 1, 34 | 'wght' 400, 35 | 'GRAD' 0, 36 | 'opsz' 48; 37 | } 38 | } 39 | 40 | 41 | .actions { 42 | display: flex; 43 | column-gap: 0.4rem; 44 | 45 | > * { 46 | flex-shrink: 0; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /client/styles/common/_header.scss: -------------------------------------------------------------------------------- 1 | .header { 2 | display: grid; 3 | grid-template-columns: minmax(300px, 1fr) auto; 4 | align-items: center; 5 | } 6 | 7 | .header--1 { 8 | margin: 0 0 2rem 0; 9 | 10 | &:first-child { 11 | margin-top: 0.8rem; 12 | } 13 | } 14 | 15 | .header--2 { 16 | margin: 2.4rem 0 1.2rem 0; 17 | 18 | &:first-child { 19 | margin-top: 0.8rem; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /client/styles/common/_misc.scss: -------------------------------------------------------------------------------- 1 | @use "./buttons"; 2 | 3 | 4 | .illustrated { 5 | &-root { 6 | display: flex; 7 | align-items: center; 8 | column-gap: 0.4rem; 9 | } 10 | 11 | &-icon { 12 | display: flex; 13 | } 14 | } 15 | 16 | .superimposed { 17 | &-root { 18 | display: grid; 19 | } 20 | 21 | &-target, &-visible { 22 | grid-column: 1; 23 | grid-row: 1; 24 | } 25 | 26 | &-target { 27 | opacity: 0; 28 | } 29 | 30 | &-root:hover &-visible.btn { 31 | @extend :hover; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /client/styles/common/_mixins.scss: -------------------------------------------------------------------------------- 1 | @use "../constants" as *; 2 | 3 | 4 | @mixin card() { 5 | border: 1px solid #ddd; 6 | border-radius: 6px; 7 | box-shadow: 2px 2px 6px rgba(#000, 2%); 8 | 9 | --card-transitions: border-color #{$dur-short}, box-shadow #{$dur-short}; 10 | transition: var(--card-transitions); 11 | 12 | &:focus-visible, &._context { 13 | outline: 2px solid #000; 14 | outline-offset: 3px; 15 | } 16 | 17 | &:disabled { 18 | border-color: #eaeaea; 19 | box-shadow: none; 20 | 21 | color: #666; 22 | } 23 | 24 | &:hover, &._context { 25 | border-color: #ccc; 26 | box-shadow: 2px 2px 6px rgba(#000, 6%); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /client/styles/components/_protocol-config.scss: -------------------------------------------------------------------------------- 1 | @use "../constants" as *; 2 | 3 | 4 | .pconfig { 5 | &-root { 6 | display: grid; 7 | align-items: baseline; 8 | grid-template-columns: 200px 1fr; 9 | row-gap: 1rem; 10 | 11 | margin: 1rem 0; 12 | } 13 | 14 | &-section { 15 | font-size: 1.2rem; 16 | font-weight: 600; 17 | } 18 | 19 | 20 | &-form { 21 | display: flex; 22 | flex-direction: column; 23 | row-gap: 0.2rem; 24 | } 25 | 26 | &-entry { 27 | display: grid; 28 | align-items: center; 29 | justify-items: start; 30 | grid-template-columns: 100px auto; 31 | 32 | &-label { 33 | font-weight: 500; 34 | } 35 | 36 | // &-input { } 37 | } 38 | 39 | &-submit { 40 | margin: 2rem 0 1rem 0; 41 | padding: 0.8rem 1.4rem; 42 | 43 | border: 1px solid #ddd; 44 | border-radius: 4px; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /client/styles/components/_protocol-status.scss: -------------------------------------------------------------------------------- 1 | @use "../common/buttons"; 2 | @use "./protocol-overview" as proto; 3 | 4 | 5 | .pstatus { 6 | &-root { 7 | max-width: 500px; 8 | margin-top: 0.8rem; 9 | } 10 | 11 | &-subtitle { 12 | margin-bottom: 0.2rem; 13 | color: #666; 14 | } 15 | 16 | &-header { 17 | display: grid; 18 | align-items: baseline; 19 | column-gap: 1rem; 20 | grid-template-columns: 1fr auto; 21 | } 22 | 23 | &-time { 24 | color: #666; 25 | } 26 | 27 | &-features { 28 | @extend %features; 29 | --background: #fff; 30 | 31 | margin: 1rem 0; 32 | } 33 | 34 | &-actions { 35 | display: flex; 36 | column-gap: 0.4rem; 37 | 38 | margin: 1rem 0; 39 | } 40 | 41 | &-button { 42 | @extend .btn; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /client/styles/components/_protocol-timeline.scss: -------------------------------------------------------------------------------- 1 | @use "../constants" as *; 2 | 3 | 4 | .timeline { 5 | &-root { 6 | margin: 1rem 0; 7 | 8 | text { 9 | dominant-baseline: middle; 10 | text-anchor: middle; 11 | } 12 | } 13 | 14 | &-line:first-of-type, &-line:last-of-type { 15 | stroke-dasharray: 0 4 100000; 16 | stroke-dashoffset: 4; 17 | 18 | transition: color $dur-short, stroke-dashoffset $dur-short; 19 | } 20 | 21 | &-line:last-of-type { 22 | stroke-dashoffset: 0; 23 | } 24 | 25 | &-line, &-stagename, &-segment * { 26 | $a: calc(255 * (1 - var(--opacity))); 27 | color: rgb(#{$a}, #{$a}, #{$a}); 28 | transition: color $dur-short; 29 | } 30 | 31 | &-root:hover &-stage, &-root:hover > &-segment { 32 | --opacity: 0.4; 33 | } 34 | 35 | &-root:hover &-stage:hover, &-root:hover &-stage:hover + &-stage &-segment:first-of-type, &-root:hover &-stage:hover + &-segment { 36 | --opacity: 1; 37 | } 38 | 39 | &-stage:hover &-line:last-of-type { 40 | stroke-dashoffset: 4; 41 | } 42 | 43 | &-stage:hover + &-stage &-line:first-of-type { 44 | stroke-dashoffset: 0; 45 | } 46 | 47 | &-stage:not(:hover) &-segmentlabel { 48 | opacity: 0; 49 | transform: translateY(+5%); 50 | } 51 | 52 | &-stagename { 53 | cursor: default; 54 | font-weight: 500; 55 | } 56 | 57 | &-stagelabel, &-segmentlabel { 58 | pointer-events: none; 59 | font-weight: 500; 60 | } 61 | 62 | &-segmentlabel { 63 | fill: #666; 64 | font-size: 0.9rem; 65 | transition: opacity $dur-short, transform $dur-short; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /client/styles/components/application.module.scss: -------------------------------------------------------------------------------- 1 | @use "../constants" as *; 2 | @use "../mixins" as *; 3 | 4 | 5 | .root { 6 | display: grid; 7 | grid-template: $titlebar-height 1fr / auto 1fr; 8 | 9 | height: 100%; 10 | 11 | // Necessary for error boundaries as they do not render the title bar. 12 | > :nth-child(2):last-child { 13 | grid-row: 1 / -1; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /client/styles/components/diagnostics-summary.module.scss: -------------------------------------------------------------------------------- 1 | @use "../constants" as *; 2 | @use "../mixins" as *; 3 | 4 | 5 | $report-icon-size: 32px; 6 | 7 | .root { 8 | display: grid; 9 | align-items: center; 10 | gap: 0 0.8rem; 11 | grid-template-columns: $report-icon-size 1fr; 12 | 13 | min-height: 62px; 14 | padding: 0 1rem; 15 | 16 | background-color: #fff; 17 | border: 1px solid #eee; 18 | border-radius: 6px; 19 | box-shadow: 1px 1px 1px #0002; 20 | } 21 | 22 | .root:has(.actionRoot) { 23 | grid-template-columns: $report-icon-size 1fr auto; 24 | } 25 | 26 | .icon { 27 | @include icon($fill: true, $size: 24px); 28 | 29 | grid-row: 1 / span 2; 30 | 31 | color: $icon-grey; 32 | font-size: $report-icon-size; 33 | } 34 | 35 | .rootSuccess .icon { color: $app-green; } 36 | .rootError .icon { color: $app-red; } 37 | .rootWarning .icon { color: $app-orange; } 38 | 39 | .title { 40 | @include text-ellipsis(); 41 | 42 | align-self: end; 43 | font-weight: 600; 44 | } 45 | 46 | .root:not(:has(.description)) .title { 47 | align-self: unset; 48 | grid-row: 1 / span 2; 49 | } 50 | 51 | .description { 52 | @include text-ellipsis(); 53 | 54 | align-self: start; 55 | color: #666; 56 | } 57 | 58 | .action { 59 | &Root { 60 | display: flex; 61 | align-items: center; 62 | column-gap: $icon-gap; 63 | 64 | grid-column: 3; 65 | grid-row: 1 / span 2; 66 | 67 | padding: 0.2rem 0.8rem 0.2rem 0.6rem; 68 | border-radius: 4px; 69 | 70 | &:hover { 71 | background-color: #f6f6f6; 72 | } 73 | } 74 | 75 | &Icon { 76 | @include icon($size: 24px); 77 | } 78 | 79 | &Label { 80 | font-weight: 600; 81 | text-transform: uppercase; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /client/styles/components/editor.module.scss: -------------------------------------------------------------------------------- 1 | @use "../constants" as *; 2 | 3 | 4 | $border-color: #ddd; 5 | 6 | .editorPanel { 7 | display: grid; 8 | grid-template-rows: auto 1fr; 9 | 10 | > :only-child { 11 | grid-row: 1 / -1; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /client/styles/components/error-boundary.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | display: flex; 3 | min-height: 4rem; 4 | 5 | background-color: #ff413633; 6 | 7 | p { 8 | margin: auto; 9 | max-width: 400px; 10 | 11 | color: #ff4136; 12 | font-weight: 500; 13 | text-align: center; 14 | } 15 | 16 | button { 17 | display: inline; 18 | width: auto; 19 | 20 | text-decoration: underline; 21 | text-underline-position: under; 22 | } 23 | 24 | strong { 25 | font-style: italic; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /client/styles/components/expression.module.scss: -------------------------------------------------------------------------------- 1 | @use "../constants" as *; 2 | @use "../mixins" as *; 3 | 4 | 5 | .root { 6 | background-color: #0074d933; 7 | border-radius: 4px; 8 | 9 | padding: 2px 0.5ch; 10 | 11 | @include text-ellipsis(); 12 | 13 | font-family: $monospace-font; 14 | font-size: 0.75em; 15 | } 16 | -------------------------------------------------------------------------------- /client/styles/components/info-bar.module.scss: -------------------------------------------------------------------------------- 1 | @use "../constants" as *; 2 | 3 | 4 | $border-color: #ddd; 5 | 6 | .container { 7 | display: grid; 8 | grid-template-rows: 1fr auto; 9 | } 10 | 11 | .root { 12 | display: flex; 13 | justify-content: space-between; 14 | height: 24px; 15 | 16 | padding: 0 0.4rem; 17 | border-top: 1px solid $border-color; 18 | 19 | font-size: 0.9rem; 20 | 21 | &[data-mode="default"] { 22 | --hover-color: #f0f0f0; 23 | } 24 | 25 | &[data-mode="edit"] { 26 | background-color: #3d9970; 27 | --hover-color: #{lighten(#3d9970, 4%)}; 28 | 29 | color: #fff; 30 | } 31 | 32 | > * { 33 | display: flex; 34 | align-items: center; 35 | 36 | > * { 37 | padding: 0 0.6rem; 38 | } 39 | 40 | > button { 41 | width: auto; 42 | align-self: stretch; 43 | 44 | &:hover { 45 | background-color: var(--hover-color); 46 | } 47 | 48 | &::before { 49 | content: "["; 50 | } 51 | 52 | &::after { 53 | content: "]"; 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /client/styles/components/modal.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | display: flex; 3 | width: 100%; 4 | height: 100%; 5 | 6 | overflow-y: auto; 7 | overscroll-behavior: contain; 8 | 9 | padding: 100px 20px; 10 | 11 | &::backdrop { 12 | background-color: #0003; 13 | } 14 | } 15 | 16 | .container { 17 | max-width: 800px; 18 | width: 100%; 19 | 20 | margin: auto; 21 | padding: 2rem 2.6rem; 22 | 23 | background-color: #fff; 24 | border-radius: 8px; 25 | box-shadow: 3px 3px 6px #0001; 26 | 27 | > * > :last-child { 28 | margin-top: 2rem; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /client/styles/components/progress-bar.module.scss: -------------------------------------------------------------------------------- 1 | @use "../constants" as *; 2 | 3 | 4 | .root { 5 | display: grid; 6 | align-items: center; 7 | column-gap: 1rem; 8 | grid-template-columns: 1fr auto; 9 | 10 | margin: 1rem 0; 11 | } 12 | 13 | .outer { 14 | display: grid; 15 | 16 | border: 1px solid #ddd; 17 | border-radius: 6px; 18 | 19 | height: 12px; 20 | padding: 0 4px; 21 | 22 | .root:global(._writable) & { 23 | cursor: pointer; 24 | } 25 | } 26 | 27 | .inner, .progress, .select { 28 | width: 100%; 29 | height: 4px; 30 | 31 | margin: auto 0; 32 | border-radius: 4px; 33 | 34 | grid-column: 1; 35 | grid-row: 1; 36 | } 37 | 38 | .progress { 39 | background-color: $app-blue; 40 | 41 | .root:global(._paused) & { 42 | background-color: #666; 43 | } 44 | } 45 | 46 | .select { 47 | background-color: #0003; 48 | } 49 | 50 | .outer + * { 51 | color: #999; 52 | font-variant-numeric: tabular-nums; 53 | text-align: right; 54 | } 55 | -------------------------------------------------------------------------------- /client/styles/components/shortcut-guide.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | display: flex; 3 | align-items: center; 4 | column-gap: 0.4rem; 5 | justify-content: center; 6 | } 7 | 8 | .shortcut { 9 | text-transform: initial; 10 | 11 | kbd { 12 | padding: 0.12rem 0.2rem; 13 | 14 | background-color: #aaa3; 15 | border-radius: 4px; 16 | 17 | font-size: 0.7rem; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /client/styles/components/split-panels.module.scss: -------------------------------------------------------------------------------- 1 | @use "../constants" as *; 2 | 3 | 4 | $border-color: #ddd; 5 | 6 | 7 | .root { 8 | display: grid; 9 | height: 100%; 10 | overflow: hidden; 11 | 12 | > :nth-child(odd) { 13 | overflow-x: hidden; 14 | } 15 | 16 | > :nth-child(even) { 17 | cursor: col-resize; 18 | pointer-events: initial; 19 | position: relative; 20 | z-index: 1; 21 | 22 | &::before, &::after { 23 | content: ""; 24 | 25 | position: absolute; 26 | inset: 0; 27 | 28 | height: 100%; 29 | } 30 | 31 | // Hover area 32 | &::before { 33 | width: 600%; 34 | translate: calc(0.5px - 50%) 0; 35 | } 36 | 37 | // Blue handle 38 | &::after { 39 | width: 100%; 40 | background-color: $border-color; 41 | } 42 | 43 | &:is(:hover, :global(._dragging))::after { 44 | background-color: $app-blue; 45 | scale: 4 1; 46 | } 47 | 48 | &:hover::after { 49 | transition: background-color $dur-short 300ms, scale $dur-short 300ms; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /client/styles/components/static-select.module.scss: -------------------------------------------------------------------------------- 1 | @use "../mixins" as *; 2 | 3 | 4 | .root { 5 | // @include superimposed-children(); 6 | position: relative; 7 | 8 | select { 9 | position: absolute; 10 | inset: 0; 11 | opacity: 0; 12 | } 13 | 14 | :global(.icon) { 15 | @include icon($size: 20px); 16 | 17 | line-height: 0; 18 | vertical-align: middle; 19 | 20 | translate: 0 -1px; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /client/styles/components/text-editor.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | position: relative; 3 | overflow: hidden; 4 | 5 | > :first-child { 6 | height: 100%; 7 | } 8 | } 9 | 10 | .summary { 11 | $scrollbar-width: 14px; 12 | // scrollbar width = 14px 13 | 14 | width: 400px; 15 | max-width: calc(100% - #{$scrollbar-width} - 2 * 6px); 16 | 17 | position: absolute; 18 | right: $scrollbar-width + 6px; 19 | bottom: 8px; 20 | } 21 | -------------------------------------------------------------------------------- /client/styles/components/view.module.scss: -------------------------------------------------------------------------------- 1 | @use "../constants" as *; 2 | 3 | 4 | .root { 5 | display: contents; 6 | } 7 | 8 | .contents { 9 | grid-column: 2; 10 | grid-row: 2; 11 | 12 | overflow-y: auto; 13 | } 14 | 15 | .legacy { 16 | padding: 1.4rem 3rem; 17 | } 18 | 19 | 20 | .blank { 21 | &Outer { 22 | display: flex; 23 | } 24 | 25 | &Inner { 26 | margin: auto; 27 | max-width: 350px; 28 | 29 | p { 30 | margin: 1rem 0; 31 | text-align: center; 32 | } 33 | } 34 | 35 | &Actions { 36 | display: flex; 37 | column-gap: 0.6rem; 38 | justify-content: center; 39 | 40 | margin: 1rem 0; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /client/styles/main.scss: -------------------------------------------------------------------------------- 1 | @use "./constants" as *; 2 | 3 | @use "./common/buttons"; 4 | @use "./common/header"; 5 | @use "./common/mixins"; 6 | @use "./common/misc"; 7 | @use "./components/barnav"; 8 | @use "./components/chip-list"; 9 | @use "./components/context-menu"; 10 | @use "./components/protocol-config"; 11 | @use "./components/protocol-overview"; 12 | @use "./components/protocol-status"; 13 | @use "./components/protocol-timeline"; 14 | @use "./components/selector"; 15 | @use "./components/standard-form"; 16 | @use "./components/text-editor"; 17 | @use "./components/visual-editor"; 18 | @use "./mcontrol"; 19 | @use "./reset"; 20 | // @use "./sidebar"; 21 | @use "./startup"; 22 | @use "./views/protocols"; 23 | 24 | 25 | html { 26 | block-size: 100%; 27 | 28 | font-size: 14px; 29 | line-height: $line-height; 30 | } 31 | 32 | body { 33 | block-size: 100%; 34 | 35 | font-family: $main-font; 36 | } 37 | 38 | code { 39 | font-family: $monospace-font; 40 | } 41 | 42 | #root { 43 | height: 100%; 44 | } 45 | -------------------------------------------------------------------------------- /client/styles/views/_protocols.scss: -------------------------------------------------------------------------------- 1 | @use "../common/mixins" as common; 2 | @use "../constants" as *; 3 | 4 | 5 | .lproto { 6 | &-container { 7 | max-width: 600px; 8 | } 9 | 10 | &-list { 11 | display: flex; 12 | flex-direction: column; 13 | row-gap: 0.8rem; 14 | } 15 | 16 | &-entry { 17 | @include common.card(); 18 | 19 | display: grid; 20 | gap: 0.4rem 2rem; 21 | grid-template: auto auto / 1fr auto; 22 | 23 | padding: 1rem 1.4rem; 24 | 25 | @at-root :not(a)#{&} { 26 | cursor: default; 27 | } 28 | } 29 | 30 | &-label { 31 | font-size: 1.2rem; 32 | font-weight: 500; 33 | } 34 | 35 | &-property { 36 | &-list { 37 | display: flex; 38 | column-gap: 1.6rem; 39 | 40 | grid-row: 2; 41 | } 42 | 43 | &-item { 44 | display: flex; 45 | align-items: center; 46 | column-gap: $icon-gap; 47 | 48 | color: #666; 49 | } 50 | } 51 | 52 | &-action { 53 | display: flex; 54 | align-self: start; 55 | 56 | grid-column: 2; 57 | grid-row: 1 / -1; 58 | 59 | transition: opacity $dur-short, transform $dur-short; 60 | } 61 | 62 | &-entry:not(:focus-visible, :hover) &-action { 63 | opacity: 0; 64 | transform: translateX(-60%); 65 | } 66 | 67 | &-entry._context &-action { 68 | transition: none; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /client/superstatic.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [ 3 | { "source": "**", "destination": "/index.html" } 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../shared/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "composite": true, 6 | "emitDeclarationOnly": true, 7 | "esModuleInterop": true, 8 | "jsx": "react-jsx", 9 | "module": "es2020", 10 | "moduleResolution": "node", 11 | "outDir": "lib/types", 12 | "rootDir": "src", 13 | }, 14 | "files": [ 15 | "src/index.tsx" 16 | ], 17 | "include": [ 18 | "src/**/*" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /host/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | __pycache__ 4 | *.egg-info 5 | -------------------------------------------------------------------------------- /host/automancer/__init__.py: -------------------------------------------------------------------------------- 1 | from pr1 import * 2 | -------------------------------------------------------------------------------- /host/pr1/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | logger = logging.getLogger("pr1.host") 4 | 5 | from .analysis import * 6 | from .devices.claim import * 7 | from .devices.nodes.collection import * 8 | from .devices.nodes.common import * 9 | from .devices.nodes.numeric import * 10 | from .devices.nodes.primitive import * 11 | from .devices.nodes.readable import * 12 | from .devices.nodes.value import * 13 | from .devices.nodes.watcher import * 14 | from .error import * 15 | from .eta import * 16 | from .host import * 17 | from .input import * 18 | from .input.dynamic import * 19 | from .input.file import * 20 | from .langservice import * 21 | from .master.analysis import * 22 | from .plugin.manager import * 23 | from .procedure import * 24 | from .rich_text import * 25 | from .staticanalysis.expr import * 26 | from .staticanalysis.expression import * 27 | from .staticanalysis.support import * 28 | from .staticanalysis.types import * 29 | from .units.base import * 30 | from .ureg import * 31 | from .util.asyncio import * 32 | from .util.decorators import * 33 | from .util.misc import * 34 | from .util.pool import * 35 | 36 | from .fiber.expr import * 37 | from .fiber.master2 import * 38 | from .fiber.parser import * 39 | from .fiber.process import * 40 | -------------------------------------------------------------------------------- /host/pr1/devices/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adaptyvbio/automancer/d577cc1775767b5d4839f908410ca5c4a0dadb9f/host/pr1/devices/__init__.py -------------------------------------------------------------------------------- /host/pr1/devices/nodes/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adaptyvbio/automancer/d577cc1775767b5d4839f908410ca5c4a0dadb9f/host/pr1/devices/nodes/__init__.py -------------------------------------------------------------------------------- /host/pr1/devices/nodes/collection.py: -------------------------------------------------------------------------------- 1 | from typing import ClassVar 2 | 3 | from .common import BaseNode, NodeId, NodePath 4 | 5 | 6 | class CollectionNode(BaseNode): 7 | def __init__(self): 8 | super().__init__() 9 | 10 | self.nodes: dict[NodeId, BaseNode] 11 | 12 | def __get_node_children__(self): 13 | return self.nodes.values() 14 | 15 | def export(self): 16 | return { 17 | **super().export(), 18 | "nodes": { node.id: node.export() for node in self.nodes.values() } 19 | } 20 | 21 | def iter_all(self): 22 | yield from super().iter_all() 23 | 24 | for child_node in self.nodes.values(): 25 | for node_path, node in child_node.iter_all(): 26 | yield NodePath([self.id, *node_path]), node 27 | 28 | 29 | class DeviceNode(CollectionNode): 30 | owner: ClassVar[str] 31 | 32 | def __init__(self): 33 | super().__init__() 34 | 35 | def export(self): 36 | return { 37 | **super().export(), 38 | "owner": self.owner 39 | } 40 | 41 | 42 | __all__ = [ 43 | 'CollectionNode', 44 | 'DeviceNode' 45 | ] 46 | -------------------------------------------------------------------------------- /host/pr1/devices/nodes/numeric.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | from typing import Optional 3 | 4 | from quantops import Context as QuantityContext 5 | from quantops import Quantity, Unit, UnitRegistry 6 | 7 | from ...ureg import ureg 8 | from .value import ValueNode 9 | 10 | 11 | class NumericNode(ValueNode[Quantity], ABC): 12 | _ureg: UnitRegistry = ureg 13 | 14 | def __init__( 15 | self, 16 | *, 17 | context: Optional[QuantityContext | str] = None, 18 | dtype: str = 'f4', 19 | range: Optional[tuple[Quantity, Quantity]] = None, 20 | resolution: Optional[Quantity] = None, 21 | **kwargs 22 | ): 23 | super().__init__(**kwargs) 24 | 25 | self.context = self._ureg.get_context(context or "dimensionless") 26 | 27 | assert (not range) or ((range[0].dimensionality == range[1].dimensionality == self.context.dimensionality) and (range[0] < range[1])) 28 | assert (resolution is None) or (resolution.dimensionality == self.context.dimensionality) 29 | 30 | self.dtype = dtype 31 | self.range = range 32 | self.resolution = resolution 33 | 34 | def _export_spec(self): 35 | return { 36 | "type": "numeric", 37 | "context": self.context.serialize_external(), 38 | "range": [self.range[0].magnitude, self.range[1].magnitude] if self.range else None, 39 | "resolution": self.resolution.magnitude if self.resolution else None, 40 | } 41 | 42 | def _export_value(self, value: Quantity, /): 43 | return { 44 | "magnitude": value.magnitude 45 | } 46 | 47 | 48 | __all__ = [ 49 | 'NumericNode' 50 | ] 51 | -------------------------------------------------------------------------------- /host/pr1/devices/nodes/primitive.py: -------------------------------------------------------------------------------- 1 | from dataclasses import KW_ONLY, dataclass 2 | import time 3 | from typing import Generic, Optional, TypeVar 4 | 5 | from .value import ValueNode 6 | 7 | 8 | class BooleanNode(ValueNode[bool]): 9 | def _export_spec(self): 10 | return { 11 | "type": "boolean" 12 | } 13 | 14 | def _export_value(self, value: bool, /): 15 | return value 16 | 17 | async def _read(self): 18 | old_value = self.value 19 | self.value = (time.time(), await self._read_value()) 20 | 21 | return (old_value is None) or (self.value[1] != old_value[1]) 22 | 23 | async def _read_value(self) -> bool: 24 | raise NotImplementedError 25 | 26 | 27 | T = TypeVar('T', int, str) 28 | 29 | @dataclass 30 | class EnumNodeCase(Generic[T]): 31 | id: T 32 | _: KW_ONLY 33 | label: Optional[str] = None 34 | 35 | class EnumNode(ValueNode[T], Generic[T]): 36 | def __init__(self, *, cases: list[EnumNodeCase], **kwargs): 37 | super().__init__(**kwargs) 38 | self.cases = cases 39 | 40 | def _export_spec(self): 41 | return { 42 | "type": "enum", 43 | "cases": [{ 44 | "id": case.id, 45 | "label": case.label 46 | } for case in self.cases] 47 | } 48 | 49 | def _export_value(self, value: T, /): 50 | return value 51 | 52 | 53 | __all__ = [ 54 | 'BooleanNode', 55 | 'EnumNode', 56 | 'EnumNodeCase' 57 | ] 58 | -------------------------------------------------------------------------------- /host/pr1/document.py: -------------------------------------------------------------------------------- 1 | import functools 2 | from dataclasses import dataclass 3 | from pathlib import PurePosixPath 4 | from typing import Any, NewType 5 | 6 | import comserde 7 | 8 | from .reader import Source 9 | 10 | DocumentId = NewType('DocumentId', str) 11 | 12 | @comserde.serializable 13 | @dataclass(frozen=True, kw_only=True) 14 | class Document: 15 | contents: str 16 | id: DocumentId 17 | path: PurePosixPath 18 | 19 | @functools.cached_property 20 | def source(self): 21 | source = Source(self.contents) 22 | source.origin = self.id 23 | 24 | return source 25 | 26 | def export(self): 27 | return { 28 | "id": self.id, 29 | "path": str(self.path), 30 | "source": self.source 31 | } 32 | 33 | @classmethod 34 | def load(cls, data: Any, /): 35 | return cls( 36 | contents=data["contents"], 37 | id=data["id"], 38 | path=PurePosixPath("/".join(data["path"])) 39 | ) 40 | 41 | @classmethod 42 | def text(cls, contents: str, id: str = 'default', path: PurePosixPath = PurePosixPath('/default')): 43 | return cls( 44 | contents=contents, 45 | id=DocumentId(id), 46 | path=path 47 | ) 48 | -------------------------------------------------------------------------------- /host/pr1/fiber/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adaptyvbio/automancer/d577cc1775767b5d4839f908410ca5c4a0dadb9f/host/pr1/fiber/__init__.py -------------------------------------------------------------------------------- /host/pr1/fiber/transparent.py: -------------------------------------------------------------------------------- 1 | from .master2 import ProgramHandle, ProgramOwner 2 | from .parser import BaseBlock, BaseProgram, BaseProgramLocation 3 | 4 | 5 | class TransparentProgramLocation(BaseProgramLocation): 6 | def export(self, context) -> dict: 7 | return {} 8 | 9 | 10 | class TransparentProgram(BaseProgram): 11 | def __init__(self, block: BaseBlock, handle: ProgramHandle): 12 | super().__init__(block, handle) 13 | 14 | self._child = block 15 | self._handle = handle 16 | 17 | self._owner: ProgramOwner 18 | 19 | def halt(self): 20 | self._owner.halt() 21 | 22 | async def run(self, point, stack): 23 | self._handle.send_location(TransparentProgramLocation()) 24 | self._owner = self._handle.create_child(self._child) 25 | 26 | await self._owner.run(point, stack) 27 | del self._owner 28 | -------------------------------------------------------------------------------- /host/pr1/history.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | from dataclasses import dataclass 3 | from comserde import serializable 4 | 5 | from .fiber.parser import BaseProgramLocation 6 | 7 | 8 | class BaseTreeChange(ABC): 9 | pass 10 | 11 | @serializable 12 | @dataclass(kw_only=True) 13 | class TreeAdditionChange(BaseTreeChange): 14 | block_child_id: int 15 | location: BaseProgramLocation 16 | parent_index: int 17 | 18 | @serializable 19 | @dataclass(kw_only=True) 20 | class TreeRemovalChange(BaseTreeChange): 21 | index: int 22 | 23 | @serializable 24 | @dataclass(kw_only=True) 25 | class TreeUpdateChange(BaseTreeChange): 26 | index: int 27 | location: BaseProgramLocation 28 | 29 | 30 | TreeChange = TreeAdditionChange | TreeRemovalChange | TreeUpdateChange 31 | -------------------------------------------------------------------------------- /host/pr1/input/__main__.py: -------------------------------------------------------------------------------- 1 | from pprint import pprint 2 | import pr1 as am 3 | from ..fiber.eval import EvalContext, EvalEnvs 4 | from ..document import Document 5 | from ..fiber.parser import AnalysisContext 6 | from ..reader import loads2 7 | 8 | analysis, data = loads2(Document.text(""" 9 | foo: qux 10 | bar: 11 | - ${{ x + 35 }} 12 | - 2 13 | - 28 14 | """).source) 15 | 16 | data_type = am.RecordType({ 17 | 'foo': am.StrType(), 18 | 'bar': am.ListType(am.IntType()) 19 | }) 20 | 21 | context = AnalysisContext( 22 | auto_expr=True, 23 | envs_list=[EvalEnvs()], 24 | eval_depth=1 25 | ) 26 | 27 | analysis, result = data_type.analyze(data, context) 28 | pprint(analysis.errors) 29 | 30 | # analysis, result = result(context.eval_context) 31 | 32 | print(result) 33 | # print(result.evaluate(context.eval_context)[1].dislocate()) 34 | 35 | print("---") 36 | 37 | analysis, result = result.evaluate(EvalContext(stack={})) 38 | pprint(analysis.errors) 39 | 40 | print(result) 41 | -------------------------------------------------------------------------------- /host/pr1/master/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adaptyvbio/automancer/d577cc1775767b5d4839f908410ca5c4a0dadb9f/host/pr1/master/__init__.py -------------------------------------------------------------------------------- /host/pr1/report.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Annotated, Optional 3 | import comserde 4 | 5 | from .history import TreeChange 6 | from .master.analysis import MasterAnalysis 7 | from .analysis import DiagnosticAnalysis 8 | from .draft import Draft 9 | from .fiber.parser import BaseBlock, GlobalContext 10 | 11 | 12 | @comserde.serializable 13 | @dataclass 14 | class ExperimentReportHeader: 15 | analysis: DiagnosticAnalysis 16 | draft: Draft 17 | name: str 18 | root: Annotated[BaseBlock, comserde.SerializationFormat('object')] 19 | start_time: float 20 | 21 | def export(self, context: GlobalContext): 22 | return { 23 | "draft": self.draft.export(), 24 | "initialAnalysis": self.analysis.export(), 25 | "name": self.name, 26 | "root": self.root.export(context), 27 | "startDate": (self.start_time * 1000) 28 | } 29 | 30 | 31 | @comserde.serializable 32 | @dataclass 33 | class ExperimentReportEvent: 34 | analysis: Optional[MasterAnalysis] 35 | changes: list[TreeChange] 36 | time: float 37 | -------------------------------------------------------------------------------- /host/pr1/staticanalysis/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adaptyvbio/automancer/d577cc1775767b5d4839f908410ca5c4a0dadb9f/host/pr1/staticanalysis/__init__.py -------------------------------------------------------------------------------- /host/pr1/staticanalysis/context.py: -------------------------------------------------------------------------------- 1 | import ast 2 | from dataclasses import dataclass 3 | 4 | from .types import Symbols 5 | from ..analysis import DiagnosticAnalysis 6 | from ..error import Diagnostic, DiagnosticDocumentReference 7 | from ..reader import LocatedString 8 | 9 | 10 | @dataclass(kw_only=True) 11 | class StaticAnalysisContext: 12 | input_value: LocatedString 13 | 14 | class StaticAnalysisDiagnostic(Diagnostic): 15 | def __init__(self, message: str, node: ast.expr | ast.stmt, context: StaticAnalysisContext, *, name: str = 'unknown'): 16 | super().__init__( 17 | message, 18 | name=('staticanalysis.' + name), 19 | references=[DiagnosticDocumentReference.from_area(context.input_value.compute_ast_node_area(node))] 20 | ) 21 | 22 | def analysis(self, *, warning: bool = False): 23 | return StaticAnalysisAnalysis(warnings=[self]) if warning else StaticAnalysisAnalysis(errors=[self]) 24 | 25 | 26 | @dataclass(kw_only=True) 27 | class StaticAnalysisAnalysis(DiagnosticAnalysis): 28 | pass 29 | -------------------------------------------------------------------------------- /host/pr1/staticanalysis/function.py: -------------------------------------------------------------------------------- 1 | import ast 2 | from typing import Optional 3 | 4 | from .context import StaticAnalysisAnalysis, StaticAnalysisContext 5 | from .special import NoneType 6 | from .type import evaluate_type_expr, instantiate_type 7 | from .types import FuncArgDef, FuncKwArgDef, FuncOverloadDef, TypeDefs, TypeVariables 8 | 9 | 10 | def parse_func(node: ast.FunctionDef, /, type_defs: TypeDefs, type_variables: TypeVariables, context: StaticAnalysisContext): 11 | analysis = StaticAnalysisAnalysis() 12 | 13 | def process_arg_type(annotation: Optional[ast.expr]): 14 | return annotation and instantiate_type(analysis.add(evaluate_type_expr(annotation, type_defs, type_variables, context))) 15 | 16 | args_pos = [FuncArgDef( 17 | name=arg.arg, 18 | type=process_arg_type(arg.annotation) 19 | ) for arg in node.args.posonlyargs] 20 | 21 | args_both = [FuncArgDef( 22 | name=arg.arg, 23 | type=process_arg_type(arg.annotation) 24 | ) for arg in node.args.args] 25 | 26 | args_kw = [FuncKwArgDef( 27 | has_default=(default is not None), 28 | name=arg.arg, 29 | type=process_arg_type(arg.annotation) 30 | ) for arg, default in zip(node.args.kwonlyargs, node.args.kw_defaults)] 31 | 32 | overload = FuncOverloadDef( 33 | args_posonly=args_pos, 34 | args_both=args_both, 35 | args_kwonly=args_kw, 36 | default_count=len(node.args.defaults), 37 | return_type=(instantiate_type(analysis.add(evaluate_type_expr(node.returns, type_defs, type_variables, context))) if node.returns else NoneType) 38 | ) 39 | 40 | return analysis, overload 41 | -------------------------------------------------------------------------------- /host/pr1/staticanalysis/special.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from .types import ClassDef, ExportedTypeDefs 4 | 5 | 6 | TypeVarClassDef = ClassDef('TypeVar') 7 | GenericClassDef = ClassDef('Generic') 8 | 9 | FunctionType = ClassDef('function') 10 | MethodType = ClassDef('method') 11 | NoneType = ClassDef('None') 12 | TypeType = ClassDef('type') 13 | 14 | 15 | CoreTypeDefs: ExportedTypeDefs = { 16 | 'Generic': GenericClassDef, 17 | 'None': NoneType, 18 | 'NoneType': NoneType, 19 | 'TypeVar': TypeVarClassDef, 20 | 'type': TypeType 21 | } 22 | -------------------------------------------------------------------------------- /host/pr1/units/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adaptyvbio/automancer/d577cc1775767b5d4839f908410ca5c4a0dadb9f/host/pr1/units/__init__.py -------------------------------------------------------------------------------- /host/pr1/ureg.py: -------------------------------------------------------------------------------- 1 | from quantops import UnitRegistry 2 | 3 | 4 | ureg = UnitRegistry.get_default() 5 | 6 | 7 | __all__ = [ 8 | 'ureg' 9 | ] 10 | -------------------------------------------------------------------------------- /host/pr1/util/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adaptyvbio/automancer/d577cc1775767b5d4839f908410ca5c4a0dadb9f/host/pr1/util/__init__.py -------------------------------------------------------------------------------- /host/pr1/util/blob.py: -------------------------------------------------------------------------------- 1 | import base64 2 | 3 | 4 | class Blob: 5 | def __init__(self, *, data, type): 6 | self.data = data 7 | self.type = type 8 | 9 | def to_url(self): 10 | data_str = base64.b64encode(self.data).decode("utf-8") 11 | return f"data:{self.type};base64,{data_str}" 12 | -------------------------------------------------------------------------------- /host/pr1/util/decorators.py: -------------------------------------------------------------------------------- 1 | from logging import Logger 2 | import warnings 3 | import functools 4 | 5 | 6 | def debug(cls): 7 | def repr_cls(self): 8 | props = ", ".join(f"{key}={repr(value)}" for key, value in self.__dict__.items()) 9 | return f"{type(self).__name__}({props})" 10 | 11 | setattr(cls, '__repr__', repr_cls) 12 | return cls 13 | 14 | 15 | def deprecated(func): 16 | @functools.wraps(func) 17 | 18 | def new_func(*args, **kwargs): 19 | warnings.simplefilter('always', DeprecationWarning) 20 | warnings.warn( 21 | message="Call to deprecated function {}.".format(func.__name__), 22 | category=DeprecationWarning, 23 | stacklevel=2 24 | ) 25 | 26 | warnings.simplefilter('default', DeprecationWarning) 27 | 28 | return func(*args, **kwargs) 29 | 30 | return new_func 31 | 32 | 33 | def provide_logger(parent_logger: Logger, *, name: str = '_logger'): 34 | def func(cls): 35 | index = 1 36 | old_init = cls.__init__ 37 | 38 | def init(self, *args, **kwargs): 39 | nonlocal index 40 | 41 | setattr(self, name, parent_logger.getChild(f"{cls.__name__}{index}")) 42 | old_init(self, *args, **kwargs) 43 | index += 1 44 | 45 | cls.__init__ = init 46 | return cls 47 | 48 | return func 49 | 50 | 51 | __all__ = [ 52 | 'debug', 53 | 'deprecated', 54 | 'provide_logger' 55 | ] 56 | -------------------------------------------------------------------------------- /host/pr1/util/encoder.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import pickle 3 | 4 | 5 | def encode(data): 6 | return base64.b85encode(pickle.dumps(data)).decode("utf-8") 7 | 8 | def decode(data): 9 | return pickle.loads(base64.b85decode(data.encode("utf-8"))) 10 | -------------------------------------------------------------------------------- /host/pr1/util/ref.py: -------------------------------------------------------------------------------- 1 | from asyncio import Future 2 | from typing import Generic, Optional, TypeVar, cast 3 | 4 | 5 | T = TypeVar('T') 6 | 7 | class Ref(Generic[T]): 8 | def __init__(self, value: Optional[T] = None): 9 | self.value = value 10 | 11 | @property 12 | def unwrapped(self) -> T: 13 | if self._value is None: 14 | raise ValueError() 15 | 16 | return self._value 17 | 18 | @property 19 | def value(self): 20 | return self._value 21 | 22 | @value.setter 23 | def value(self, value: Optional[T]): 24 | if (self._value is None) != (value is None): 25 | raise ValueError() 26 | 27 | self._value = value 28 | 29 | if self._value is None: 30 | self._future = Future() 31 | else: 32 | self._future.set_result(None) 33 | self._future = None 34 | 35 | async def get_value(self) -> T: 36 | if self._future is not None: 37 | await self._future 38 | 39 | return cast(T, self.value) 40 | -------------------------------------------------------------------------------- /host/pr1/util/types.py: -------------------------------------------------------------------------------- 1 | from typing import Awaitable, Callable 2 | 3 | 4 | SimpleCallbackFunction = Callable[[], None] 5 | SimpleAsyncCallbackFunction = Callable[[], Awaitable[None]] 6 | -------------------------------------------------------------------------------- /host/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=66", "setuptools_scm[toml]>=6.2"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "pr1" 7 | dynamic = ["version"] 8 | requires-python = ">3.11" 9 | 10 | dependencies=[ 11 | "comserde~=0.2.0", 12 | "numpy~=1.23", 13 | "quantops~=0.2.0", 14 | "regex==2023.*" 15 | ] 16 | 17 | [tool.setuptools.packages] 18 | find = {} 19 | 20 | [tool.setuptools_scm] 21 | root = ".." 22 | -------------------------------------------------------------------------------- /host/test.yml: -------------------------------------------------------------------------------- 1 | name: IL6 test 2 | models: 3 | - m1024 4 | parameters: 5 | Button: 6 | default: inlet/1 7 | display: visible 8 | repr: push 9 | Biotin: 10 | default: inlet/2 11 | name: Biotin BSA 12 | # in3: inlet/3 13 | Pbs: 14 | name: PBS 15 | display: hidden 16 | Nv: 17 | name: Neutravidin NV 18 | 19 | shorthands: 20 | p: 21 | # name: Foo {3 + 4} {args[0]} {args[1].evaluate().to_value() + 9} 22 | name: The name is {str(args[0]).upper()} and {expr(args[1]) * 2} 23 | duration: 10 sec 24 | x: 25 | use: pp(bar, 16 + expr(args[0]) + delta, [34 + 52]) 26 | 27 | cycle: 28 | if: expr(args[0]) > 0 29 | actions: 30 | - duration: 10 sec 31 | - use: cycle(expr(args[0]) - 1) 32 | 33 | 34 | stages: 35 | - name: Stage A 36 | steps: 37 | - use: cycle(3) 38 | - use: p(foo, 3) 39 | -------------------------------------------------------------------------------- /pyrightconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": [ 3 | "**/node_modules", 4 | "**/__pycache__", 5 | ".git", 6 | 7 | "**/build", 8 | "**/tmp", 9 | "**/*copy*" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | cd "$(dirname "$0")" 5 | cd .. 6 | 7 | python3 -m build --outdir scripts/tmp/packages host 8 | python3 -m build --outdir scripts/tmp/packages app/server 9 | python3 -m build --outdir scripts/tmp/packages units/amf 10 | python3 -m build --outdir scripts/tmp/packages units/core 11 | python3 -m build --outdir scripts/tmp/packages units/adaptyv_nikon 12 | python3 -m build --outdir scripts/tmp/packages units/okolab 13 | python3 -m build --outdir scripts/tmp/packages units/s3 14 | 15 | pip3 install twine 16 | python3 -m twine upload --repository-url https://gitlab.com/api/v4/projects/45232449/packages/pypi --skip-existing --verbose scripts/tmp/packages/* 17 | 18 | cd - 19 | -------------------------------------------------------------------------------- /shared/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2022", 4 | "esModuleInterop": true, 5 | 6 | "forceConsistentCasingInFileNames": true, 7 | "declaration": true, 8 | "declarationMap": true, 9 | 10 | "exactOptionalPropertyTypes": true, 11 | "noImplicitOverride": true, 12 | "noImplicitReturns": true, 13 | "noPropertyAccessFromIndexSignature": true, 14 | "skipLibCheck": true, 15 | "strict": true 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /units/adaptyv_nikon/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | node_modules 4 | package-lock.json 5 | *.egg-info 6 | __pycache__ 7 | 8 | src/*/client/** 9 | !src/*/client/*.py 10 | -------------------------------------------------------------------------------- /units/adaptyv_nikon/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "build": "npm run build:js && (npm test || true)", 4 | "build:js": "esbuild src/index.tsx --bundle --external:*.css --external:pr1 --external:react --format=esm --minify --outdir=../src/pr1_adaptyv_nikon/client", 5 | "test": "tsc" 6 | }, 7 | "devDependencies": { 8 | "@types/react": "^18.0.16", 9 | "esbuild": "~0.15.16", 10 | "pr1": "file:../../../client", 11 | "pr1-shared": "file:../../../app/shared", 12 | "typescript": "~4.9.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /units/adaptyv_nikon/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "files": [ 4 | "src/index.tsx" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /units/adaptyv_nikon/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=66", "setuptools_scm[toml]>=6.2"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "pr1-adaptyv-nikon" 7 | dynamic = ["version"] 8 | requires-python = ">3.11" 9 | 10 | dependencies=[ 11 | "pywin32>=306" 12 | ] 13 | 14 | [project.entry-points."automancer.plugins"] 15 | adaptyv_nikon = "pr1_adaptyv_nikon" 16 | 17 | [tool.setuptools.packages.find] 18 | where = ["src"] 19 | 20 | [tool.setuptools.package-data] 21 | "pr1_adaptyv_nikon.client" = ["*"] 22 | 23 | [tool.setuptools_scm] 24 | root = "../.." 25 | -------------------------------------------------------------------------------- /units/adaptyv_nikon/readme.md: -------------------------------------------------------------------------------- 1 | # Adaptyv-Nikon 2 | 3 | This unit provides imaging functionality using NIS Elements macros. The microscope must be connected when the host starts, and must stay connected until the end of the experiment. Furthermore, NIS Elements (version 5) must be running during that time. 4 | 5 | 6 | ## Setup configuration 7 | 8 | ```yml 9 | # Location of NIS Elements 10 | # Defaults to 'C:\Program Files\NIS-Elements\nis_ar.exe' which should work for most setups. 11 | nis_path: C:\Program Files\custom\location\for\nis.exe 12 | 13 | # Bounds for X, Y and Z movements, in micrometers 14 | # Defaults to no bounds 15 | stage_bounds: 16 | x: -57000, 57000 17 | y: -37500, 37500 18 | z: 0, 6000 19 | ``` 20 | 21 | 22 | ## Segment configuration 23 | 24 | ```yml 25 | capture: 26 | exposure: 300 # in milliseconds 27 | objective: 28 | optconf: 29 | 30 | # '{}' will be replaced with the chip number, starting at 0 31 | save: pictures/picture_{}.nd2 32 | ``` 33 | -------------------------------------------------------------------------------- /units/adaptyv_nikon/src/pr1_adaptyv_nikon/__init__.py: -------------------------------------------------------------------------------- 1 | from importlib.resources import files 2 | 3 | import automancer as am 4 | 5 | 6 | namespace = am.PluginName("adaptyv_nikon") 7 | version = 0 8 | 9 | metadata = am.Metadata( 10 | description="This unit provides imaging functionality using NIS Elements macros.", 11 | icon=am.MetadataIcon(kind='icon', value="biotech"), 12 | title="Adaptyv Nikon", 13 | version="2.0" 14 | ) 15 | 16 | client_path = files(__name__ + '.client') 17 | logger = am.plugin_logger.getChild(namespace) 18 | 19 | from .executor import Executor 20 | from .parser import Parser 21 | from .runner import Runner 22 | -------------------------------------------------------------------------------- /units/adaptyv_nikon/src/pr1_adaptyv_nikon/client/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adaptyvbio/automancer/d577cc1775767b5d4839f908410ca5c4a0dadb9f/units/adaptyv_nikon/src/pr1_adaptyv_nikon/client/__init__.py -------------------------------------------------------------------------------- /units/adaptyv_nikon/src/pr1_adaptyv_nikon/macros/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adaptyvbio/automancer/d577cc1775767b5d4839f908410ca5c4a0dadb9f/units/adaptyv_nikon/src/pr1_adaptyv_nikon/macros/__init__.py -------------------------------------------------------------------------------- /units/adaptyv_nikon/src/pr1_adaptyv_nikon/macros/inspect.mac: -------------------------------------------------------------------------------- 1 | int main() { 2 | char buf[256]; 3 | char output[65536]; 4 | int i; 5 | 6 | // List objectives 7 | for (i = 0; i < Stg_GetNosepiecePositions(); i = i + 1) { 8 | Stg_GetNosepieceObjectiveName(i, &buf, 256); 9 | strcat(&output, &buf); 10 | strcat(&output, ";"); 11 | } 12 | 13 | strcat(&output, "//"); 14 | 15 | // List optical configurations 16 | for (i = 0; i < GetOptConfCount(); i = i + 1) { 17 | GetOptConfName(i, &buf, 256); 18 | strcat(&output, &buf); 19 | strcat(&output, ";"); 20 | } 21 | 22 | WriteFile("%(output_path)s", &output, strlen(&output) * 2); 23 | } 24 | -------------------------------------------------------------------------------- /units/adaptyv_nikon/src/pr1_adaptyv_nikon/macros/query.mac: -------------------------------------------------------------------------------- 1 | int main() { 2 | char buf[256]; 3 | char output[65536]; 4 | char outputFilePath[256]; 5 | int chipCount; 6 | int chipIndex, chipPointIndex, pointIndex; 7 | int i, j; 8 | double ax, ay, az; 9 | double dx[%(chip_point_count)i], dy[%(chip_point_count)i], dz[%(chip_point_count)i]; 10 | 11 | chipCount = %(chip_count)i; 12 | outputFilePath = "%(output_path)s"; 13 | 14 | for (chipIndex = 0; chipIndex < chipCount; chipIndex = chipIndex + 1) { 15 | for (pointIndex = 0; pointIndex < 4; pointIndex = pointIndex + 1) { 16 | i = pointIndex + 1; 17 | j = chipIndex + 1; 18 | 19 | sprintf(&buf, "Go to point %%d of chip %%d", "i, j"); 20 | WaitText(0, &buf); 21 | 22 | StgGetPosXY(&ax, &ay); 23 | StgGetPosZ(&az, 0); 24 | 25 | chipPointIndex = chipIndex * 4 + pointIndex; 26 | dx[chipPointIndex] = ax; 27 | dy[chipPointIndex] = ay; 28 | dz[chipPointIndex] = az; 29 | 30 | sprintf(&buf, "%%f;%%f;%%f;", "ax, ay, az"); 31 | strcat(&output, &buf); 32 | } 33 | } 34 | 35 | WriteFile(&outputFilePath, &output, strlen(&output) * 2); 36 | } 37 | -------------------------------------------------------------------------------- /units/adaptyv_nikon/src/pr1_adaptyv_nikon/parser.py: -------------------------------------------------------------------------------- 1 | import automancer as am 2 | 3 | from . import namespace 4 | from .executor import Executor 5 | from .process import Process 6 | 7 | 8 | class Parser(am.BaseParser): 9 | namespace = namespace 10 | 11 | def __init__(self, fiber): 12 | super().__init__(fiber) 13 | 14 | executor: Executor = fiber.host.executors[namespace] 15 | objectives = executor._objectives 16 | optconfs = executor._optconfs 17 | 18 | assert objectives is not None 19 | assert optconfs is not None 20 | 21 | self.transformers = [am.ProcessTransformer(Process, { 22 | 'capture': am.Attribute( 23 | description="Capture images on the Nikon Ti-2E microscope", 24 | type=am.RecordType({ 25 | 'exposure': am.QuantityType('millisecond'), 26 | 'objective': am.EnumType(*objectives), 27 | 'optconf': am.EnumType(*optconfs), 28 | 'save': am.PathType(), 29 | 'z_offset': am.Attribute( 30 | am.QuantityType('micrometer'), 31 | default=(0.0 * am.ureg.µm), 32 | description="An offset on the Z axis compared to registered grid" 33 | ) 34 | }) 35 | ) 36 | }, parser=fiber)] 37 | -------------------------------------------------------------------------------- /units/adaptyv_nikon/src/pr1_adaptyv_nikon/runner.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | import automancer as am 4 | import numpy as np 5 | 6 | from . import namespace 7 | from .executor import Executor 8 | 9 | 10 | class Runner(am.BaseRunner): 11 | def __init__(self, master): 12 | self._executor: Executor = master.host.executors[namespace] 13 | self._master = master 14 | 15 | self._chip_count = 1 16 | self._points: Optional[np.ndarray] = None 17 | self._points_path = master.experiment.path / namespace / "points.json" 18 | 19 | async def request(self, data, /, agent): 20 | match data["type"]: 21 | case "queryPoints": 22 | self._points = await self._executor.query(chip_count=self._chip_count) 23 | 24 | self._points_path.parent.mkdir(exist_ok=True, parents=True) 25 | np.save(self._points_path.open("wb"), self._points) 26 | 27 | case "setChipCount": 28 | self._chip_count = data["value"] 29 | 30 | if (self._points is not None) and (self._points.shape[2] != self._chip_count): 31 | self._points = None 32 | self._points_path.unlink() 33 | 34 | def export(self): 35 | return { 36 | "chipCount": self._chip_count, 37 | "pointsSaved": (self._points is not None) 38 | } 39 | -------------------------------------------------------------------------------- /units/amf/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | node_modules 4 | package-lock.json 5 | *.egg-info 6 | __pycache__ 7 | 8 | src/*/client/** 9 | !src/*/client/*.py 10 | -------------------------------------------------------------------------------- /units/amf/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "build": "npm run build:css && npm run build:js && (npm test || true)", 4 | "build:css": "sass styles:../src/pr1_amf/client --no-source-map", 5 | "build:js": "esbuild src/index.ts --bundle --external:*.css --external:pr1 --external:react --format=esm --minify --outdir=../src/pr1_amf/client", 6 | "test": "tsc" 7 | }, 8 | "devDependencies": { 9 | "@types/react": "^18.0.16", 10 | "esbuild": "~0.15.16", 11 | "pr1": "file:../../../client", 12 | "sass": "^1.54.4", 13 | "typescript": "~4.9.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /units/amf/client/src/types.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.css'; 2 | -------------------------------------------------------------------------------- /units/amf/client/styles/index.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adaptyvbio/automancer/d577cc1775767b5d4839f908410ca5c4a0dadb9f/units/amf/client/styles/index.scss -------------------------------------------------------------------------------- /units/amf/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "files": [ 4 | "src/index.ts" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /units/amf/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=66", "setuptools_scm[toml]>=6.2"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "pr1-amf" 7 | dynamic = ["version"] 8 | requires-python = ">3.11" 9 | 10 | dependencies=[ 11 | "amf-rotary-valve~=0.2.0" 12 | ] 13 | 14 | [project.entry-points."pr1.units"] 15 | amf = "pr1_amf" 16 | 17 | [tool.setuptools.packages.find] 18 | where = ["src"] 19 | 20 | [tool.setuptools.package-data] 21 | "pr1_amf.client" = ["*"] 22 | 23 | [tool.setuptools_scm] 24 | root = "../.." 25 | -------------------------------------------------------------------------------- /units/amf/src/pr1_amf/__init__.py: -------------------------------------------------------------------------------- 1 | from importlib.resources import files 2 | from pathlib import Path 3 | 4 | from pr1.units.base import Metadata, MetadataIcon, logger as parent_logger 5 | from pr1.util.blob import Blob 6 | 7 | 8 | namespace = "amf" 9 | version = 0 10 | 11 | logo = Blob( 12 | data=(Path(__file__).parent / "data/logo.png").open("rb").read(), 13 | type="image/png" 14 | ) 15 | 16 | metadata = Metadata( 17 | description="Advanced microfluidics", 18 | icon=MetadataIcon(kind='bitmap', value=logo.to_url()), 19 | title="AMF", 20 | version="6.0" 21 | ) 22 | 23 | client_path = files(__name__ + '.client') 24 | logger = parent_logger.getChild(namespace) 25 | 26 | from .executor import Executor 27 | -------------------------------------------------------------------------------- /units/amf/src/pr1_amf/client/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adaptyvbio/automancer/d577cc1775767b5d4839f908410ca5c4a0dadb9f/units/amf/src/pr1_amf/client/__init__.py -------------------------------------------------------------------------------- /units/amf/src/pr1_amf/data/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adaptyvbio/automancer/d577cc1775767b5d4839f908410ca5c4a0dadb9f/units/amf/src/pr1_amf/data/__init__.py -------------------------------------------------------------------------------- /units/amf/src/pr1_amf/data/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adaptyvbio/automancer/d577cc1775767b5d4839f908410ca5c4a0dadb9f/units/amf/src/pr1_amf/data/logo.png -------------------------------------------------------------------------------- /units/core/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | node_modules 4 | package-lock.json 5 | *.egg-info 6 | __pycache__ 7 | -------------------------------------------------------------------------------- /units/core/client/build.mjs: -------------------------------------------------------------------------------- 1 | import esbuild from 'esbuild'; 2 | import { postcssModules, sassPlugin } from 'esbuild-sass-plugin'; 3 | import fs from 'node:fs/promises'; 4 | import path from 'node:path'; 5 | import { fileURLToPath } from 'node:url'; 6 | 7 | import externalLibraries from 'pr1/scripts/external.js'; 8 | 9 | 10 | let workingDirPath = path.dirname(fileURLToPath(import.meta.url)); 11 | let sourceDirPath = path.join(workingDirPath, 'src'); 12 | 13 | let watch = process.argv.slice(2).includes('--watch'); 14 | 15 | let context = await esbuild.context({ 16 | entryPoints: (await fs.readdir(sourceDirPath)) 17 | .map((relativeDirPath) => path.join(sourceDirPath, relativeDirPath, 'index.tsx')), 18 | absWorkingDir: workingDirPath, 19 | bundle: true, 20 | external: Object.keys(externalLibraries), 21 | format: 'esm', 22 | minify: !watch, 23 | outdir: 'dist', 24 | sourcemap: watch, 25 | define: { this: 'window' }, 26 | plugins: [ 27 | sassPlugin({ 28 | filter: /\.module\.scss$/, 29 | transform: postcssModules({}), 30 | type: 'style' 31 | }) 32 | ] 33 | }); 34 | 35 | if (watch) { 36 | await context.watch(); 37 | } else { 38 | await context.rebuild(); 39 | await context.dispose(); 40 | } 41 | -------------------------------------------------------------------------------- /units/core/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "build": "npm run build:js && npm run build:python", 4 | "build:js": "node build.mjs", 5 | "build:python": "python3 -c \"for d in __import__('pathlib').Path('dist').glob('*'): (d / '__init__.py').open('w')\"", 6 | "deref": "python3 -c \"for f in __import__('pathlib').Path('dist').glob('*'): d = f'../src/pr1_{f.name}/client'; __import__('os').unlink(d); __import__('shutil').copytree(f, d);\"", 7 | "ref": "python3 -c \"import os\nfor f in __import__('pathlib').Path('dist').glob('*'): d = f'../src/pr1_{f.name}/client'; __import__('shutil').rmtree(d); os.symlink(os.path.relpath(f, d + '/..'), d);\"", 8 | "test": "tsc" 9 | }, 10 | "dependencies": { 11 | "d3": "^7.8.4", 12 | "d3fc": "^15.2.6" 13 | }, 14 | "devDependencies": { 15 | "@types/d3": "^7.4.0", 16 | "@types/react": "^18.0.16", 17 | "esbuild": "~0.17.0", 18 | "esbuild-sass-plugin": "^2.9.0", 19 | "immutable": "^4.3.0", 20 | "postcss": "^8.4.23", 21 | "postcss-modules": "^6.0.0", 22 | "pr1": "file:../../../client", 23 | "pr1-shared": "file:../../../app/shared", 24 | "quantops":"~0.1.0", 25 | "typescript": "~4.9.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /units/core/client/src/devices/blocks/applier.ts: -------------------------------------------------------------------------------- 1 | import { PluginBlockImpl } from 'pr1'; 2 | import { MasterBlockLocation, ProtocolBlock, createZeroTerm } from 'pr1-shared'; 3 | 4 | 5 | export interface ApplierBlock extends ProtocolBlock { 6 | child: ProtocolBlock; 7 | } 8 | 9 | export interface ApplierLocation extends MasterBlockLocation { 10 | children: { 0: MasterBlockLocation }; 11 | mode: 'applying' | 'halting' | 'normal'; 12 | } 13 | 14 | 15 | export default { 16 | getChildren(block, context) { 17 | return [{ 18 | block: block.child, 19 | delay: createZeroTerm() 20 | }]; 21 | }, 22 | getChildrenExecution(block, location, context) { 23 | return (location.mode === 'normal') 24 | ? [{ location: location.children[0] }] 25 | : null; 26 | } 27 | } satisfies PluginBlockImpl 28 | -------------------------------------------------------------------------------- /units/core/client/src/devices/index.tsx: -------------------------------------------------------------------------------- 1 | import { Map as ImMap, List } from 'immutable'; 2 | import { Plugin } from 'pr1'; 3 | import { ProtocolBlockName } from 'pr1-shared'; 4 | 5 | import applierBlock from './blocks/applier'; 6 | import publisherBlock from './blocks/publisher'; 7 | import { PersistentStoreEntries, SessionStoreEntries, namespace } from './types'; 8 | import { DeviceControlView } from './view'; 9 | 10 | 11 | export default { 12 | namespace, 13 | blocks: { 14 | ['applier' as ProtocolBlockName]: applierBlock, 15 | ['publisher' as ProtocolBlockName]: publisherBlock 16 | }, 17 | 18 | views: [{ 19 | id: 'device-control', 20 | icon: 'tune', 21 | label: 'Device control', 22 | Component: DeviceControlView 23 | }], 24 | 25 | persistentStoreDefaults: { 26 | nodePrefs: ImMap(), 27 | userNodes: List() 28 | }, 29 | sessionStoreDefaults: { 30 | selectedNodePath: null 31 | } 32 | } satisfies Plugin 33 | -------------------------------------------------------------------------------- /units/core/client/src/devices/util.ts: -------------------------------------------------------------------------------- 1 | import { List } from 'immutable'; 2 | 3 | import { BaseNode, CollectionNode, NodePath, NodePathLike, ValueNode } from './types'; 4 | 5 | 6 | export function findNode(node: BaseNode, path: NodePathLike) { 7 | let currentNode = node; 8 | 9 | for (let id of path) { 10 | if (!isCollectionNode(currentNode)) { 11 | return null; 12 | } 13 | 14 | currentNode = currentNode.nodes[id]; 15 | 16 | if (!currentNode) { 17 | return null; 18 | } 19 | } 20 | 21 | return currentNode; 22 | } 23 | 24 | export function* iterNodes(node: BaseNode, nodePath: NodePath = List()): Iterable<[NodePath, BaseNode]> { 25 | if (!nodePath.isEmpty()) { 26 | yield [nodePath, node]; 27 | } 28 | 29 | if (isCollectionNode(node)) { 30 | for (let childNode of Object.values(node.nodes)) { 31 | yield* iterNodes(childNode, nodePath.push(childNode.id)); 32 | } 33 | } 34 | } 35 | 36 | export function isCollectionNode(node: BaseNode): node is CollectionNode { 37 | return 'nodes' in node; 38 | } 39 | 40 | export function isValueNode(node: BaseNode): node is ValueNode { 41 | return 'spec' in node; 42 | } 43 | -------------------------------------------------------------------------------- /units/core/client/src/name/index.tsx: -------------------------------------------------------------------------------- 1 | import { Plugin, PluginBlockImpl } from 'pr1'; 2 | import { MasterBlockLocation, PluginName, ProtocolBlock, ProtocolBlockName, createZeroTerm } from 'pr1-shared'; 3 | 4 | 5 | export interface Block extends ProtocolBlock { 6 | child: ProtocolBlock; 7 | value: string; 8 | } 9 | 10 | export interface Location extends MasterBlockLocation { 11 | 12 | } 13 | 14 | 15 | export default { 16 | namespace: ('name' as PluginName), 17 | blocks: { 18 | ['_' as ProtocolBlockName]: { 19 | getChildren(block, context) { 20 | return [{ 21 | block: block.child, 22 | delay: createZeroTerm() 23 | }]; 24 | }, 25 | getChildrenExecution(block, location, context) { 26 | return [{ 27 | location: location.children[0] 28 | }]; 29 | } 30 | } satisfies PluginBlockImpl 31 | } 32 | } satisfies Plugin 33 | -------------------------------------------------------------------------------- /units/core/client/src/shorthands/index.tsx: -------------------------------------------------------------------------------- 1 | import { BlockUnit, ProtocolBlock, ProtocolBlockGraphRendererMetrics } from 'pr1'; 2 | import { UnitNamespace } from 'pr1-shared'; 3 | 4 | 5 | export interface Block extends ProtocolBlock { 6 | namespace: typeof namespace; 7 | child: ProtocolBlock; 8 | } 9 | 10 | export type BlockMetrics = ProtocolBlockGraphRendererMetrics; 11 | 12 | export interface Location { 13 | children: { 0: unknown; }; 14 | } 15 | 16 | 17 | 18 | const namespace = ('shorthands' as UnitNamespace); 19 | 20 | export const unit: BlockUnit = { 21 | namespace, 22 | graphRenderer: { 23 | computeMetrics(block, ancestors, location, options, context) { 24 | return options.computeMetrics(block.child, [...ancestors, block], (location?.children[0] ?? null)); 25 | }, 26 | render(block, path, metrics, position, location, options, context) { 27 | return options.render(block.child, [...path, 0], metrics, position, (location?.children[0] ?? null), options); 28 | } 29 | }, 30 | getActiveChildLocation(location, id) { 31 | return location.children[0]; 32 | }, 33 | getChildBlock(block, key) { 34 | return block.child; 35 | }, 36 | getChildrenExecutionRefs(block, location) { 37 | return location.children[0] 38 | ? [{ 39 | blockKey: (null as never), 40 | executionId: 0 41 | }] 42 | : []; 43 | } 44 | } 45 | 46 | export default unit; 47 | -------------------------------------------------------------------------------- /units/core/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": [ 4 | "src/*/index.tsx" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /units/core/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=66", "setuptools_scm[toml]>=6.2"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "pr1-core" 7 | dynamic = ["version"] 8 | requires-python = ">3.11" 9 | 10 | [project.entry-points."pr1.units"] 11 | devices = "pr1_devices" 12 | do = "pr1_do" 13 | name = "pr1_name" 14 | repeat = "pr1_repeat" 15 | sequence = "pr1_sequence" 16 | shorthands = "pr1_shorthands" 17 | timer = "pr1_timer" 18 | 19 | [tool.setuptools.packages.find] 20 | where = ["src"] 21 | 22 | [tool.setuptools.package-data] 23 | "pr1_devices.client" = ["*"] 24 | "pr1_name.client" = ["*"] 25 | "pr1_repeat.client" = ["*"] 26 | "pr1_sequence.client" = ["*"] 27 | "pr1_shorthands.client" = ["*"] 28 | "pr1_timer.client" = ["*"] 29 | 30 | [tool.setuptools_scm] 31 | root = "../.." 32 | -------------------------------------------------------------------------------- /units/core/src/pr1_devices/__init__.py: -------------------------------------------------------------------------------- 1 | from importlib.resources import files 2 | from pathlib import Path 3 | 4 | import automancer as am 5 | 6 | 7 | namespace = am.PluginName("devices") 8 | version = 0 9 | 10 | metadata = am.Metadata( 11 | description="Devices", 12 | icon=am.MetadataIcon(kind='icon', value="settings_input_hdmi"), 13 | title="Devices", 14 | version="3.0" 15 | ) 16 | 17 | client_path = files(__name__ + '.client') 18 | logger = am.logger.getChild(namespace) 19 | 20 | from .executor import Executor 21 | from .parser import Parser 22 | from .runner import Runner 23 | -------------------------------------------------------------------------------- /units/core/src/pr1_devices/client: -------------------------------------------------------------------------------- 1 | ../../client/dist/devices -------------------------------------------------------------------------------- /units/core/src/pr1_devices/mock.py: -------------------------------------------------------------------------------- 1 | from pr1.devices.nodes.collection import DeviceNode 2 | from pr1.devices.nodes.common import NodeId 3 | 4 | 5 | class MockDevice(DeviceNode): 6 | description = None 7 | id = NodeId("Mock") 8 | label = "Mock device" 9 | model = "Mock device" 10 | owner = "devices" 11 | 12 | def __init__(self): 13 | super().__init__() 14 | self.connected = True 15 | 16 | self.nodes = { 17 | # 'valueBool': MockBoolNode() 18 | } 19 | 20 | # class MockBoolNode(BooleanWritableNode): 21 | # id = "valueBool" 22 | # description = None 23 | # label = "Bool value" 24 | 25 | # def __init__(self): 26 | # super().__init__() 27 | # self.connected = True 28 | 29 | # self.current_value = False 30 | # self.target_value = False 31 | 32 | # async def write(self, value: bool): 33 | # print("MockBoolNode.write", value) 34 | -------------------------------------------------------------------------------- /units/core/src/pr1_do/__init__.py: -------------------------------------------------------------------------------- 1 | from importlib.resources import files 2 | from pathlib import Path 3 | 4 | from pr1.units.base import Metadata, MetadataIcon, logger as parent_logger 5 | 6 | namespace = "do" 7 | version = 0 8 | 9 | metadata = Metadata( 10 | description="Do", 11 | icon=MetadataIcon(kind='icon', value="description"), 12 | title="Do", 13 | version="2.0" 14 | ) 15 | 16 | logger = parent_logger.getChild(namespace) 17 | 18 | from .parser import Parser 19 | -------------------------------------------------------------------------------- /units/core/src/pr1_name/__init__.py: -------------------------------------------------------------------------------- 1 | from importlib.resources import files 2 | from pathlib import Path 3 | 4 | from pr1.units.base import Metadata, MetadataIcon, logger as parent_logger 5 | 6 | namespace = "name" 7 | version = 0 8 | 9 | metadata = Metadata( 10 | description="Name", 11 | icon=MetadataIcon(kind='icon', value="description"), 12 | title="Name", 13 | version="1.0" 14 | ) 15 | 16 | client_path = Path(files(__name__ + '.client')) 17 | logger = parent_logger.getChild(namespace) 18 | 19 | from .parser import Parser 20 | -------------------------------------------------------------------------------- /units/core/src/pr1_name/client: -------------------------------------------------------------------------------- 1 | ../../client/dist/name -------------------------------------------------------------------------------- /units/core/src/pr1_repeat/__init__.py: -------------------------------------------------------------------------------- 1 | from importlib.resources import files 2 | from pathlib import Path 3 | 4 | from pr1.units.base import Metadata, MetadataIcon, logger as parent_logger 5 | 6 | namespace = "repeat" 7 | version = 0 8 | 9 | metadata = Metadata( 10 | description="Repeat", 11 | icon=MetadataIcon(kind='icon', value="replay"), 12 | title="Repeat", 13 | version="2.0" 14 | ) 15 | 16 | client_path = Path(files(__name__ + '.client')) 17 | logger = parent_logger.getChild(namespace) 18 | 19 | from .parser import Parser 20 | # from .runner import Runner 21 | -------------------------------------------------------------------------------- /units/core/src/pr1_repeat/client: -------------------------------------------------------------------------------- 1 | ../../client/dist/repeat -------------------------------------------------------------------------------- /units/core/src/pr1_segment/__init__.py: -------------------------------------------------------------------------------- 1 | from importlib.resources import files 2 | from pathlib import Path 3 | 4 | from pr1.units.base import Metadata, MetadataIcon, logger as parent_logger 5 | 6 | namespace = "segment" 7 | version = 0 8 | 9 | metadata = Metadata( 10 | description="Segment", 11 | icon=MetadataIcon(kind='icon', value="description"), 12 | title="Segment", 13 | version="1.0" 14 | ) 15 | 16 | client_path = Path(files(__name__ + '.client')) 17 | logger = parent_logger.getChild(namespace) 18 | 19 | # from .parser import Parser 20 | # from .runner import Runner 21 | -------------------------------------------------------------------------------- /units/core/src/pr1_segment/client: -------------------------------------------------------------------------------- 1 | ../../client/dist/segment -------------------------------------------------------------------------------- /units/core/src/pr1_sequence/__init__.py: -------------------------------------------------------------------------------- 1 | from importlib.resources import files 2 | from pathlib import Path 3 | 4 | from pr1.units.base import Metadata, MetadataIcon, logger as parent_logger 5 | 6 | namespace = "sequence" 7 | version = 0 8 | 9 | metadata = Metadata( 10 | description="Sequence", 11 | icon=MetadataIcon(kind='icon', value="description"), 12 | title="Sequence", 13 | version="1.0" 14 | ) 15 | 16 | client_path = Path(files(__name__ + '.client')) 17 | logger = parent_logger.getChild(namespace) 18 | 19 | from .parser import Parser 20 | # from .runner import Runner 21 | -------------------------------------------------------------------------------- /units/core/src/pr1_sequence/client: -------------------------------------------------------------------------------- 1 | ../../client/dist/sequence -------------------------------------------------------------------------------- /units/core/src/pr1_shorthands/__init__.py: -------------------------------------------------------------------------------- 1 | from importlib.resources import files 2 | from pathlib import Path 3 | 4 | from pr1.units.base import Metadata, MetadataIcon, logger as parent_logger 5 | 6 | namespace = "shorthands" 7 | version = 0 8 | 9 | metadata = Metadata( 10 | description="Shorthands", 11 | icon=MetadataIcon(kind='icon', value="description"), 12 | title="Shorthands", 13 | version="2.0" 14 | ) 15 | 16 | client_path = Path(files(__name__ + '.client')) 17 | logger = parent_logger.getChild(namespace) 18 | 19 | from .parser import Parser 20 | -------------------------------------------------------------------------------- /units/core/src/pr1_shorthands/client: -------------------------------------------------------------------------------- 1 | ../../client/dist/shorthands -------------------------------------------------------------------------------- /units/core/src/pr1_state/__init__.py: -------------------------------------------------------------------------------- 1 | from importlib.resources import files 2 | from pathlib import Path 3 | 4 | from pr1.units.base import Metadata, MetadataIcon, logger as parent_logger 5 | 6 | namespace = "state" 7 | version = 0 8 | 9 | metadata = Metadata( 10 | description="State", 11 | icon=MetadataIcon(kind='icon', value="description"), 12 | title="State", 13 | version="1.0" 14 | ) 15 | 16 | client_path = Path(files(__name__ + '.client')) 17 | logger = parent_logger.getChild(namespace) 18 | 19 | from .parser import Parser 20 | -------------------------------------------------------------------------------- /units/core/src/pr1_state/client: -------------------------------------------------------------------------------- 1 | ../../client/dist/state -------------------------------------------------------------------------------- /units/core/src/pr1_timer/__init__.py: -------------------------------------------------------------------------------- 1 | from importlib.resources import files 2 | from pathlib import Path 3 | 4 | from pr1.units.base import Metadata, MetadataIcon, logger as parent_logger 5 | 6 | namespace = "timer" 7 | version = 0 8 | 9 | metadata = Metadata( 10 | description="Timer", 11 | icon=MetadataIcon(kind='icon', value="schedule"), 12 | title="Timer", 13 | version="2.0" 14 | ) 15 | 16 | client_path = Path(files(__name__ + '.client')) 17 | logger = parent_logger.getChild(namespace) 18 | 19 | from .parser import Parser 20 | -------------------------------------------------------------------------------- /units/core/src/pr1_timer/client: -------------------------------------------------------------------------------- 1 | ../../client/dist/timer -------------------------------------------------------------------------------- /units/core/src/pr1_timer/parser.py: -------------------------------------------------------------------------------- 1 | import pr1 as am 2 | 3 | from . import namespace 4 | from .process import process 5 | 6 | 7 | class Parser(am.BaseParser): 8 | namespace = namespace 9 | 10 | def __init__(self, fiber): 11 | super().__init__(fiber) 12 | 13 | self.transformers = [am.ProcessTransformer(process, { 14 | 'wait': am.Attribute( 15 | description="Wait for a fixed delay", 16 | documentation=["Accepts either a quantity or the `forever` keyword.", "Examples:\n```prl\nwait: forever\nwait: 10 min\n```\n"], 17 | type=am.UnionType( 18 | am.EnumType('forever'), 19 | am.QuantityType('second', min=(1 * am.ureg.ms)) 20 | ) 21 | ) 22 | }, parser=fiber)] 23 | -------------------------------------------------------------------------------- /units/debug/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | node_modules 4 | package-lock.json 5 | *.egg-info 6 | __pycache__ 7 | 8 | src/*/client/** 9 | !src/*/client/*.py 10 | -------------------------------------------------------------------------------- /units/debug/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "build": "npm run build:js && (npm test || true)", 4 | "build:js": "esbuild src/index.ts --bundle --external:pr1 --external:react --format=esm --minify --outdir=../src/pr1_debug/client", 5 | "test": "tsc" 6 | }, 7 | "devDependencies": { 8 | "@types/react": "^18.0.16", 9 | "esbuild": "~0.15.16", 10 | "pr1": "file:../../../client", 11 | "pr1-shared": "file:../../../app/shared", 12 | "sass": "^1.54.4", 13 | "typescript": "~4.9.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /units/debug/client/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Plugin } from 'pr1'; 2 | import { PluginName } from 'pr1-shared'; 3 | 4 | 5 | export default { 6 | namespace: ('debug' as PluginName), 7 | blocks: {} 8 | } satisfies Plugin 9 | -------------------------------------------------------------------------------- /units/debug/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "files": [ 4 | "src/index.ts" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /units/debug/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=66", "setuptools_scm[toml]>=6.2"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "pr1-debug" 7 | dynamic = ["version"] 8 | requires-python = ">3.11" 9 | 10 | [project.entry-points."pr1.units"] 11 | debug = "pr1_debug" 12 | 13 | [tool.setuptools.packages.find] 14 | where = ["src"] 15 | 16 | [tool.setuptools.package-data] 17 | "pr1_debug.client" = ["*"] 18 | 19 | [tool.setuptools_scm] 20 | root = "../.." 21 | -------------------------------------------------------------------------------- /units/debug/src/pr1_debug/__init__.py: -------------------------------------------------------------------------------- 1 | from importlib.resources import files 2 | 3 | from pr1.units.base import Metadata, MetadataIcon, logger as parent_logger 4 | 5 | 6 | namespace = "debug" 7 | version = 0 8 | 9 | metadata = Metadata( 10 | description="Simple template unit.", 11 | icon=MetadataIcon(kind='icon', value="description"), 12 | title="Debug", 13 | version="1.0" 14 | ) 15 | 16 | client_path = files(__name__ + '.client') 17 | logger = parent_logger.getChild(namespace) 18 | 19 | from .executor import Executor 20 | -------------------------------------------------------------------------------- /units/debug/src/pr1_debug/client/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adaptyvbio/automancer/d577cc1775767b5d4839f908410ca5c4a0dadb9f/units/debug/src/pr1_debug/client/__init__.py -------------------------------------------------------------------------------- /units/expect/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | node_modules 4 | package-lock.json 5 | *.egg-info 6 | __pycache__ 7 | 8 | src/*/client/** 9 | !src/*/client/*.py 10 | -------------------------------------------------------------------------------- /units/expect/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "build": "npm run build:js && (npm test || true)", 4 | "build:js": "esbuild src/index.tsx --bundle --external:pr1 --external:react --format=esm --minify --outdir=../src/pr1_expect/client", 5 | "test": "tsc" 6 | }, 7 | "devDependencies": { 8 | "@types/react": "^18.0.16", 9 | "esbuild": "~0.15.16", 10 | "pr1": "file:../../../client", 11 | "typescript": "~4.9.0" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /units/expect/client/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { DynamicValue, formatDynamicValue, StateUnit } from 'pr1'; 2 | 3 | 4 | export interface State { 5 | entries: { 6 | condition: DynamicValue; 7 | }[]; 8 | } 9 | 10 | export interface Location { 11 | 12 | } 13 | 14 | export default { 15 | namespace: 'expect', 16 | createStateFeatures(state, ancestorStates, location, context) { 17 | return state.entries.map((entry) => ({ 18 | icon: 'notification_important', 19 | label: formatDynamicValue(entry.condition), 20 | description: 'Expect' 21 | })); 22 | } 23 | } satisfies StateUnit 24 | -------------------------------------------------------------------------------- /units/expect/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "files": [ 4 | "src/index.tsx" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /units/expect/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=42"] 3 | build-backend = "setuptools.build_meta" 4 | -------------------------------------------------------------------------------- /units/expect/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | 3 | setup( 4 | name="pr1_expect", 5 | version="0.0.0", 6 | 7 | packages=find_packages(where="src"), 8 | package_dir={"": "src"}, 9 | 10 | entry_points={ 11 | 'pr1.units': [ 12 | "expect = pr1_expect" 13 | ] 14 | }, 15 | package_data={ 16 | "pr1_expect.client": ["*"] 17 | } 18 | ) 19 | -------------------------------------------------------------------------------- /units/expect/src/pr1_expect/__init__.py: -------------------------------------------------------------------------------- 1 | from importlib.resources import files 2 | 3 | from pr1.units.base import Metadata, MetadataIcon, logger as parent_logger 4 | 5 | 6 | namespace = "expect" 7 | version = 0 8 | 9 | metadata = Metadata( 10 | description="Expect", 11 | icon=MetadataIcon(kind='icon', value="notification_important"), 12 | title="Expect", 13 | version="1.0" 14 | ) 15 | 16 | client_path = files(__name__ + '.client') 17 | logger = parent_logger.getChild(namespace) 18 | 19 | from .parser import Parser 20 | from .runner import Runner 21 | -------------------------------------------------------------------------------- /units/expect/src/pr1_expect/client/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adaptyvbio/automancer/d577cc1775767b5d4839f908410ca5c4a0dadb9f/units/expect/src/pr1_expect/client/__init__.py -------------------------------------------------------------------------------- /units/mfcontrol/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | node_modules 4 | package-lock.json 5 | *.egg-info 6 | __pycache__ 7 | 8 | src/*/client/** 9 | !src/*/client/*.py 10 | -------------------------------------------------------------------------------- /units/mfcontrol/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "build": "npm run build:css && npm run build:js && (npm test || true)", 4 | "build:css": "sass styles:../src/pr1_mfcontrol/client --no-source-map", 5 | "build:js": "esbuild src/index.ts --bundle --external:*.css --external:pr1 --external:react --format=esm --minify --outdir=../src/pr1_mfcontrol/client", 6 | "test": "tsc" 7 | }, 8 | "dependencies": { 9 | "immutable": "^4.1.0" 10 | }, 11 | "devDependencies": { 12 | "@types/react": "^18.0.16", 13 | "esbuild": "~0.15.16", 14 | "pr1": "file:../../../client", 15 | "sass": "^1.54.4", 16 | "typescript": "~4.9.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /units/mfcontrol/client/src/types.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.css'; 2 | -------------------------------------------------------------------------------- /units/mfcontrol/client/src/util.ts: -------------------------------------------------------------------------------- 1 | export function encodeIndices(arr: number[]): bigint { 2 | return arr.reduce((sum, item) => sum | (1n << BigInt(item)), 0n); 3 | } 4 | -------------------------------------------------------------------------------- /units/mfcontrol/client/styles/index.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adaptyvbio/automancer/d577cc1775767b5d4839f908410ca5c4a0dadb9f/units/mfcontrol/client/styles/index.scss -------------------------------------------------------------------------------- /units/mfcontrol/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "files": [ 4 | "src/index.ts" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /units/mfcontrol/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=42"] 3 | build-backend = "setuptools.build_meta" 4 | -------------------------------------------------------------------------------- /units/mfcontrol/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | 3 | setup( 4 | name="pr1_mfcontrol", 5 | version="0.0.0", 6 | 7 | packages=find_packages(where="src"), 8 | package_dir={"": "src"}, 9 | 10 | entry_points={ 11 | 'pr1.units': [ 12 | "mfcontrol = pr1_mfcontrol", 13 | ] 14 | }, 15 | package_data={ 16 | "pr1_mfcontrol.client": ["*"], 17 | "pr1_mfcontrol.data": ["*"] 18 | }, 19 | 20 | install_requires=[ 21 | "regex==2022.8.17" 22 | ] 23 | ) 24 | -------------------------------------------------------------------------------- /units/mfcontrol/src/pr1_mfcontrol/__init__.py: -------------------------------------------------------------------------------- 1 | from importlib.resources import files 2 | 3 | from pr1.units.base import Metadata, MetadataIcon, logger as parent_logger 4 | 5 | 6 | namespace = "mfcontrol" 7 | version = 0 8 | 9 | metadata = Metadata( 10 | description="Microfluidic control.", 11 | icon=MetadataIcon(kind='icon', value="air"), 12 | title="Microfluidic control", 13 | version="1.1" 14 | ) 15 | 16 | client_path = files(__name__ + '.client') 17 | logger = parent_logger.getChild(namespace) 18 | 19 | from .executor import Executor 20 | from .parser import Parser 21 | from .runner import Runner 22 | -------------------------------------------------------------------------------- /units/mfcontrol/src/pr1_mfcontrol/client/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adaptyvbio/automancer/d577cc1775767b5d4839f908410ca5c4a0dadb9f/units/mfcontrol/src/pr1_mfcontrol/client/__init__.py -------------------------------------------------------------------------------- /units/mfcontrol/src/pr1_mfcontrol/data/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adaptyvbio/automancer/d577cc1775767b5d4839f908410ca5c4a0dadb9f/units/mfcontrol/src/pr1_mfcontrol/data/__init__.py -------------------------------------------------------------------------------- /units/mfcontrol/src/pr1_mfcontrol/data/repr.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": "flow", 3 | "icons": { 4 | "barrier": { 5 | "forwards": "vertical_align_center", 6 | "backwards": "-vertical_align_center" 7 | }, 8 | "cycle": { 9 | "forwards": "cycle", 10 | "backwards": "-cycle" 11 | }, 12 | "fill": { 13 | "forwards": "format_color_fill", 14 | "backwards": "-format_color_fill" 15 | }, 16 | "flow": { 17 | "forwards": "air", 18 | "backwards": "-air" 19 | }, 20 | "isolate": { 21 | "forwards": "view_column", 22 | "backwards": "-view_column" 23 | }, 24 | "left": { 25 | "forwards": "turn_slight_left", 26 | "backwards": "-turn_slight_left" 27 | }, 28 | "merge": { 29 | "forwards": "call_merge", 30 | "backwards": "-call_merge" 31 | }, 32 | "move": { 33 | "forwards": "moving", 34 | "backwards": "-moving" 35 | }, 36 | "push": { 37 | "forwards": "download", 38 | "backwards": "upload" 39 | }, 40 | "right": { 41 | "forwards": "turn_slight_right", 42 | "backwards": "-turn_slight_right" 43 | }, 44 | "split": { 45 | "forwards": "call_split", 46 | "backwards": "-call_split" 47 | }, 48 | "subset": { 49 | "forwards": "table_rows", 50 | "backwards": "-table_rows" 51 | }, 52 | "valve": { 53 | "forwards": "valve", 54 | "backwards": "-valve" 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /units/numato/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | *.egg-info 4 | __pycache__ 5 | -------------------------------------------------------------------------------- /units/numato/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=42"] 3 | build-backend = "setuptools.build_meta" 4 | -------------------------------------------------------------------------------- /units/numato/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | 3 | setup( 4 | name="pr1_numato", 5 | version="0.0.0", 6 | 7 | packages=find_packages(where="src"), 8 | package_dir={"": "src"}, 9 | 10 | entry_points={ 11 | 'pr1.units': [ 12 | "numato = pr1_numato" 13 | ] 14 | }, 15 | package_data={ 16 | "pr1_numato.data": ["*"] 17 | }, 18 | 19 | install_requires=[ 20 | "aioserial==1.3.1", 21 | "pyserial==3.5" 22 | ] 23 | ) 24 | -------------------------------------------------------------------------------- /units/numato/src/pr1_numato/__init__.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from pr1.units.base import Metadata, MetadataIcon, logger as parent_logger 4 | 5 | 6 | namespace = "numato" 7 | version = 0 8 | 9 | metadata = Metadata( 10 | description="This unit adds support to the Numato relay modules.", 11 | icon=MetadataIcon(kind='svg', value=(Path(__file__).parent / "data/logo.svg").open().read()), 12 | title="Numato", 13 | version="3.0" 14 | ) 15 | 16 | logger = parent_logger.getChild(namespace) 17 | 18 | from .executor import Executor 19 | -------------------------------------------------------------------------------- /units/numato/src/pr1_numato/data/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adaptyvbio/automancer/d577cc1775767b5d4839f908410ca5c4a0dadb9f/units/numato/src/pr1_numato/data/__init__.py -------------------------------------------------------------------------------- /units/numato/src/pr1_numato/data/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /units/numato/src/pr1_numato/devices/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adaptyvbio/automancer/d577cc1775767b5d4839f908410ca5c4a0dadb9f/units/numato/src/pr1_numato/devices/__init__.py -------------------------------------------------------------------------------- /units/okolab/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | *.egg-info 4 | __pycache__ 5 | -------------------------------------------------------------------------------- /units/okolab/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=66", "setuptools_scm[toml]>=6.2"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "pr1-okolab" 7 | dynamic = ["version"] 8 | requires-python = ">3.11" 9 | 10 | dependencies=[ 11 | "okolab~=0.2.1" 12 | ] 13 | 14 | [project.entry-points."pr1.units"] 15 | okolab = "pr1_okolab" 16 | 17 | [tool.setuptools.packages.find] 18 | where = ["src"] 19 | 20 | [tool.setuptools.package-data] 21 | "pr1_okolab.client" = ["*"] 22 | 23 | [tool.setuptools_scm] 24 | root = "../.." 25 | -------------------------------------------------------------------------------- /units/okolab/src/pr1_okolab/__init__.py: -------------------------------------------------------------------------------- 1 | from pr1.units.base import Metadata, MetadataIcon, logger as parent_logger 2 | 3 | 4 | namespace = "okolab" 5 | version = 0 6 | 7 | metadata = Metadata( 8 | description="Okolab", 9 | icon=MetadataIcon(kind='icon', value="description"), 10 | title="Okolab", 11 | version="3.0" 12 | ) 13 | 14 | logger = parent_logger.getChild(namespace) 15 | 16 | from .executor import Executor 17 | -------------------------------------------------------------------------------- /units/opcua/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | node_modules 4 | package-lock.json 5 | *.egg-info 6 | __pycache__ 7 | 8 | src/*/client/** 9 | !src/*/client/*.py 10 | -------------------------------------------------------------------------------- /units/opcua/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "build": "npm run build:js && (npm test || true)", 4 | "build:js": "esbuild src/index.tsx --bundle --external:pr1 --external:react --format=esm --minify --outdir=../src/pr1_opcua/client", 5 | "test": "tsc" 6 | }, 7 | "devDependencies": { 8 | "@types/react": "^18.0.16", 9 | "esbuild": "~0.15.16", 10 | "pr1": "file:../../../client", 11 | "typescript": "~4.9.0" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /units/opcua/client/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Description, ItemList, React, Unit } from 'pr1'; 2 | 3 | 4 | export default { 5 | namespace: 'opcua', 6 | 7 | OptionsComponent(props) { 8 | return ( 9 | 10 |

OPC-UA

11 | 12 |

Devices

13 | 14 | ` 21 | } }, 22 | { id: 'b', 23 | label: 'USB ACM 2', 24 | description: 'Not configured', 25 | action: { 26 | type: 'explicit', 27 | contents: ( 28 | 29 | ) 30 | } }, 31 | { id: 'c', 32 | label: 'USB ACM 2', 33 | description: 'Not configured' } 34 | ]} /> 35 |
36 | ); 37 | } 38 | } satisfies Unit; 39 | -------------------------------------------------------------------------------- /units/opcua/client/styles/index.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adaptyvbio/automancer/d577cc1775767b5d4839f908410ca5c4a0dadb9f/units/opcua/client/styles/index.scss -------------------------------------------------------------------------------- /units/opcua/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "files": [ 4 | "src/index.tsx" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /units/opcua/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=66", "setuptools_scm[toml]>=6.2"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "pr1-opcua" 7 | dynamic = ["version"] 8 | requires-python = ">3.11" 9 | 10 | dependencies=[ 11 | "asyncua==1.0.0" 12 | ] 13 | 14 | [project.entry-points."automancer.plugins"] 15 | opcua = "pr1_opcua" 16 | 17 | [tool.setuptools.packages.find] 18 | where = ["src"] 19 | 20 | [tool.setuptools.package-data] 21 | "pr1_opcua.client" = ["*"] 22 | 23 | [tool.setuptools_scm] 24 | root = "../.." 25 | -------------------------------------------------------------------------------- /units/opcua/src/pr1_opcua/__init__.py: -------------------------------------------------------------------------------- 1 | from importlib.resources import files 2 | 3 | from pr1.units.base import Metadata, MetadataIcon, logger as parent_logger 4 | 5 | 6 | namespace = "opcua" 7 | version = 0 8 | 9 | metadata = Metadata( 10 | description="OPC-UA communication.", 11 | icon=MetadataIcon(kind='icon', value="sensors"), 12 | title="OPC-UA", 13 | version="5.0" 14 | ) 15 | 16 | client_path = files(__name__ + '.client') 17 | logger = parent_logger.getChild(namespace) 18 | 19 | from .executor import Executor 20 | -------------------------------------------------------------------------------- /units/opcua/src/pr1_opcua/client/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adaptyvbio/automancer/d577cc1775767b5d4839f908410ca5c4a0dadb9f/units/opcua/src/pr1_opcua/client/__init__.py -------------------------------------------------------------------------------- /units/record/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.egg-info 3 | build 4 | dist 5 | node_modules 6 | package-lock.json 7 | 8 | src/*/client/** 9 | !src/*/client/*.py 10 | -------------------------------------------------------------------------------- /units/record/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "build": "npm run build:js && (npm test || true)", 4 | "build:js": "esbuild src/index.tsx --bundle --external:pr1 --external:react --format=esm --minify --outdir=../src/pr1_record/client", 5 | "test": "tsc" 6 | }, 7 | "devDependencies": { 8 | "@types/react": "^18.0.16", 9 | "esbuild": "~0.15.16", 10 | "pr1": "file:../../../client", 11 | "pr1-shared": "file:../../../app/shared", 12 | "typescript": "~4.9.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /units/record/client/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { Plugin, PluginBlockImpl } from 'pr1'; 2 | import { PluginName, ProtocolBlock, ProtocolBlockName } from 'pr1-shared'; 3 | 4 | 5 | export interface Block extends ProtocolBlock { 6 | child: ProtocolBlock; 7 | } 8 | 9 | export interface Location { 10 | rows: number; 11 | } 12 | 13 | export default { 14 | namespace: ('record' as PluginName), 15 | 16 | blocks: { 17 | ['_' as ProtocolBlockName]: { 18 | createFeatures(block, location, context) { 19 | return [ 20 | { icon: 'monitoring', 21 | label: 'Record data' + (location ? ` (${location.rows} rows)` : '') } 22 | ]; 23 | }, 24 | getChildren(block, context) { 25 | return [block.child]; 26 | }, 27 | } satisfies PluginBlockImpl 28 | } 29 | } satisfies Plugin; 30 | -------------------------------------------------------------------------------- /units/record/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "files": [ 4 | "src/index.tsx" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /units/record/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=66", "setuptools_scm[toml]>=6.2"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "pr1-record" 7 | dynamic = ["version"] 8 | requires-python = ">3.11" 9 | 10 | dependencies=[ 11 | "numpy>=1.23.0", 12 | "pandas>=1.5.0" 13 | ] 14 | 15 | [project.entry-points."pr1.units"] 16 | record = "pr1_record" 17 | 18 | [tool.setuptools.packages.find] 19 | where = ["src"] 20 | 21 | [tool.setuptools.package-data] 22 | "pr1_record.client" = ["*"] 23 | 24 | [tool.setuptools_scm] 25 | root = "../.." 26 | -------------------------------------------------------------------------------- /units/record/src/pr1_record/__init__.py: -------------------------------------------------------------------------------- 1 | from importlib.resources import files 2 | 3 | from pr1.units.base import Metadata, MetadataIcon, logger as parent_logger 4 | 5 | 6 | namespace = "record" 7 | version = 0 8 | 9 | metadata = Metadata( 10 | description="Record", 11 | icon=MetadataIcon(kind='icon', value="monitoring"), 12 | title="Record", 13 | version="2.0" 14 | ) 15 | 16 | client_path = files(__name__ + '.client') 17 | logger = parent_logger.getChild(namespace) 18 | 19 | from .parser import Parser 20 | # from .runner import Runner 21 | -------------------------------------------------------------------------------- /units/record/src/pr1_record/client/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adaptyvbio/automancer/d577cc1775767b5d4839f908410ca5c4a0dadb9f/units/record/src/pr1_record/client/__init__.py -------------------------------------------------------------------------------- /units/s3/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | node_modules 4 | package-lock.json 5 | *.egg-info 6 | __pycache__ 7 | 8 | src/*/client/** 9 | !src/*/client/*.py 10 | -------------------------------------------------------------------------------- /units/s3/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "build": "npm run build:js && (npm test || true)", 4 | "build:js": "esbuild src/index.tsx --bundle --external:pr1 --external:react --format=esm --minify --outdir=../src/pr1_s3/client", 5 | "test": "tsc" 6 | }, 7 | "devDependencies": { 8 | "@types/react": "^18.0.16", 9 | "esbuild": "~0.15.16", 10 | "pr1": "file:../../../client", 11 | "pr1-shared": "file:../../../app/shared", 12 | "typescript": "~4.9.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /units/s3/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "files": [ 4 | "src/index.tsx" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /units/s3/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=66", "setuptools_scm[toml]>=6.2"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "pr1-s3" 7 | dynamic = ["version"] 8 | requires-python = ">3.11" 9 | 10 | dependencies=[ 11 | "boto3==1.26.46" 12 | ] 13 | 14 | [project.entry-points."pr1.units"] 15 | s3 = "pr1_s3" 16 | 17 | [tool.setuptools.packages.find] 18 | where = ["src"] 19 | 20 | [tool.setuptools.package-data] 21 | "pr1_s3.client" = ["*"] 22 | 23 | [tool.setuptools_scm] 24 | root = "../.." 25 | -------------------------------------------------------------------------------- /units/s3/src/pr1_s3/__init__.py: -------------------------------------------------------------------------------- 1 | from importlib.resources import files 2 | 3 | from pr1.units.base import Metadata, MetadataIcon, logger as parent_logger 4 | 5 | 6 | namespace = "s3" 7 | version = 0 8 | 9 | metadata = Metadata( 10 | description="S3 Sync", 11 | icon=MetadataIcon(kind='icon', value="database"), 12 | title="S3 Sync", 13 | version="2.0" 14 | ) 15 | 16 | client_path = files(__name__ + '.client') 17 | logger = parent_logger.getChild(namespace) 18 | 19 | from .parser import Parser 20 | -------------------------------------------------------------------------------- /units/s3/src/pr1_s3/client/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adaptyvbio/automancer/d577cc1775767b5d4839f908410ca5c4a0dadb9f/units/s3/src/pr1_s3/client/__init__.py -------------------------------------------------------------------------------- /units/simple/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | *.egg-info 4 | __pycache__ 5 | -------------------------------------------------------------------------------- /units/simple/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=42"] 3 | build-backend = "setuptools.build_meta" 4 | -------------------------------------------------------------------------------- /units/simple/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | 3 | setup( 4 | name="pr1_simple", 5 | version="0.0.0", 6 | 7 | packages=find_packages(where="src"), 8 | package_dir={"": "src"}, 9 | 10 | entry_points={ 11 | 'pr1.units': [ 12 | "simple = pr1_simple", 13 | ] 14 | }, 15 | package_data={ 16 | "pr1_simple.client": ["*"] 17 | } 18 | ) 19 | -------------------------------------------------------------------------------- /units/simple/src/pr1_simple/__init__.py: -------------------------------------------------------------------------------- 1 | from importlib.resources import files 2 | 3 | from pr1.units.base import Metadata, MetadataIcon, logger as parent_logger 4 | 5 | 6 | namespace = "simple" 7 | version = 0 8 | 9 | metadata = Metadata( 10 | description="Simple template unit.", 11 | icon=MetadataIcon(kind='icon', value="description"), 12 | title="Simple template", 13 | version="1.0" 14 | ) 15 | 16 | client_path = files(__name__ + '.client') 17 | logger = parent_logger.getChild(namespace) 18 | -------------------------------------------------------------------------------- /units/simple/src/pr1_simple/client/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adaptyvbio/automancer/d577cc1775767b5d4839f908410ca5c4a0dadb9f/units/simple/src/pr1_simple/client/__init__.py -------------------------------------------------------------------------------- /units/simple/src/pr1_simple/client/index.css: -------------------------------------------------------------------------------- 1 | .rocket-title { 2 | color: red; 3 | } 4 | -------------------------------------------------------------------------------- /units/simple/src/pr1_simple/client/index.js: -------------------------------------------------------------------------------- 1 | import htm from 'https://cdn.skypack.dev/htm'; 2 | import { React } from 'pr1'; 3 | 4 | import mainStyles from './index.css' assert { type: 'css' }; 5 | 6 | 7 | const html = htm.bind(React.createElement); 8 | 9 | export const namespace = 'simple'; 10 | export const styleSheets = [mainStyles]; 11 | 12 | 13 | export function getGeneralTabs() { 14 | return [ 15 | { 16 | id: 'simple.example', 17 | label: 'Example', 18 | icon: 'rocket', 19 | component: ExampleTab 20 | } 21 | ]; 22 | } 23 | 24 | export function ExampleTab(props) { 25 | return html` 26 |
27 |

Rocket

28 |
29 | `; 30 | } 31 | -------------------------------------------------------------------------------- /units/slack/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | node_modules 4 | package-lock.json 5 | *.egg-info 6 | __pycache__ 7 | 8 | src/*/client/** 9 | !src/*/client/*.py 10 | -------------------------------------------------------------------------------- /units/slack/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "build": "npm run build:js && (npm test || true)", 4 | "build:js": "esbuild src/index.tsx --bundle --external:pr1 --external:react --format=esm --minify --outdir=../src/pr1_slack/client", 5 | "test": "tsc" 6 | }, 7 | "devDependencies": { 8 | "@types/react": "^18.0.16", 9 | "esbuild": "~0.15.16", 10 | "pr1": "file:../../../client", 11 | "pr1-shared": "file:../../../app/shared", 12 | "typescript": "~4.9.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /units/slack/client/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { DynamicValue, ExpandableText, Plugin, ProgressBar, createProcessBlockImpl, formatDynamicValue } from 'pr1'; 2 | import { PluginName, ProtocolBlockName } from 'pr1-shared'; 3 | 4 | 5 | export interface ProcessData { 6 | 7 | } 8 | 9 | export interface ProcessLocation { 10 | body: string; 11 | fileCount: number; 12 | phase: number; 13 | } 14 | 15 | export default { 16 | namespace: ('s3' as PluginName), 17 | blocks: { 18 | ['_' as ProtocolBlockName]: createProcessBlockImpl({ 19 | Component(props) { 20 | return

Progress: {props.location.phase}/{props.location.fileCount + 1}

21 | }, 22 | createFeatures(data, location) { 23 | return [ 24 | { icon: 'chat', 25 | description: 'Slack message', 26 | label: location?.body ?? '...' } 27 | ]; 28 | } 29 | }) 30 | } 31 | } satisfies Plugin 32 | -------------------------------------------------------------------------------- /units/slack/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "files": [ 4 | "src/index.tsx" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /units/slack/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=66", "setuptools_scm[toml]>=6.2"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "pr1-slack" 7 | dynamic = ["version"] 8 | requires-python = ">3.11" 9 | 10 | dependencies=[ 11 | "slack_sdk~=3.21.3" 12 | ] 13 | 14 | [project.entry-points."automancer.plugins"] 15 | slack = "pr1_slack" 16 | 17 | [tool.setuptools.packages.find] 18 | where = ["src"] 19 | 20 | [tool.setuptools.package-data] 21 | "pr1_slack.client" = ["*"] 22 | 23 | [tool.setuptools_scm] 24 | root = "../.." 25 | -------------------------------------------------------------------------------- /units/slack/src/pr1_slack/__init__.py: -------------------------------------------------------------------------------- 1 | from importlib.resources import files 2 | 3 | import automancer as am 4 | 5 | 6 | namespace = am.PluginName("slack") 7 | version = 0 8 | 9 | metadata = am.Metadata( 10 | description="Slack", 11 | icon=am.MetadataIcon(kind='icon', value="chat"), 12 | title="Slack", 13 | version="1.0" 14 | ) 15 | 16 | client_path = files(__name__ + '.client') 17 | logger = am.logger.getChild(namespace) 18 | 19 | from .parser import Parser 20 | -------------------------------------------------------------------------------- /units/slack/src/pr1_slack/client/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adaptyvbio/automancer/d577cc1775767b5d4839f908410ca5c4a0dadb9f/units/slack/src/pr1_slack/client/__init__.py -------------------------------------------------------------------------------- /units/template/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | node_modules 4 | package-lock.json 5 | *.egg-info 6 | __pycache__ 7 | 8 | src/*/client/** 9 | !src/*/client/*.py 10 | -------------------------------------------------------------------------------- /units/template/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "build": "npm run build:css && npm run build:js && (npm test || true)", 4 | "build:css": "sass styles:../src/pr1_template/client --no-source-map", 5 | "build:js": "esbuild src/index.ts --bundle --external:*.css --external:pr1 --external:react --format=esm --minify --outdir=../src/pr1_template/client", 6 | "test": "tsc" 7 | }, 8 | "devDependencies": { 9 | "@types/react": "^18.0.16", 10 | "esbuild": "~0.15.16", 11 | "pr1": "file:../../../client", 12 | "sass": "^1.54.4", 13 | "typescript": "~4.9.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /units/template/client/src/index.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import mainStyles from './index.css' assert { type: 'css' }; 4 | 5 | 6 | export const namespace = 'template'; 7 | export const styleSheets = [mainStyles]; 8 | -------------------------------------------------------------------------------- /units/template/client/src/types.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.css'; 2 | -------------------------------------------------------------------------------- /units/template/client/styles/index.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adaptyvbio/automancer/d577cc1775767b5d4839f908410ca5c4a0dadb9f/units/template/client/styles/index.scss -------------------------------------------------------------------------------- /units/template/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "files": [ 4 | "src/index.ts" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /units/template/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=42"] 3 | build-backend = "setuptools.build_meta" 4 | -------------------------------------------------------------------------------- /units/template/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | 3 | setup( 4 | name="pr1_template", 5 | version="0.0.0", 6 | 7 | packages=find_packages(where="src"), 8 | package_dir={"": "src"}, 9 | 10 | entry_points={ 11 | 'pr1.units': [ 12 | "template = pr1_template", 13 | ] 14 | }, 15 | package_data={ 16 | "pr1_template.client": ["*"] 17 | } 18 | ) 19 | -------------------------------------------------------------------------------- /units/template/src/pr1_template/__init__.py: -------------------------------------------------------------------------------- 1 | from importlib.resources import files 2 | 3 | from pr1.units.base import Metadata, MetadataIcon, logger as parent_logger 4 | 5 | 6 | namespace = "template" 7 | version = 0 8 | 9 | metadata = Metadata( 10 | description="Template unit.", 11 | icon=MetadataIcon(kind='icon', value="description"), 12 | title="Template", 13 | version="1.0" 14 | ) 15 | 16 | client_path = files(__name__ + '.client') 17 | logger = parent_logger.getChild(namespace) 18 | 19 | # from .executor import Executor 20 | # from .matrix import Matrix 21 | # from .runner import Runner 22 | -------------------------------------------------------------------------------- /units/template/src/pr1_template/client/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adaptyvbio/automancer/d577cc1775767b5d4839f908410ca5c4a0dadb9f/units/template/src/pr1_template/client/__init__.py -------------------------------------------------------------------------------- /units/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../shared/tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "jsx": "react-jsx", 6 | "module": "esnext", 7 | "moduleResolution": "node", 8 | "noEmit": true 9 | }, 10 | "references": [ 11 | { "path": "../client" } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /units/utils/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | node_modules 4 | package-lock.json 5 | *.egg-info 6 | __pycache__ 7 | 8 | src/*/client/** 9 | !src/*/client/*.py 10 | -------------------------------------------------------------------------------- /units/utils/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "build": "npm run build:js && (npm test || true)", 4 | "build:js": "esbuild src/index.tsx --bundle --external:pr1 --external:react --format=esm --minify --outdir=../src/pr1_utils/client", 5 | "test": "tsc" 6 | }, 7 | "devDependencies": { 8 | "@types/react": "^18.0.16", 9 | "esbuild": "~0.15.16", 10 | "pr1": "file:../../../client", 11 | "typescript": "~4.9.0" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /units/utils/client/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { DynamicValue, formatDynamicValue, ProcessUnit, React } from 'pr1'; 2 | 3 | 4 | export interface ProcessData { 5 | type: 'run'; 6 | command: DynamicValue; 7 | } 8 | 9 | export interface ProcessLocation { 10 | command: string; 11 | pid: number; 12 | } 13 | 14 | export default { 15 | namespace: 'utils', 16 | 17 | ProcessComponent(props) { 18 | return ( 19 |
PID: {props.location.pid}
20 | ); 21 | }, 22 | 23 | createProcessFeatures(data, location, context) { 24 | switch (data.type) { 25 | case 'run': return [ 26 | { description: 'Run command', 27 | icon: 'terminal', 28 | label: location 29 | ? location.command 30 | : formatDynamicValue(data.command) } 31 | ]; 32 | } 33 | }, 34 | getProcessLabel(data, context) { 35 | return 'Run command'; 36 | } 37 | } satisfies ProcessUnit 38 | -------------------------------------------------------------------------------- /units/utils/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "files": [ 4 | "src/index.tsx" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /units/utils/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=42"] 3 | build-backend = "setuptools.build_meta" 4 | -------------------------------------------------------------------------------- /units/utils/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | 3 | setup( 4 | name="pr1_utils", 5 | version="0.0.0", 6 | 7 | packages=find_packages(where="src"), 8 | package_dir={"": "src"}, 9 | 10 | entry_points={ 11 | 'pr1.units': [ 12 | "utils = pr1_utils", 13 | ] 14 | }, 15 | package_data={ 16 | "pr1_utils.client": ["*"] 17 | }, 18 | 19 | install_requires=[ 20 | "psutil==5.9.4" 21 | ] 22 | ) 23 | -------------------------------------------------------------------------------- /units/utils/src/pr1_utils/__init__.py: -------------------------------------------------------------------------------- 1 | from importlib.resources import files 2 | 3 | from pr1.units.base import Metadata, MetadataIcon, logger as parent_logger 4 | 5 | 6 | namespace = "utils" 7 | version = 0 8 | 9 | metadata = Metadata( 10 | description="Utilities", 11 | icon=MetadataIcon(kind='icon', value="terminal"), 12 | title="Utilities", 13 | version="1.0" 14 | ) 15 | 16 | client_path = files(__name__ + '.client') 17 | logger = parent_logger.getChild(namespace) 18 | 19 | from .executor import Executor 20 | # from .parser import Parser 21 | # from .runner import Runner 22 | -------------------------------------------------------------------------------- /units/utils/src/pr1_utils/client/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adaptyvbio/automancer/d577cc1775767b5d4839f908410ca5c4a0dadb9f/units/utils/src/pr1_utils/client/__init__.py --------------------------------------------------------------------------------