Compare commits
	
		
			61 Commits
		
	
	
		
			remove-unu
			...
			franknoiro
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 7255dea879 | |||
| 700c98f07f | |||
| 45a2783ed0 | |||
| ab9c736d3c | |||
| 0dc2216848 | |||
| 9ea5be90f4 | |||
| f2ef6e968b | |||
| 6cb90195bf | |||
| 4b537cff24 | |||
| 8112db21bb | |||
| b6d0ad0a9b | |||
| 81d6c4c1b3 | |||
| 4e9bdb5b41 | |||
| 3d30282bc6 | |||
| a9e4935619 | |||
| f7068e51d3 | |||
| 3c61d79a65 | |||
| 7fc61110f6 | |||
| e330059e99 | |||
| 38b447e9da | |||
| eeddd4fb07 | |||
| 5524064902 | |||
| fbf94aaba9 | |||
| e77b42f308 | |||
| e344e4a063 | |||
| fb3fcfeef3 | |||
| ca466275aa | |||
| 3bc1547a03 | |||
| 22251ea353 | |||
| 6f2e227ea7 | |||
| a5fd0150b8 | |||
| d210feeebe | |||
| 5c34fdd619 | |||
| 4d65a4618e | |||
| 80af10a988 | |||
| 01fc2c43c2 | |||
| 8402975e80 | |||
| 657883e09a | |||
| c68718d835 | |||
| 0a2e668ee4 | |||
| 5ac504d000 | |||
| 60354af367 | |||
| 9978ad6e23 | |||
| c9587fda07 | |||
| fcffc72655 | |||
| 216af5a0ca | |||
| af0c591639 | |||
| 15d676a2c3 | |||
| b5ff97c99b | |||
| a3b8b1c859 | |||
| ba2570d2cd | |||
| b3e1326921 | |||
| 8f3687106e | |||
| 562f8337bd | |||
| 8490b3815d | |||
| 0236926cc8 | |||
| 5d0530257c | |||
| 9d673d1903 | |||
| 983d6160d1 | |||
| 7c1f3bc484 | |||
| 4c6ad6d0ca | 
| @ -3,14 +3,13 @@ | ||||
| > dpdm --no-warning --no-tree -T --skip-dynamic-imports=circular src/index.tsx | ||||
|  | ||||
| • Circular Dependencies | ||||
|   01) src/lang/std/sketch.ts -> src/lang/modifyAst.ts -> src/lang/modifyAst/addEdgeTreatment.ts | ||||
|   02) src/lang/std/sketch.ts -> src/lang/modifyAst.ts | ||||
|   03) src/lang/std/sketch.ts -> src/lang/modifyAst.ts -> src/lang/std/sketchcombos.ts | ||||
|   04) src/lib/singletons.ts -> src/editor/manager.ts -> src/lib/selections.ts | ||||
|   05) src/lib/singletons.ts -> src/editor/manager.ts -> src/lib/selections.ts -> src/machines/appMachine.ts -> src/machines/engineStreamMachine.ts | ||||
|   06) src/lib/singletons.ts -> src/editor/manager.ts -> src/lib/selections.ts -> src/machines/appMachine.ts -> src/machines/settingsMachine.ts | ||||
|   07) src/machines/appMachine.ts -> src/machines/settingsMachine.ts -> src/machines/commandBarMachine.ts -> src/lib/commandBarConfigs/authCommandConfig.ts | ||||
|   08) src/lib/singletons.ts -> src/lang/codeManager.ts | ||||
|   09) src/lib/singletons.ts -> src/clientSideScene/sceneEntities.ts -> src/clientSideScene/segments.ts -> src/components/Toolbar/angleLengthInfo.ts | ||||
|   10) src/hooks/useModelingContext.ts -> src/components/ModelingMachineProvider.tsx -> src/components/Toolbar/Intersect.tsx -> src/components/SetHorVertDistanceModal.tsx -> src/lib/useCalculateKclExpression.ts | ||||
|   11) src/routes/Onboarding/index.tsx -> src/routes/Onboarding/Camera.tsx -> src/routes/Onboarding/utils.tsx | ||||
|   1) src/lang/std/sketch.ts -> src/lang/modifyAst.ts -> src/lang/modifyAst/addEdgeTreatment.ts | ||||
|   2) src/lang/std/sketch.ts -> src/lang/modifyAst.ts | ||||
|   3) src/lang/std/sketch.ts -> src/lang/modifyAst.ts -> src/lang/std/sketchcombos.ts | ||||
|   4) src/lib/singletons.ts -> src/editor/manager.ts -> src/lib/selections.ts | ||||
|   5) src/lib/singletons.ts -> src/editor/manager.ts -> src/lib/selections.ts | ||||
|   6) src/lib/singletons.ts -> src/lang/codeManager.ts | ||||
|   7) src/lib/singletons.ts -> src/clientSideScene/sceneEntities.ts -> src/clientSideScene/segments.ts -> src/components/Toolbar/angleLengthInfo.ts | ||||
|   8) src/lib/singletons.ts -> src/clientSideScene/sceneEntities.ts -> src/clientSideScene/segments.ts -> src/machines/commandBarMachine.ts -> src/lib/commandBarConfigs/authCommandConfig.ts | ||||
|   9) src/hooks/useModelingContext.ts -> src/components/ModelingMachineProvider.tsx -> src/components/Toolbar/Intersect.tsx -> src/components/SetHorVertDistanceModal.tsx -> src/lib/useCalculateKclExpression.ts | ||||
|   10) src/routes/Onboarding/index.tsx -> src/routes/Onboarding/Camera.tsx -> src/routes/Onboarding/utils.tsx | ||||
|  | ||||
| @ -114,6 +114,7 @@ | ||||
|     "circular-deps": "dpdm --no-warning --no-tree -T --skip-dynamic-imports=circular src/index.tsx", | ||||
|     "circular-deps:overwrite": "npm run circular-deps | sed '$d' | grep -v '^npm run' > known-circular.txt", | ||||
|     "circular-deps:diff": "./scripts/diff-circular-deps.sh", | ||||
|     "circular-deps:diff:nodejs": "npm run circular-deps:diff || node ./scripts/diff.js", | ||||
|     "files:set-version": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json", | ||||
|     "files:set-notes": "./scripts/set-files-notes.sh", | ||||
|     "files:flip-to-nightly": "./scripts/flip-files-to-nightly.sh", | ||||
|  | ||||
							
								
								
									
										59
									
								
								scripts/diff.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								scripts/diff.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,59 @@ | ||||
| const fs = require('fs') | ||||
| const latestRun = fs.readFileSync('/tmp/circular-deps.txt','utf-8') | ||||
| const knownCircular = fs.readFileSync('./known-circular.txt','utf-8') | ||||
|  | ||||
| function parseLine (line) { | ||||
|   let num = null | ||||
|   let depPath = null | ||||
|   const res = line.split(")",2) | ||||
|   if (res.length === 2) { | ||||
|     // should be a dep line | ||||
|     num = parseInt(res[0]) | ||||
|     depPath = res[1] | ||||
|   } | ||||
|   return { | ||||
|     num, | ||||
|     depPath | ||||
|   } | ||||
| } | ||||
|  | ||||
| function makeDependencyHash (file) { | ||||
|   const deps = {} | ||||
|   file.split("\n").forEach((line)=>{ | ||||
|     const {num, depPath} = parseLine(line) | ||||
|     if (depPath && !isNaN(num)) { | ||||
|       deps[depPath] = 1 | ||||
|     } | ||||
|   }) | ||||
|   return deps | ||||
| } | ||||
|  | ||||
| const latestRunDepHash = makeDependencyHash(latestRun) | ||||
| const knownDepHash = makeDependencyHash(knownCircular) | ||||
|  | ||||
| const dup1 = JSON.parse(JSON.stringify(latestRunDepHash)) | ||||
| const dup2 = JSON.parse(JSON.stringify(knownDepHash)) | ||||
| Object.keys(knownDepHash).forEach((key)=>{ | ||||
|   delete dup1[key] | ||||
| }) | ||||
|  | ||||
| Object.keys(latestRunDepHash).forEach((key)=>{ | ||||
|   delete dup2[key] | ||||
| }) | ||||
|  | ||||
| console.log(" ") | ||||
| console.log("diff.js - line item diff") | ||||
| console.log(" ") | ||||
| console.log("Added(+)") | ||||
| Object.keys(dup1).forEach((dep, index)=>{ | ||||
|   console.log(`${index+1}) ${dep}`) | ||||
| }) | ||||
|  | ||||
| console.log(" ") | ||||
| console.log("Removed(-)") | ||||
| if (Object.keys(dup2).length === 0) { | ||||
|   console.log("None") | ||||
| } | ||||
| Object.keys(dup2).forEach((dep, index)=>{ | ||||
|   console.log(`${index+1}) ${dep}`) | ||||
| }) | ||||
| @ -35,11 +35,7 @@ import { | ||||
| } from '@src/lib/singletons' | ||||
| import { maybeWriteToDisk } from '@src/lib/telemetry' | ||||
| import { type IndexLoaderData } from '@src/lib/types' | ||||
| import { | ||||
|   engineStreamActor, | ||||
|   useSettings, | ||||
|   useToken, | ||||
| } from '@src/machines/appMachine' | ||||
| import { engineStreamActor, useSettings, useToken } from '@src/lib/singletons' | ||||
| import { commandBarActor } from '@src/machines/commandBarMachine' | ||||
| import { EngineStreamTransition } from '@src/machines/engineStreamMachine' | ||||
| import { onboardingPaths } from '@src/routes/Onboarding/paths' | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| import Loading from '@src/components/Loading' | ||||
| import { useAuthState } from '@src/machines/appMachine' | ||||
| import { useAuthState } from '@src/lib/singletons' | ||||
|  | ||||
| // Wrapper around protected routes, used in src/Router.tsx | ||||
| export const Auth = ({ children }: React.PropsWithChildren) => { | ||||
|  | ||||
							
								
								
									
										35
									
								
								src/Root.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								src/Root.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,35 @@ | ||||
| import { AppStateProvider } from '@src/AppState' | ||||
| import LspProvider from '@src/components/LspProvider' | ||||
| import { MachineManagerProvider } from '@src/components/MachineManagerProvider' | ||||
| import { OpenInDesktopAppHandler } from '@src/components/OpenInDesktopAppHandler' | ||||
| import { SystemIOMachineLogicListenerDesktop } from '@src/components/Providers/SystemIOProviderDesktop' | ||||
| import { SystemIOMachineLogicListenerWeb } from '@src/components/Providers/SystemIOProviderWeb' | ||||
| import { RouteProvider } from '@src/components/RouteProvider' | ||||
| import { KclContextProvider } from '@src/lang/KclProvider' | ||||
| import { Outlet } from 'react-router-dom' | ||||
| import { isDesktop } from '@src/lib/isDesktop' | ||||
| // Root component will live for the entire applications runtime | ||||
| function RootLayout() { | ||||
|   return ( | ||||
|     <OpenInDesktopAppHandler> | ||||
|       <RouteProvider> | ||||
|         <LspProvider> | ||||
|           <KclContextProvider> | ||||
|             <AppStateProvider> | ||||
|               <MachineManagerProvider> | ||||
|                 {isDesktop() ? ( | ||||
|                   <SystemIOMachineLogicListenerDesktop /> | ||||
|                 ) : ( | ||||
|                   <SystemIOMachineLogicListenerWeb /> | ||||
|                 )} | ||||
|                 <Outlet /> | ||||
|               </MachineManagerProvider> | ||||
|             </AppStateProvider> | ||||
|           </KclContextProvider> | ||||
|         </LspProvider> | ||||
|       </RouteProvider> | ||||
|     </OpenInDesktopAppHandler> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| export default RootLayout | ||||
| @ -9,22 +9,15 @@ import { | ||||
| } from 'react-router-dom' | ||||
|  | ||||
| import { App } from '@src/App' | ||||
| import { AppStateProvider } from '@src/AppState' | ||||
| import { Auth } from '@src/Auth' | ||||
| import { CommandBar } from '@src/components/CommandBar/CommandBar' | ||||
| import DownloadAppBanner from '@src/components/DownloadAppBanner' | ||||
| import { ErrorPage } from '@src/components/ErrorPage' | ||||
| import FileMachineProvider from '@src/components/FileMachineProvider' | ||||
| import LspProvider from '@src/components/LspProvider' | ||||
| import { MachineManagerProvider } from '@src/components/MachineManagerProvider' | ||||
| import ModelingMachineProvider from '@src/components/ModelingMachineProvider' | ||||
| import { OpenInDesktopAppHandler } from '@src/components/OpenInDesktopAppHandler' | ||||
| import { ProjectsContextProvider } from '@src/components/ProjectsContextProvider' | ||||
| import { RouteProvider } from '@src/components/RouteProvider' | ||||
| import { WasmErrBanner } from '@src/components/WasmErrBanner' | ||||
| import { NetworkContext } from '@src/hooks/useNetworkContext' | ||||
| import { useNetworkStatus } from '@src/hooks/useNetworkStatus' | ||||
| import { KclContextProvider } from '@src/lang/KclProvider' | ||||
| import { coreDump } from '@src/lang/wasm' | ||||
| import { | ||||
|   ASK_TO_OPEN_QUERY_PARAM, | ||||
| @ -42,7 +35,8 @@ import { | ||||
|   rustContext, | ||||
| } from '@src/lib/singletons' | ||||
| import { reportRejection } from '@src/lib/trap' | ||||
| import { useToken } from '@src/machines/appMachine' | ||||
| import { useToken } from '@src/lib/singletons' | ||||
| import RootLayout from '@src/Root' | ||||
| import Home from '@src/routes/Home' | ||||
| import Onboarding, { onboardingRoutes } from '@src/routes/Onboarding' | ||||
| import { Settings } from '@src/routes/Settings' | ||||
| @ -54,27 +48,13 @@ const createRouter = isDesktop() ? createHashRouter : createBrowserRouter | ||||
| const router = createRouter([ | ||||
|   { | ||||
|     id: PATHS.INDEX, | ||||
|     element: ( | ||||
|       <OpenInDesktopAppHandler> | ||||
|         <RouteProvider> | ||||
|           <LspProvider> | ||||
|             <ProjectsContextProvider> | ||||
|               <KclContextProvider> | ||||
|                 <AppStateProvider> | ||||
|                   <MachineManagerProvider> | ||||
|                     <Outlet /> | ||||
|                   </MachineManagerProvider> | ||||
|                 </AppStateProvider> | ||||
|               </KclContextProvider> | ||||
|             </ProjectsContextProvider> | ||||
|           </LspProvider> | ||||
|         </RouteProvider> | ||||
|       </OpenInDesktopAppHandler> | ||||
|     ), | ||||
|     errorElement: <ErrorPage />, | ||||
|     element: <RootLayout />, | ||||
|     // Gotcha: declaring errorElement on the root will unmount the element causing our forever React components to unmount. | ||||
|     // Leave errorElement on the child components, this allows for the entire react context on error pages as well. | ||||
|     children: [ | ||||
|       { | ||||
|         path: PATHS.INDEX, | ||||
|         errorElement: <ErrorPage />, | ||||
|         loader: async ({ request }) => { | ||||
|           const onDesktop = isDesktop() | ||||
|           const url = new URL(request.url) | ||||
| @ -95,6 +75,7 @@ const router = createRouter([ | ||||
|         loader: fileLoader, | ||||
|         id: PATHS.FILE, | ||||
|         path: PATHS.FILE + '/:id', | ||||
|         errorElement: <ErrorPage />, | ||||
|         element: ( | ||||
|           <Auth> | ||||
|             <FileMachineProvider> | ||||
| @ -141,6 +122,7 @@ const router = createRouter([ | ||||
|       }, | ||||
|       { | ||||
|         path: PATHS.HOME, | ||||
|         errorElement: <ErrorPage />, | ||||
|         element: ( | ||||
|           <Auth> | ||||
|             <Outlet /> | ||||
| @ -169,6 +151,7 @@ const router = createRouter([ | ||||
|       }, | ||||
|       { | ||||
|         path: PATHS.SIGN_IN, | ||||
|         errorElement: <ErrorPage />, | ||||
|         element: <SignIn />, | ||||
|       }, | ||||
|     ], | ||||
|  | ||||
| @ -40,7 +40,7 @@ import { | ||||
| } from '@src/lib/singletons' | ||||
| import { err, reportRejection, trap } from '@src/lib/trap' | ||||
| import { throttle, toSync } from '@src/lib/utils' | ||||
| import type { useSettings } from '@src/machines/appMachine' | ||||
| import type { useSettings } from '@src/lib/singletons' | ||||
| import { commandBarActor } from '@src/machines/commandBarMachine' | ||||
| import type { SegmentOverlay } from '@src/machines/modelingMachine' | ||||
|  | ||||
|  | ||||
| @ -5,7 +5,7 @@ import { RefreshButton } from '@src/components/RefreshButton' | ||||
| import UserSidebarMenu from '@src/components/UserSidebarMenu' | ||||
| import { isDesktop } from '@src/lib/isDesktop' | ||||
| import { type IndexLoaderData } from '@src/lib/types' | ||||
| import { useUser } from '@src/machines/appMachine' | ||||
| import { useUser } from '@src/lib/singletons' | ||||
|  | ||||
| import styles from './AppHeader.module.css' | ||||
|  | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| import { Switch } from '@headlessui/react' | ||||
| import { useEffect, useState } from 'react' | ||||
|  | ||||
| import { settingsActor, useSettings } from '@src/machines/appMachine' | ||||
| import { settingsActor, useSettings } from '@src/lib/singletons' | ||||
|  | ||||
| export function CameraProjectionToggle() { | ||||
|   const settings = useSettings() | ||||
|  | ||||
| @ -29,7 +29,7 @@ import { err } from '@src/lib/trap' | ||||
| import { useCalculateKclExpression } from '@src/lib/useCalculateKclExpression' | ||||
| import { roundOff } from '@src/lib/utils' | ||||
| import { varMentions } from '@src/lib/varCompletionExtension' | ||||
| import { useSettings } from '@src/machines/appMachine' | ||||
| import { useSettings } from '@src/lib/singletons' | ||||
| import { | ||||
|   commandBarActor, | ||||
|   useCommandBarState, | ||||
|  | ||||
| @ -3,7 +3,7 @@ import { useState } from 'react' | ||||
|  | ||||
| import { ActionButton } from '@src/components/ActionButton' | ||||
| import { CREATE_FILE_URL_PARAM } from '@src/lib/constants' | ||||
| import { useSettings } from '@src/machines/appMachine' | ||||
| import { useSettings } from '@src/lib/singletons' | ||||
| import { useSearchParams } from 'react-router-dom' | ||||
|  | ||||
| const DownloadAppBanner = () => { | ||||
|  | ||||
| @ -18,7 +18,7 @@ import { REASONABLE_TIME_TO_REFRESH_STREAM_SIZE } from '@src/lib/timings' | ||||
| import { err, reportRejection, trap } from '@src/lib/trap' | ||||
| import type { IndexLoaderData } from '@src/lib/types' | ||||
| import { uuidv4 } from '@src/lib/utils' | ||||
| import { engineStreamActor, useSettings } from '@src/machines/appMachine' | ||||
| import { engineStreamActor, useSettings } from '@src/lib/singletons' | ||||
| import { useCommandBarState } from '@src/machines/commandBarMachine' | ||||
| import { | ||||
|   EngineStreamState, | ||||
|  | ||||
| @ -33,7 +33,7 @@ import { markOnce } from '@src/lib/performance' | ||||
| import { codeManager, kclManager } from '@src/lib/singletons' | ||||
| import { err, reportRejection } from '@src/lib/trap' | ||||
| import { type IndexLoaderData } from '@src/lib/types' | ||||
| import { useSettings, useToken } from '@src/machines/appMachine' | ||||
| import { useSettings, useToken } from '@src/lib/singletons' | ||||
| import { commandBarActor } from '@src/machines/commandBarMachine' | ||||
| import { fileMachine } from '@src/machines/fileMachine' | ||||
| import { modelingMenuCallbackMostActions } from '@src/menu/register' | ||||
|  | ||||
| @ -27,7 +27,7 @@ import { useModelingContext } from '@src/hooks/useModelingContext' | ||||
| import { AxisNames } from '@src/lib/constants' | ||||
| import { sceneInfra } from '@src/lib/singletons' | ||||
| import { reportRejection } from '@src/lib/trap' | ||||
| import { useSettings } from '@src/machines/appMachine' | ||||
| import { useSettings } from '@src/lib/singletons' | ||||
|  | ||||
| const CANVAS_SIZE = 80 | ||||
| const FRUSTUM_SIZE = 0.5 | ||||
|  | ||||
| @ -10,7 +10,7 @@ import { createAndOpenNewTutorialProject } from '@src/lib/desktopFS' | ||||
| import { openExternalBrowserIfDesktop } from '@src/lib/openWindow' | ||||
| import { PATHS } from '@src/lib/paths' | ||||
| import { reportRejection } from '@src/lib/trap' | ||||
| import { settingsActor } from '@src/machines/appMachine' | ||||
| import { settingsActor } from '@src/lib/singletons' | ||||
| import type { WebContentSendPayload } from '@src/menu/channels' | ||||
|  | ||||
| const HelpMenuDivider = () => ( | ||||
|  | ||||
| @ -27,7 +27,7 @@ import { PATHS } from '@src/lib/paths' | ||||
| import type { FileEntry } from '@src/lib/project' | ||||
| import { codeManager } from '@src/lib/singletons' | ||||
| import { err } from '@src/lib/trap' | ||||
| import { useToken } from '@src/machines/appMachine' | ||||
| import { useToken } from '@src/lib/singletons' | ||||
|  | ||||
| function getWorkspaceFolders(): LSP.WorkspaceFolder[] { | ||||
|   return [] | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| import { engineStreamActor } from '@src/machines/appMachine' | ||||
| import { engineStreamActor } from '@src/lib/singletons' | ||||
| import { EngineStreamState } from '@src/machines/engineStreamMachine' | ||||
| import { useSelector } from '@xstate/react' | ||||
|  | ||||
|  | ||||
| @ -110,7 +110,7 @@ import { submitAndAwaitTextToKcl } from '@src/lib/textToCad' | ||||
| import { err, reject, reportRejection, trap } from '@src/lib/trap' | ||||
| import type { IndexLoaderData } from '@src/lib/types' | ||||
| import { platform, uuidv4 } from '@src/lib/utils' | ||||
| import { useSettings, useToken } from '@src/machines/appMachine' | ||||
| import { useSettings, useToken } from '@src/lib/singletons' | ||||
| import { commandBarActor } from '@src/machines/commandBarMachine' | ||||
| import { kclEditorActor } from '@src/machines/kclEditorMachine' | ||||
| import { | ||||
|  | ||||
| @ -5,7 +5,7 @@ import { ActionButton } from '@src/components/ActionButton' | ||||
| import { ActionIcon } from '@src/components/ActionIcon' | ||||
| import type { CustomIconName } from '@src/components/CustomIcon' | ||||
| import Tooltip from '@src/components/Tooltip' | ||||
| import { useSettings } from '@src/machines/appMachine' | ||||
| import { useSettings } from '@src/lib/singletons' | ||||
| import { onboardingPaths } from '@src/routes/Onboarding/paths' | ||||
|  | ||||
| import styles from './ModelingPane.module.css' | ||||
|  | ||||
| @ -47,7 +47,7 @@ import { codeManagerHistoryCompartment } from '@src/lang/codeManager' | ||||
| import { codeManager, editorManager, kclManager } from '@src/lib/singletons' | ||||
| import { Themes, getSystemTheme } from '@src/lib/theme' | ||||
| import { onMouseDragMakeANewNumber, onMouseDragRegex } from '@src/lib/utils' | ||||
| import { useSettings } from '@src/machines/appMachine' | ||||
| import { useSettings } from '@src/lib/singletons' | ||||
| import { | ||||
|   editorIsMountedSelector, | ||||
|   kclEditorActor, | ||||
|  | ||||
| @ -22,7 +22,7 @@ import { useKclContext } from '@src/lang/KclProvider' | ||||
| import { EngineConnectionStateType } from '@src/lang/std/engineConnection' | ||||
| import { SIDEBAR_BUTTON_SUFFIX } from '@src/lib/constants' | ||||
| import { isDesktop } from '@src/lib/isDesktop' | ||||
| import { useSettings } from '@src/machines/appMachine' | ||||
| import { useSettings } from '@src/lib/singletons' | ||||
| import { commandBarActor } from '@src/machines/commandBarMachine' | ||||
| import { onboardingPaths } from '@src/routes/Onboarding/paths' | ||||
|  | ||||
|  | ||||
| @ -23,7 +23,7 @@ import { | ||||
|   kclManager, | ||||
| } from '@src/lib/singletons' | ||||
| import { type IndexLoaderData } from '@src/lib/types' | ||||
| import { useToken } from '@src/machines/appMachine' | ||||
| import { useToken } from '@src/lib/singletons' | ||||
| import { commandBarActor } from '@src/machines/commandBarMachine' | ||||
|  | ||||
| const ProjectSidebarMenu = ({ | ||||
|  | ||||
| @ -1,492 +0,0 @@ | ||||
| import { useMachine } from '@xstate/react' | ||||
| import { createContext, useCallback, useEffect, useState } from 'react' | ||||
| import toast from 'react-hot-toast' | ||||
| import { useLocation, useNavigate, useSearchParams } from 'react-router-dom' | ||||
| import type { Actor, AnyStateMachine, Prop, StateFrom } from 'xstate' | ||||
| import { fromPromise } from 'xstate' | ||||
|  | ||||
| import { useLspContext } from '@src/components/LspProvider' | ||||
| import { useFileSystemWatcher } from '@src/hooks/useFileSystemWatcher' | ||||
| import { useProjectsLoader } from '@src/hooks/useProjectsLoader' | ||||
| import useStateMachineCommands from '@src/hooks/useStateMachineCommands' | ||||
| import { newKclFile } from '@src/lang/project' | ||||
| import { projectsCommandBarConfig } from '@src/lib/commandBarConfigs/projectsCommandConfig' | ||||
| import { | ||||
|   CREATE_FILE_URL_PARAM, | ||||
|   FILE_EXT, | ||||
|   PROJECT_ENTRYPOINT, | ||||
| } from '@src/lib/constants' | ||||
| import { | ||||
|   createNewProjectDirectory, | ||||
|   listProjects, | ||||
|   renameProjectDirectory, | ||||
| } from '@src/lib/desktop' | ||||
| import { | ||||
|   doesProjectNameNeedInterpolated, | ||||
|   getNextFileName, | ||||
|   getNextProjectIndex, | ||||
|   getUniqueProjectName, | ||||
|   interpolateProjectNameWithIndex, | ||||
| } from '@src/lib/desktopFS' | ||||
| import { isDesktop } from '@src/lib/isDesktop' | ||||
| import { PATHS } from '@src/lib/paths' | ||||
| import type { Project } from '@src/lib/project' | ||||
| import { codeManager, kclManager } from '@src/lib/singletons' | ||||
| import { err } from '@src/lib/trap' | ||||
| import { useSettings } from '@src/machines/appMachine' | ||||
| import { commandBarActor } from '@src/machines/commandBarMachine' | ||||
| import { projectsMachine } from '@src/machines/projectsMachine' | ||||
|  | ||||
| type MachineContext<T extends AnyStateMachine> = { | ||||
|   state?: StateFrom<T> | ||||
|   send: Prop<Actor<T>, 'send'> | ||||
| } | ||||
|  | ||||
| export const ProjectsMachineContext = createContext( | ||||
|   {} as MachineContext<typeof projectsMachine> | ||||
| ) | ||||
|  | ||||
| /** | ||||
|  * Watches the project directory and provides project management-related commands, | ||||
|  * like "Create project", "Open project", "Delete project", etc. | ||||
|  * | ||||
|  * If in the future we implement full-fledge project management in the web version, | ||||
|  * we can unify these components but for now, we need this to be only for the desktop version. | ||||
|  */ | ||||
| export const ProjectsContextProvider = ({ | ||||
|   children, | ||||
| }: { | ||||
|   children: React.ReactNode | ||||
| }) => { | ||||
|   return isDesktop() ? ( | ||||
|     <ProjectsContextDesktop>{children}</ProjectsContextDesktop> | ||||
|   ) : ( | ||||
|     <ProjectsContextWeb>{children}</ProjectsContextWeb> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * We need some of the functionality of the ProjectsContextProvider in the web version | ||||
|  * but we can't perform file system operations in the browser, | ||||
|  * so most of the behavior of this machine is stubbed out. | ||||
|  */ | ||||
| const ProjectsContextWeb = ({ children }: { children: React.ReactNode }) => { | ||||
|   const [searchParams, setSearchParams] = useSearchParams() | ||||
|   const clearImportSearchParams = useCallback(() => { | ||||
|     // Clear the search parameters related to the "Import file from URL" command | ||||
|     // or we'll never be able cancel or submit it. | ||||
|     searchParams.delete(CREATE_FILE_URL_PARAM) | ||||
|     searchParams.delete('code') | ||||
|     searchParams.delete('name') | ||||
|     searchParams.delete('units') | ||||
|     setSearchParams(searchParams) | ||||
|   }, [searchParams, setSearchParams]) | ||||
|   const settings = useSettings() | ||||
|  | ||||
|   const [state, send, actor] = useMachine( | ||||
|     projectsMachine.provide({ | ||||
|       actions: { | ||||
|         navigateToProject: () => {}, | ||||
|         navigateToProjectIfNeeded: () => {}, | ||||
|         navigateToFile: () => {}, | ||||
|         toastSuccess: ({ event }) => | ||||
|           toast.success( | ||||
|             ('data' in event && typeof event.data === 'string' && event.data) || | ||||
|               ('output' in event && | ||||
|                 'message' in event.output && | ||||
|                 typeof event.output.message === 'string' && | ||||
|                 event.output.message) || | ||||
|               '' | ||||
|           ), | ||||
|         toastError: ({ event }) => | ||||
|           toast.error( | ||||
|             ('data' in event && typeof event.data === 'string' && event.data) || | ||||
|               ('output' in event && | ||||
|                 typeof event.output === 'string' && | ||||
|                 event.output) || | ||||
|               '' | ||||
|           ), | ||||
|       }, | ||||
|       actors: { | ||||
|         readProjects: fromPromise(async () => [] as Project[]), | ||||
|         createProject: fromPromise(async () => ({ | ||||
|           message: 'not implemented on web', | ||||
|         })), | ||||
|         renameProject: fromPromise(async () => ({ | ||||
|           message: 'not implemented on web', | ||||
|           oldName: '', | ||||
|           newName: '', | ||||
|         })), | ||||
|         deleteProject: fromPromise(async () => ({ | ||||
|           message: 'not implemented on web', | ||||
|           name: '', | ||||
|         })), | ||||
|         createFile: fromPromise(async ({ input }) => { | ||||
|           // Browser version doesn't navigate, just overwrites the current file | ||||
|           clearImportSearchParams() | ||||
|  | ||||
|           const codeToWrite = newKclFile( | ||||
|             input.code, | ||||
|             settings.modeling.defaultUnit.current | ||||
|           ) | ||||
|           if (err(codeToWrite)) return Promise.reject(codeToWrite) | ||||
|           codeManager.updateCodeStateEditor(codeToWrite) | ||||
|           await codeManager.writeToFile() | ||||
|           await kclManager.executeCode() | ||||
|  | ||||
|           return { | ||||
|             message: 'File overwritten successfully', | ||||
|             fileName: input.name, | ||||
|             projectName: '', | ||||
|           } | ||||
|         }), | ||||
|       }, | ||||
|     }), | ||||
|     { | ||||
|       input: { | ||||
|         projects: [], | ||||
|         defaultProjectName: settings.projects.defaultProjectName.current, | ||||
|         defaultDirectory: settings.app.projectDirectory.current, | ||||
|         hasListedProjects: false, | ||||
|       }, | ||||
|     } | ||||
|   ) | ||||
|  | ||||
|   // register all project-related command palette commands | ||||
|   useStateMachineCommands({ | ||||
|     machineId: 'projects', | ||||
|     send, | ||||
|     state, | ||||
|     commandBarConfig: projectsCommandBarConfig, | ||||
|     actor, | ||||
|     onCancel: clearImportSearchParams, | ||||
|   }) | ||||
|  | ||||
|   return ( | ||||
|     <ProjectsMachineContext.Provider | ||||
|       value={{ | ||||
|         state, | ||||
|         send, | ||||
|       }} | ||||
|     > | ||||
|       {children} | ||||
|     </ProjectsMachineContext.Provider> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| const ProjectsContextDesktop = ({ | ||||
|   children, | ||||
| }: { | ||||
|   children: React.ReactNode | ||||
| }) => { | ||||
|   const navigate = useNavigate() | ||||
|   const location = useLocation() | ||||
|   const [searchParams, setSearchParams] = useSearchParams() | ||||
|   const clearImportSearchParams = useCallback(() => { | ||||
|     // Clear the search parameters related to the "Import file from URL" command | ||||
|     // or we'll never be able cancel or submit it. | ||||
|     searchParams.delete(CREATE_FILE_URL_PARAM) | ||||
|     searchParams.delete('code') | ||||
|     searchParams.delete('name') | ||||
|     searchParams.delete('units') | ||||
|     setSearchParams(searchParams) | ||||
|   }, [searchParams, setSearchParams]) | ||||
|   const { onProjectOpen } = useLspContext() | ||||
|   const settings = useSettings() | ||||
|   const [projectsLoaderTrigger, setProjectsLoaderTrigger] = useState(0) | ||||
|   const { projectPaths, projectsDir } = useProjectsLoader([ | ||||
|     projectsLoaderTrigger, | ||||
|   ]) | ||||
|  | ||||
|   const [state, send, actor] = useMachine( | ||||
|     projectsMachine.provide({ | ||||
|       actions: { | ||||
|         navigateToProject: ({ context, event }) => { | ||||
|           const nameFromEventData = | ||||
|             'data' in event && | ||||
|             event.data && | ||||
|             'name' in event.data && | ||||
|             event.data.name | ||||
|           const nameFromOutputData = | ||||
|             'output' in event && | ||||
|             event.output && | ||||
|             'name' in event.output && | ||||
|             event.output.name | ||||
|  | ||||
|           const name = nameFromEventData || nameFromOutputData | ||||
|  | ||||
|           if (name) { | ||||
|             let projectPath = | ||||
|               context.defaultDirectory + window.electron.path.sep + name | ||||
|             onProjectOpen( | ||||
|               { | ||||
|                 name, | ||||
|                 path: projectPath, | ||||
|               }, | ||||
|               null | ||||
|             ) | ||||
|             commandBarActor.send({ type: 'Close' }) | ||||
|             const newPathName = `${PATHS.FILE}/${encodeURIComponent( | ||||
|               projectPath | ||||
|             )}` | ||||
|             navigate(newPathName) | ||||
|           } | ||||
|         }, | ||||
|         navigateToProjectIfNeeded: ({ event }) => { | ||||
|           if ( | ||||
|             event.type.startsWith('xstate.done.actor.') && | ||||
|             'output' in event | ||||
|           ) { | ||||
|             const isInAProject = location.pathname.startsWith(PATHS.FILE) | ||||
|             const isInDeletedProject = | ||||
|               event.type === 'xstate.done.actor.delete-project' && | ||||
|               isInAProject && | ||||
|               decodeURIComponent(location.pathname).includes(event.output.name) | ||||
|             if (isInDeletedProject) { | ||||
|               navigate(PATHS.HOME) | ||||
|               return | ||||
|             } | ||||
|  | ||||
|             const isInRenamedProject = | ||||
|               event.type === 'xstate.done.actor.rename-project' && | ||||
|               isInAProject && | ||||
|               decodeURIComponent(location.pathname).includes( | ||||
|                 event.output.oldName | ||||
|               ) | ||||
|  | ||||
|             if (isInRenamedProject) { | ||||
|               // TODO: In future, we can navigate to the new project path | ||||
|               // directly, but we need to coordinate with | ||||
|               // @lf94's useFileSystemWatcher in SettingsAuthProvider.tsx:224 | ||||
|               // Because it's beating us to the punch and updating the route | ||||
|               // const newPathName = location.pathname.replace( | ||||
|               //   encodeURIComponent(event.output.oldName), | ||||
|               //   encodeURIComponent(event.output.newName) | ||||
|               // ) | ||||
|               // navigate(newPathName) | ||||
|               return | ||||
|             } | ||||
|           } | ||||
|         }, | ||||
|         navigateToFile: ({ context, event }) => { | ||||
|           if (event.type !== 'xstate.done.actor.create-file') return | ||||
|           // For now, the browser version of create-file doesn't need to navigate | ||||
|           // since it just overwrites the current file. | ||||
|           if (!isDesktop()) return | ||||
|           let projectPath = window.electron.join( | ||||
|             context.defaultDirectory, | ||||
|             event.output.projectName | ||||
|           ) | ||||
|           let filePath = window.electron.join( | ||||
|             projectPath, | ||||
|             event.output.fileName | ||||
|           ) | ||||
|           onProjectOpen( | ||||
|             { | ||||
|               name: event.output.projectName, | ||||
|               path: projectPath, | ||||
|             }, | ||||
|             null | ||||
|           ) | ||||
|           const pathToNavigateTo = `${PATHS.FILE}/${encodeURIComponent( | ||||
|             filePath | ||||
|           )}` | ||||
|           navigate(pathToNavigateTo) | ||||
|         }, | ||||
|         toastSuccess: ({ event }) => | ||||
|           toast.success( | ||||
|             ('data' in event && typeof event.data === 'string' && event.data) || | ||||
|               ('output' in event && | ||||
|                 'message' in event.output && | ||||
|                 typeof event.output.message === 'string' && | ||||
|                 event.output.message) || | ||||
|               '' | ||||
|           ), | ||||
|         toastError: ({ event }) => | ||||
|           toast.error( | ||||
|             ('data' in event && typeof event.data === 'string' && event.data) || | ||||
|               ('output' in event && | ||||
|                 typeof event.output === 'string' && | ||||
|                 event.output) || | ||||
|               ('error' in event && | ||||
|                 event.error instanceof Error && | ||||
|                 event.error.message) || | ||||
|               '' | ||||
|           ), | ||||
|       }, | ||||
|       actors: { | ||||
|         readProjects: fromPromise(() => { | ||||
|           return listProjects() | ||||
|         }), | ||||
|         createProject: fromPromise(async ({ input }) => { | ||||
|           let name = ( | ||||
|             input && 'name' in input && input.name | ||||
|               ? input.name | ||||
|               : settings.projects.defaultProjectName.current | ||||
|           ).trim() | ||||
|  | ||||
|           const uniqueName = getUniqueProjectName(name, input.projects) | ||||
|           await createNewProjectDirectory(uniqueName) | ||||
|  | ||||
|           return { | ||||
|             message: `Successfully created "${uniqueName}"`, | ||||
|             name: uniqueName, | ||||
|           } | ||||
|         }), | ||||
|         renameProject: fromPromise(async ({ input }) => { | ||||
|           const { | ||||
|             oldName, | ||||
|             newName, | ||||
|             defaultProjectName, | ||||
|             defaultDirectory, | ||||
|             projects, | ||||
|           } = input | ||||
|           let name = newName ? newName : defaultProjectName | ||||
|           if (doesProjectNameNeedInterpolated(name)) { | ||||
|             const nextIndex = getNextProjectIndex(name, projects) | ||||
|             name = interpolateProjectNameWithIndex(name, nextIndex) | ||||
|           } | ||||
|  | ||||
|           // Toast an error if the project name is taken | ||||
|           if (projects.find((p) => p.name === name)) { | ||||
|             return Promise.reject( | ||||
|               new Error(`Project with name "${name}" already exists`) | ||||
|             ) | ||||
|           } | ||||
|  | ||||
|           await renameProjectDirectory( | ||||
|             window.electron.path.join(defaultDirectory, oldName), | ||||
|             name | ||||
|           ) | ||||
|           return { | ||||
|             message: `Successfully renamed "${oldName}" to "${name}"`, | ||||
|             oldName: oldName, | ||||
|             newName: name, | ||||
|           } | ||||
|         }), | ||||
|         deleteProject: fromPromise(async ({ input }) => { | ||||
|           await window.electron.rm( | ||||
|             window.electron.path.join(input.defaultDirectory, input.name), | ||||
|             { | ||||
|               recursive: true, | ||||
|             } | ||||
|           ) | ||||
|           return { | ||||
|             message: `Successfully deleted "${input.name}"`, | ||||
|             name: input.name, | ||||
|           } | ||||
|         }), | ||||
|         createFile: fromPromise(async ({ input }) => { | ||||
|           let projectName = | ||||
|             (input.method === 'newProject' ? input.name : input.projectName) || | ||||
|             settings.projects.defaultProjectName.current | ||||
|           let fileName = | ||||
|             input.method === 'newProject' | ||||
|               ? PROJECT_ENTRYPOINT | ||||
|               : input.name.endsWith(FILE_EXT) | ||||
|                 ? input.name | ||||
|                 : input.name + FILE_EXT | ||||
|           let message = 'File created successfully' | ||||
|  | ||||
|           const needsInterpolated = doesProjectNameNeedInterpolated(projectName) | ||||
|           if (needsInterpolated) { | ||||
|             const nextIndex = getNextProjectIndex(projectName, input.projects) | ||||
|             projectName = interpolateProjectNameWithIndex( | ||||
|               projectName, | ||||
|               nextIndex | ||||
|             ) | ||||
|           } | ||||
|  | ||||
|           // Create the project around the file if newProject | ||||
|           let fileLoaded = false | ||||
|           if (input.method === 'newProject') { | ||||
|             await createNewProjectDirectory(projectName, input.code) | ||||
|             fileLoaded = true | ||||
|             message = `Project "${projectName}" created successfully with link contents` | ||||
|           } else { | ||||
|             message = `File "${fileName}" created successfully` | ||||
|           } | ||||
|  | ||||
|           // Create the file | ||||
|           let baseDir = window.electron.join( | ||||
|             settings.app.projectDirectory.current, | ||||
|             projectName | ||||
|           ) | ||||
|           const { name, path } = getNextFileName({ | ||||
|             entryName: fileName, | ||||
|             baseDir, | ||||
|           }) | ||||
|  | ||||
|           fileName = name | ||||
|           if (!fileLoaded) { | ||||
|             const codeToWrite = newKclFile( | ||||
|               input.code, | ||||
|               settings.modeling.defaultUnit.current | ||||
|             ) | ||||
|             if (err(codeToWrite)) return Promise.reject(codeToWrite) | ||||
|             await window.electron.writeFile(path, codeToWrite) | ||||
|           } | ||||
|  | ||||
|           // TODO: Return the project's file name if one was created. | ||||
|           return { | ||||
|             message, | ||||
|             fileName, | ||||
|             projectName, | ||||
|           } | ||||
|         }), | ||||
|       }, | ||||
|     }), | ||||
|     { | ||||
|       input: { | ||||
|         projects: projectPaths, | ||||
|         defaultProjectName: settings.projects.defaultProjectName.current, | ||||
|         defaultDirectory: settings.app.projectDirectory.current, | ||||
|         hasListedProjects: false, | ||||
|       }, | ||||
|     } | ||||
|   ) | ||||
|  | ||||
|   useFileSystemWatcher( | ||||
|     async () => { | ||||
|       // Gotcha: Chokidar is buggy. It will emit addDir or add on files that did not get created. | ||||
|       // This means while the application initialize and Chokidar initializes you cannot tell if | ||||
|       // a directory or file is actually created or they are buggy signals. This means you must | ||||
|       // ignore all signals during initialization because it is ambiguous. Once those signals settle | ||||
|       // you can actually start listening to real signals. | ||||
|       // If someone creates folders or files during initialization we ignore those events! | ||||
|       if (!actor.getSnapshot().context.hasListedProjects) { | ||||
|         return | ||||
|       } | ||||
|       return setProjectsLoaderTrigger(projectsLoaderTrigger + 1) | ||||
|     }, | ||||
|     projectsDir ? [projectsDir] : [] | ||||
|   ) | ||||
|  | ||||
|   // Gotcha: Triggers listProjects() on chokidar changes | ||||
|   // Gotcha: Load the projects when the projectDirectory changes. | ||||
|   const projectDirectory = settings.app.projectDirectory.current | ||||
|   useEffect(() => { | ||||
|     send({ type: 'Read projects', data: {} }) | ||||
|   }, [projectPaths, projectDirectory]) | ||||
|  | ||||
|   // register all project-related command palette commands | ||||
|   useStateMachineCommands({ | ||||
|     machineId: 'projects', | ||||
|     send, | ||||
|     state, | ||||
|     commandBarConfig: projectsCommandBarConfig, | ||||
|     actor, | ||||
|     onCancel: clearImportSearchParams, | ||||
|   }) | ||||
|  | ||||
|   return ( | ||||
|     <ProjectsMachineContext.Provider | ||||
|       value={{ | ||||
|         state, | ||||
|         send, | ||||
|       }} | ||||
|     > | ||||
|       {children} | ||||
|     </ProjectsMachineContext.Provider> | ||||
|   ) | ||||
| } | ||||
							
								
								
									
										122
									
								
								src/components/Providers/SystemIOProviderDesktop.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										122
									
								
								src/components/Providers/SystemIOProviderDesktop.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,122 @@ | ||||
| import { useFileSystemWatcher } from '@src/hooks/useFileSystemWatcher' | ||||
| import { PATHS } from '@src/lib/paths' | ||||
| import { systemIOActor, useSettings } from '@src/lib/singletons' | ||||
| import { | ||||
|   useHasListedProjects, | ||||
|   useProjectDirectoryPath, | ||||
|   useRequestedFileName, | ||||
|   useRequestedProjectName, | ||||
| } from '@src/machines/systemIO/hooks' | ||||
| import { SystemIOMachineEvents } from '@src/machines/systemIO/utils' | ||||
| import { useNavigate } from 'react-router-dom' | ||||
| import { useEffect, useCallback } from 'react' | ||||
| import { useClearURLParams } from '@src/machines/systemIO/hooks' | ||||
| import { useSearchParams } from 'react-router-dom' | ||||
| import { CREATE_FILE_URL_PARAM } from '@src/lib/constants' | ||||
|  | ||||
| export function SystemIOMachineLogicListenerDesktop() { | ||||
|   const requestedProjectName = useRequestedProjectName() | ||||
|   const requestedFileName = useRequestedFileName() | ||||
|   const projectDirectoryPath = useProjectDirectoryPath() | ||||
|   const hasListedProjects = useHasListedProjects() | ||||
|   const navigate = useNavigate() | ||||
|   const settings = useSettings() | ||||
|   const clearURLParams = useClearURLParams() | ||||
|   const [searchParams, setSearchParams] = useSearchParams() | ||||
|   const clearImportSearchParams = useCallback(() => { | ||||
|     // Clear the search parameters related to the "Import file from URL" command | ||||
|     // or we'll never be able cancel or submit it. | ||||
|     searchParams.delete(CREATE_FILE_URL_PARAM) | ||||
|     searchParams.delete('code') | ||||
|     searchParams.delete('name') | ||||
|     searchParams.delete('units') | ||||
|     setSearchParams(searchParams) | ||||
|   }, [searchParams, setSearchParams]) | ||||
|  | ||||
|   const useGlobalProjectNavigation = () => { | ||||
|     useEffect(() => { | ||||
|       if (!requestedProjectName.name) { | ||||
|         return | ||||
|       } | ||||
|       let projectPathWithoutSpecificKCLFile = | ||||
|         projectDirectoryPath + | ||||
|         window.electron.path.sep + | ||||
|         requestedProjectName.name | ||||
|  | ||||
|       const requestedPath = `${PATHS.FILE}/${encodeURIComponent( | ||||
|         projectPathWithoutSpecificKCLFile | ||||
|       )}` | ||||
|       navigate(requestedPath) | ||||
|     }, [requestedProjectName]) | ||||
|   } | ||||
|  | ||||
|   const useGlobalFileNavigation = () => { | ||||
|     useEffect(() => { | ||||
|       console.log(requestedFileName, 'NEW!') | ||||
|       if (!requestedFileName.file || !requestedFileName.project) { | ||||
|         return | ||||
|       } | ||||
|       const projectPath = window.electron.join( | ||||
|         projectDirectoryPath, | ||||
|         requestedFileName.project | ||||
|       ) | ||||
|       const filePath = window.electron.join(projectPath, requestedFileName.file) | ||||
|       const requestedPath = `${PATHS.FILE}/${encodeURIComponent(filePath)}` | ||||
|       navigate(requestedPath) | ||||
|     }, [requestedFileName]) | ||||
|   } | ||||
|  | ||||
|   const useApplicationProjectDirectory = () => { | ||||
|     useEffect(() => { | ||||
|       systemIOActor.send({ | ||||
|         type: SystemIOMachineEvents.setProjectDirectoryPath, | ||||
|         data: { | ||||
|           requestedProjectDirectoryPath: | ||||
|             settings.app.projectDirectory.current || '', | ||||
|         }, | ||||
|       }) | ||||
|     }, [settings.app.projectDirectory.current]) | ||||
|   } | ||||
|  | ||||
|   const useDefaultProjectName = () => { | ||||
|     useEffect(() => { | ||||
|       systemIOActor.send({ | ||||
|         type: SystemIOMachineEvents.setDefaultProjectFolderName, | ||||
|         data: { | ||||
|           requestedDefaultProjectFolderName: | ||||
|             settings.projects.defaultProjectName.current || '', | ||||
|         }, | ||||
|       }) | ||||
|     }, [settings.projects.defaultProjectName.current]) | ||||
|   } | ||||
|  | ||||
|   const useWatchingApplicationProjectDirectory = () => { | ||||
|     useFileSystemWatcher( | ||||
|       async () => { | ||||
|         // Gotcha: Chokidar is buggy. It will emit addDir or add on files that did not get created. | ||||
|         // This means while the application initialize and Chokidar initializes you cannot tell if | ||||
|         // a directory or file is actually created or they are buggy signals. This means you must | ||||
|         // ignore all signals during initialization because it is ambiguous. Once those signals settle | ||||
|         // you can actually start listening to real signals. | ||||
|         // If someone creates folders or files during initialization we ignore those events! | ||||
|         if (!hasListedProjects) { | ||||
|           return | ||||
|         } | ||||
|         systemIOActor.send({ | ||||
|           type: SystemIOMachineEvents.readFoldersFromProjectDirectory, | ||||
|         }) | ||||
|       }, | ||||
|       settings.app.projectDirectory.current | ||||
|         ? [settings.app.projectDirectory.current] | ||||
|         : [] | ||||
|     ) | ||||
|   } | ||||
|  | ||||
|   useGlobalProjectNavigation() | ||||
|   useGlobalFileNavigation() | ||||
|   useApplicationProjectDirectory() | ||||
|   useDefaultProjectName() | ||||
|   useWatchingApplicationProjectDirectory() | ||||
|  | ||||
|   return null | ||||
| } | ||||
							
								
								
									
										29
									
								
								src/components/Providers/SystemIOProviderWeb.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								src/components/Providers/SystemIOProviderWeb.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,29 @@ | ||||
| import { useEffect, useCallback } from 'react' | ||||
| import { useClearURLParams } from '@src/machines/systemIO/hooks' | ||||
| import { useSearchParams } from 'react-router-dom' | ||||
| import { CREATE_FILE_URL_PARAM } from '@src/lib/constants' | ||||
|  | ||||
| export function SystemIOMachineLogicListenerWeb() { | ||||
|   const clearURLParams = useClearURLParams() | ||||
|   const [searchParams, setSearchParams] = useSearchParams() | ||||
|   const clearImportSearchParams = useCallback(() => { | ||||
|     // Clear the search parameters related to the "Import file from URL" command | ||||
|     // or we'll never be able cancel or submit it. | ||||
|     searchParams.delete(CREATE_FILE_URL_PARAM) | ||||
|     searchParams.delete('code') | ||||
|     searchParams.delete('name') | ||||
|     searchParams.delete('units') | ||||
|     setSearchParams(searchParams) | ||||
|   }, [searchParams, setSearchParams]) | ||||
|  | ||||
|   const useClearQueryParams = () => { | ||||
|     useEffect(() => { | ||||
|       if (clearURLParams.value) { | ||||
|         clearImportSearchParams() | ||||
|       } | ||||
|     }, [clearURLParams]) | ||||
|   } | ||||
|  | ||||
|   useClearQueryParams() | ||||
|   return null | ||||
| } | ||||
| @ -13,7 +13,7 @@ import { | ||||
| } from '@src/lib/singletons' | ||||
| import { reportRejection } from '@src/lib/trap' | ||||
| import { toSync } from '@src/lib/utils' | ||||
| import { useToken } from '@src/machines/appMachine' | ||||
| import { useToken } from '@src/lib/singletons' | ||||
| import type { WebContentSendPayload } from '@src/menu/channels' | ||||
|  | ||||
| export const RefreshButton = ({ children }: React.PropsWithChildren) => { | ||||
|  | ||||
| @ -18,7 +18,7 @@ import { markOnce } from '@src/lib/performance' | ||||
| import { loadAndValidateSettings } from '@src/lib/settings/settingsUtils' | ||||
| import { trap } from '@src/lib/trap' | ||||
| import type { IndexLoaderData } from '@src/lib/types' | ||||
| import { settingsActor, useSettings } from '@src/machines/appMachine' | ||||
| import { settingsActor, useSettings } from '@src/lib/singletons' | ||||
|  | ||||
| export const RouteProviderContext = createContext({}) | ||||
|  | ||||
|  | ||||
| @ -28,7 +28,7 @@ import { | ||||
| } from '@src/lib/settings/settingsUtils' | ||||
| import { reportRejection } from '@src/lib/trap' | ||||
| import { toSync } from '@src/lib/utils' | ||||
| import { settingsActor, useSettings } from '@src/machines/appMachine' | ||||
| import { settingsActor, useSettings } from '@src/lib/singletons' | ||||
| import { APP_VERSION, IS_NIGHTLY, getReleaseUrl } from '@src/routes/utils' | ||||
| import { waitFor } from 'xstate' | ||||
|  | ||||
|  | ||||
| @ -9,7 +9,7 @@ import type { | ||||
|   WildcardSetEvent, | ||||
| } from '@src/lib/settings/settingsTypes' | ||||
| import { getSettingInputType } from '@src/lib/settings/settingsUtils' | ||||
| import { settingsActor, useSettings } from '@src/machines/appMachine' | ||||
| import { settingsActor, useSettings } from '@src/lib/singletons' | ||||
|  | ||||
| interface SettingsFieldInputProps { | ||||
|   // We don't need the fancy types here, | ||||
|  | ||||
| @ -8,7 +8,7 @@ import { useNavigate } from 'react-router-dom' | ||||
| import { CustomIcon } from '@src/components/CustomIcon' | ||||
| import { interactionMap } from '@src/lib/settings/initialKeybindings' | ||||
| import type { SettingsLevel } from '@src/lib/settings/settingsTypes' | ||||
| import { useSettings } from '@src/machines/appMachine' | ||||
| import { useSettings } from '@src/lib/singletons' | ||||
|  | ||||
| type ExtendedSettingsLevel = SettingsLevel | 'keybindings' | ||||
|  | ||||
|  | ||||
| @ -3,7 +3,7 @@ import decamelize from 'decamelize' | ||||
| import type { Setting } from '@src/lib/settings/initialSettings' | ||||
| import type { SettingsLevel } from '@src/lib/settings/settingsTypes' | ||||
| import { shouldHideSetting } from '@src/lib/settings/settingsUtils' | ||||
| import { useSettings } from '@src/machines/appMachine' | ||||
| import { useSettings } from '@src/lib/singletons' | ||||
|  | ||||
| interface SettingsSectionsListProps { | ||||
|   searchParamTab: SettingsLevel | ||||
|  | ||||
| @ -11,7 +11,7 @@ import { useAbsoluteFilePath } from '@src/hooks/useAbsoluteFilePath' | ||||
| import usePlatform from '@src/hooks/usePlatform' | ||||
| import { isDesktop } from '@src/lib/isDesktop' | ||||
| import { PATHS } from '@src/lib/paths' | ||||
| import { authActor } from '@src/machines/appMachine' | ||||
| import { authActor } from '@src/lib/singletons' | ||||
|  | ||||
| type User = Models['User_type'] | ||||
|  | ||||
|  | ||||
| @ -12,7 +12,7 @@ import type { AxisNames } from '@src/lib/constants' | ||||
| import { VIEW_NAMES_SEMANTIC } from '@src/lib/constants' | ||||
| import { sceneInfra } from '@src/lib/singletons' | ||||
| import { reportRejection } from '@src/lib/trap' | ||||
| import { useSettings } from '@src/machines/appMachine' | ||||
| import { useSettings } from '@src/lib/singletons' | ||||
|  | ||||
| export function useViewControlMenuItems() { | ||||
|   const { state: modelingState, send: modelingSend } = useModelingContext() | ||||
|  | ||||
| @ -2,7 +2,7 @@ import { useEffect } from 'react' | ||||
| import { useLocation, useNavigate } from 'react-router-dom' | ||||
|  | ||||
| import { PATHS } from '@src/lib/paths' | ||||
| import { useAuthState } from '@src/machines/appMachine' | ||||
| import { useAuthState } from '@src/lib/singletons' | ||||
|  | ||||
| /** | ||||
|  * A simple hook that listens to the auth state of the app and navigates | ||||
|  | ||||
| @ -9,7 +9,7 @@ import { CREATE_FILE_URL_PARAM, DEFAULT_FILE_NAME } from '@src/lib/constants' | ||||
| import { isDesktop } from '@src/lib/isDesktop' | ||||
| import type { FileLinkParams } from '@src/lib/links' | ||||
| import { PATHS } from '@src/lib/paths' | ||||
| import { useSettings } from '@src/machines/appMachine' | ||||
| import { useSettings } from '@src/lib/singletons' | ||||
|  | ||||
| // For initializing the command arguments, we actually want `method` to be undefined | ||||
| // so that we don't skip it in the command palette. | ||||
|  | ||||
| @ -27,7 +27,7 @@ import { | ||||
| } from '@src/lib/singletons' | ||||
| import { err, reportRejection } from '@src/lib/trap' | ||||
| import { getModuleId } from '@src/lib/utils' | ||||
| import { engineStreamActor } from '@src/machines/appMachine' | ||||
| import { engineStreamActor } from '@src/lib/singletons' | ||||
| import { EngineStreamState } from '@src/machines/engineStreamMachine' | ||||
| import type { | ||||
|   EdgeCutInfo, | ||||
|  | ||||
| @ -1,7 +0,0 @@ | ||||
| import { useContext } from 'react' | ||||
|  | ||||
| import { ProjectsMachineContext } from '@src/components/ProjectsContextProvider' | ||||
|  | ||||
| export const useProjectsContext = () => { | ||||
|   return useContext(ProjectsMachineContext) | ||||
| } | ||||
| @ -1,5 +1,5 @@ | ||||
| import { Themes, getSystemTheme } from '@src/lib/theme' | ||||
| import { useSettings } from '@src/machines/appMachine' | ||||
| import { useSettings } from '@src/lib/singletons' | ||||
|  | ||||
| /** | ||||
|  * Resolves the current theme based on the theme setting | ||||
|  | ||||
| @ -14,7 +14,6 @@ import { createMachineCommand } from '@src/lib/createMachineCommand' | ||||
| import type { authMachine } from '@src/machines/authMachine' | ||||
| import { commandBarActor } from '@src/machines/commandBarMachine' | ||||
| import type { modelingMachine } from '@src/machines/modelingMachine' | ||||
| import type { projectsMachine } from '@src/machines/projectsMachine' | ||||
| import type { settingsMachine } from '@src/machines/settingsMachine' | ||||
|  | ||||
| // This might not be necessary, AnyStateMachine from xstate is working | ||||
| @ -22,7 +21,6 @@ export type AllMachines = | ||||
|   | typeof modelingMachine | ||||
|   | typeof settingsMachine | ||||
|   | typeof authMachine | ||||
|   | typeof projectsMachine | ||||
|  | ||||
| interface UseStateMachineCommandsArgs< | ||||
|   T extends AllMachines, | ||||
|  | ||||
| @ -13,7 +13,7 @@ import { initializeWindowExceptionHandler } from '@src/lib/exceptions' | ||||
| import { isDesktop } from '@src/lib/isDesktop' | ||||
| import { markOnce } from '@src/lib/performance' | ||||
| import { reportRejection } from '@src/lib/trap' | ||||
| import { appActor } from '@src/machines/appMachine' | ||||
| import { appActor } from '@src/lib/singletons' | ||||
| import reportWebVitals from '@src/reportWebVitals' | ||||
|  | ||||
| markOnce('code/willAuth') | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| import type { Command } from '@src/lib/commandTypes' | ||||
| import { authActor } from '@src/machines/appMachine' | ||||
| import { authActor } from '@src/lib/singletons' | ||||
| import { ACTOR_IDS } from '@src/machines/machineConstants' | ||||
|  | ||||
| export const authCommands: Command[] = [ | ||||
|  | ||||
| @ -12,7 +12,7 @@ import type { Command, CommandArgumentOption } from '@src/lib/commandTypes' | ||||
| import { engineCommandManager } from '@src/lib/singletons' | ||||
| import { err, reportRejection } from '@src/lib/trap' | ||||
| import { uuidv4 } from '@src/lib/utils' | ||||
| import { getSettings, settingsActor } from '@src/machines/appMachine' | ||||
| import { getSettings, settingsActor } from '@src/lib/singletons' | ||||
|  | ||||
| function isWorldCoordinateSystemType( | ||||
|   x: string | ||||
|  | ||||
| @ -1,23 +1,15 @@ | ||||
| import { CommandBarOverwriteWarning } from '@src/components/CommandBarOverwriteWarning' | ||||
| import type { StateMachineCommandSetConfig } from '@src/lib/commandTypes' | ||||
| import { isDesktop } from '@src/lib/isDesktop' | ||||
| import type { projectsMachine } from '@src/machines/projectsMachine' | ||||
| import type { Command, CommandArgumentOption } from '@src/lib/commandTypes' | ||||
|  | ||||
| import { | ||||
|   folderSnapshot, | ||||
|   defaultProjectFolderNameSnapshot, | ||||
| } from '@src/machines/systemIO/snapshotContext' | ||||
| import { systemIOActor } from '@src/lib/singletons' | ||||
| import { SystemIOMachineEvents } from '@src/machines/systemIO/utils' | ||||
|  | ||||
| export type ProjectsCommandSchema = { | ||||
|   'Read projects': Record<string, unknown> | ||||
|   'Create project': { | ||||
|     name: string | ||||
|   } | ||||
|   'Open project': { | ||||
|     name: string | ||||
|   } | ||||
|   'Delete project': { | ||||
|     name: string | ||||
|   } | ||||
|   'Rename project': { | ||||
|     oldName: string | ||||
|     newName: string | ||||
|   } | ||||
|   'Import file from URL': { | ||||
|     name: string | ||||
|     code?: string | ||||
| @ -26,147 +18,243 @@ export type ProjectsCommandSchema = { | ||||
|   } | ||||
| } | ||||
|  | ||||
| export const projectsCommandBarConfig: StateMachineCommandSetConfig< | ||||
|   typeof projectsMachine, | ||||
|   ProjectsCommandSchema | ||||
| > = { | ||||
|   'Open project': { | ||||
|     icon: 'arrowRight', | ||||
|     description: 'Open a project', | ||||
|     status: isDesktop() ? 'active' : 'inactive', | ||||
|     args: { | ||||
|       name: { | ||||
|         inputType: 'options', | ||||
|         required: true, | ||||
|         options: (_, context) => | ||||
|           context?.projects.map((p) => ({ | ||||
|             name: p.name, | ||||
|             value: p.name, | ||||
|           })) || [], | ||||
|       }, | ||||
|     }, | ||||
| export const openProjectCommand: Command = { | ||||
|   icon: 'arrowRight', | ||||
|   name: 'Open project', | ||||
|   displayName: `Open project`, | ||||
|   description: 'Open a project', | ||||
|   groupId: 'projects', | ||||
|   needsReview: false, | ||||
|   onSubmit: (record) => { | ||||
|     if (record) { | ||||
|       systemIOActor.send({ | ||||
|         type: SystemIOMachineEvents.navigateToProject, | ||||
|         data: { requestedProjectName: record.name }, | ||||
|       }) | ||||
|     } | ||||
|   }, | ||||
|   'Create project': { | ||||
|     icon: 'folderPlus', | ||||
|     description: 'Create a project', | ||||
|     status: isDesktop() ? 'active' : 'inactive', | ||||
|     args: { | ||||
|       name: { | ||||
|         inputType: 'string', | ||||
|         required: true, | ||||
|         defaultValueFromContext: (context) => context.defaultProjectName, | ||||
|   args: { | ||||
|     name: { | ||||
|       required: true, | ||||
|       inputType: 'options', | ||||
|       options: () => { | ||||
|         const folders = folderSnapshot() | ||||
|         const options: CommandArgumentOption<string>[] = [] | ||||
|         folders.forEach((folder) => { | ||||
|           options.push({ | ||||
|             name: folder.name, | ||||
|             value: folder.name, | ||||
|             isCurrent: false, | ||||
|           }) | ||||
|         }) | ||||
|         return options | ||||
|       }, | ||||
|     }, | ||||
|   }, | ||||
|   'Delete project': { | ||||
|     icon: 'close', | ||||
|     description: 'Delete a project', | ||||
|     status: isDesktop() ? 'active' : 'inactive', | ||||
|     needsReview: true, | ||||
|     reviewMessage: ({ argumentsToSubmit }) => | ||||
|       CommandBarOverwriteWarning({ | ||||
|         heading: 'Are you sure you want to delete?', | ||||
|         message: `This will permanently delete the project "${argumentsToSubmit.name}" and all its contents.`, | ||||
|       }), | ||||
|     args: { | ||||
|       name: { | ||||
|         inputType: 'options', | ||||
|         required: true, | ||||
|         options: (_, context) => | ||||
|           context?.projects.map((p) => ({ | ||||
|             name: p.name, | ||||
|             value: p.name, | ||||
|           })) || [], | ||||
|       }, | ||||
|     }, | ||||
|   }, | ||||
|   'Rename project': { | ||||
|     icon: 'folder', | ||||
|     description: 'Rename a project', | ||||
|     needsReview: true, | ||||
|     status: isDesktop() ? 'active' : 'inactive', | ||||
|     args: { | ||||
|       oldName: { | ||||
|         inputType: 'options', | ||||
|         required: true, | ||||
|         options: (_, context) => | ||||
|           context?.projects.map((p) => ({ | ||||
|             name: p.name, | ||||
|             value: p.name, | ||||
|           })) || [], | ||||
|       }, | ||||
|       newName: { | ||||
|         inputType: 'string', | ||||
|         required: true, | ||||
|         defaultValueFromContext: (context) => context.defaultProjectName, | ||||
|       }, | ||||
|     }, | ||||
|   }, | ||||
|   'Import file from URL': { | ||||
|     icon: 'file', | ||||
|     description: 'Create a file', | ||||
|     needsReview: true, | ||||
|     status: 'active', | ||||
|     args: { | ||||
|       method: { | ||||
|         inputType: 'options', | ||||
|         required: true, | ||||
|         skip: true, | ||||
|         options: isDesktop() | ||||
|           ? [ | ||||
|               { name: 'New project', value: 'newProject' }, | ||||
|               { name: 'Existing project', value: 'existingProject' }, | ||||
|             ] | ||||
|           : [{ name: 'Overwrite', value: 'existingProject' }], | ||||
|         valueSummary(value) { | ||||
|           return isDesktop() | ||||
|             ? value === 'newProject' | ||||
|               ? 'New project' | ||||
|               : 'Existing project' | ||||
|             : 'Overwrite' | ||||
|         }, | ||||
|       }, | ||||
|       // TODO: We can't get the currently-opened project to auto-populate here because | ||||
|       // it's not available on projectMachine, but lower in fileMachine. Unify these. | ||||
|       projectName: { | ||||
|         inputType: 'options', | ||||
|         required: (commandsContext) => | ||||
|           isDesktop() && | ||||
|           commandsContext.argumentsToSubmit.method === 'existingProject', | ||||
|         skip: true, | ||||
|         options: (_, context) => | ||||
|           context?.projects.map((p) => ({ | ||||
|             name: p.name, | ||||
|             value: p.name, | ||||
|           })) || [], | ||||
|       }, | ||||
|       name: { | ||||
|         inputType: 'string', | ||||
|         required: isDesktop(), | ||||
|         skip: true, | ||||
|       }, | ||||
|       code: { | ||||
|         inputType: 'text', | ||||
|         required: true, | ||||
|         skip: true, | ||||
|         valueSummary(value) { | ||||
|           const lineCount = value?.trim().split('\n').length | ||||
|           return `${lineCount} line${lineCount === 1 ? '' : 's'}` | ||||
|         }, | ||||
|       }, | ||||
|     }, | ||||
|     reviewMessage(commandBarContext) { | ||||
|       return isDesktop() | ||||
|         ? `Will add the contents from URL to a new ${ | ||||
|             commandBarContext.argumentsToSubmit.method === 'newProject' | ||||
|               ? 'project with file main.kcl' | ||||
|               : `file within the project "${commandBarContext.argumentsToSubmit.projectName}"` | ||||
|           } named "${ | ||||
|             commandBarContext.argumentsToSubmit.name | ||||
|           }", and set default units to "${ | ||||
|             commandBarContext.argumentsToSubmit.units | ||||
|           }".` | ||||
|         : `Will overwrite the contents of the current file with the contents from the URL.` | ||||
|     }, | ||||
|   }, | ||||
| } | ||||
|  | ||||
| export const createProjectCommand: Command = { | ||||
|   icon: 'folder', | ||||
|   name: 'Create project', | ||||
|   displayName: `Create project`, | ||||
|   description: 'Create a project', | ||||
|   groupId: 'projects', | ||||
|   needsReview: false, | ||||
|   onSubmit: (record) => { | ||||
|     if (record) { | ||||
|       systemIOActor.send({ | ||||
|         type: SystemIOMachineEvents.createProject, | ||||
|         data: { requestedProjectName: record.name }, | ||||
|       }) | ||||
|     } | ||||
|   }, | ||||
|   args: { | ||||
|     name: { | ||||
|       required: true, | ||||
|       inputType: 'string', | ||||
|       defaultValue: defaultProjectFolderNameSnapshot, | ||||
|     }, | ||||
|   }, | ||||
| } | ||||
|  | ||||
| export const deleteProjectCommand: Command = { | ||||
|   icon: 'folder', | ||||
|   name: 'Delete project', | ||||
|   displayName: `Delete project`, | ||||
|   description: 'Delete a project', | ||||
|   groupId: 'projects', | ||||
|   needsReview: true, | ||||
|   onSubmit: (record) => { | ||||
|     if (record) { | ||||
|       systemIOActor.send({ | ||||
|         type: SystemIOMachineEvents.deleteProject, | ||||
|         data: { requestedProjectName: record.name }, | ||||
|       }) | ||||
|     } | ||||
|   }, | ||||
|   reviewMessage: ({ argumentsToSubmit }) => | ||||
|     CommandBarOverwriteWarning({ | ||||
|       heading: 'Are you sure you want to delete?', | ||||
|       message: `This will permanently delete the project "${argumentsToSubmit.name}" and all its contents.`, | ||||
|     }), | ||||
|   args: { | ||||
|     name: { | ||||
|       inputType: 'options', | ||||
|       required: true, | ||||
|       options: () => { | ||||
|         const folders = folderSnapshot() | ||||
|         const options: CommandArgumentOption<string>[] = [] | ||||
|         folders.forEach((folder) => { | ||||
|           options.push({ | ||||
|             name: folder.name, | ||||
|             value: folder.name, | ||||
|             isCurrent: false, | ||||
|           }) | ||||
|         }) | ||||
|         return options | ||||
|       }, | ||||
|     }, | ||||
|   }, | ||||
| } | ||||
|  | ||||
| export const renameProjectCommand: Command = { | ||||
|   icon: 'folder', | ||||
|   name: 'Rename project', | ||||
|   displayName: `Rename project`, | ||||
|   description: 'Rename a project', | ||||
|   groupId: 'projects', | ||||
|   needsReview: true, | ||||
|   onSubmit: (record) => { | ||||
|     if (record) { | ||||
|       systemIOActor.send({ | ||||
|         type: SystemIOMachineEvents.renameProject, | ||||
|         data: { | ||||
|           requestedProjectName: record.newName, | ||||
|           projectName: record.oldName, | ||||
|         }, | ||||
|       }) | ||||
|     } | ||||
|   }, | ||||
|   args: { | ||||
|     oldName: { | ||||
|       inputType: 'options', | ||||
|       required: true, | ||||
|       options: () => { | ||||
|         const folders = folderSnapshot() | ||||
|         const options: CommandArgumentOption<string>[] = [] | ||||
|         folders.forEach((folder) => { | ||||
|           options.push({ | ||||
|             name: folder.name, | ||||
|             value: folder.name, | ||||
|             isCurrent: false, | ||||
|           }) | ||||
|         }) | ||||
|         return options | ||||
|       }, | ||||
|     }, | ||||
|     newName: { | ||||
|       inputType: 'string', | ||||
|       required: true, | ||||
|       defaultValue: defaultProjectFolderNameSnapshot, | ||||
|     }, | ||||
|   }, | ||||
| } | ||||
|  | ||||
| export const importFileFromURL: Command = { | ||||
|   name: 'Import file from URL', | ||||
|   groupId: 'projects', | ||||
|   icon: 'file', | ||||
|   description: 'Create a file', | ||||
|   needsReview: true, | ||||
|   onSubmit: (record) => { | ||||
|     if (record) { | ||||
|       systemIOActor.send({ | ||||
|         type: SystemIOMachineEvents.importFileFromURL, | ||||
|         data: { | ||||
|           requestedProjectName: record.projectName, | ||||
|           requestedCode: record.code, | ||||
|           requestedFileName: record.name, | ||||
|         }, | ||||
|       }) | ||||
|     } | ||||
|   }, | ||||
|   args: { | ||||
|     method: { | ||||
|       inputType: 'options', | ||||
|       required: true, | ||||
|       skip: true, | ||||
|       options: isDesktop() | ||||
|         ? [ | ||||
|             { name: 'New project', value: 'newProject' }, | ||||
|             { name: 'Existing project', value: 'existingProject' }, | ||||
|           ] | ||||
|         : [{ name: 'Overwrite', value: 'existingProject' }], | ||||
|       valueSummary(value) { | ||||
|         return isDesktop() | ||||
|           ? value === 'newProject' | ||||
|             ? 'New project' | ||||
|             : 'Existing project' | ||||
|           : 'Overwrite' | ||||
|       }, | ||||
|     }, | ||||
|     // TODO: We can't get the currently-opened project to auto-populate here because | ||||
|     // it's not available on projectMachine, but lower in fileMachine. Unify these. | ||||
|     projectName: { | ||||
|       inputType: 'options', | ||||
|       required: (commandsContext) => | ||||
|         isDesktop() && | ||||
|         commandsContext.argumentsToSubmit.method === 'existingProject', | ||||
|       skip: true, | ||||
|       options: (_, context) => { | ||||
|         const folders = folderSnapshot() | ||||
|         const options: CommandArgumentOption<string>[] = [] | ||||
|         folders.forEach((folder) => { | ||||
|           options.push({ | ||||
|             name: folder.name, | ||||
|             value: folder.name, | ||||
|             isCurrent: false, | ||||
|           }) | ||||
|         }) | ||||
|         return options | ||||
|       }, | ||||
|     }, | ||||
|     name: { | ||||
|       inputType: 'string', | ||||
|       required: isDesktop(), | ||||
|       skip: true, | ||||
|     }, | ||||
|     code: { | ||||
|       inputType: 'text', | ||||
|       required: true, | ||||
|       skip: true, | ||||
|       valueSummary(value) { | ||||
|         const lineCount = value?.trim().split('\n').length | ||||
|         return `${lineCount} line${lineCount === 1 ? '' : 's'}` | ||||
|       }, | ||||
|     }, | ||||
|   }, | ||||
|   reviewMessage(commandBarContext) { | ||||
|     return isDesktop() | ||||
|       ? `Will add the contents from URL to a new ${ | ||||
|           commandBarContext.argumentsToSubmit.method === 'newProject' | ||||
|             ? 'project with file main.kcl' | ||||
|             : `file within the project "${commandBarContext.argumentsToSubmit.projectName}"` | ||||
|         } named "${ | ||||
|           commandBarContext.argumentsToSubmit.name | ||||
|         }", and set default units to "${ | ||||
|           commandBarContext.argumentsToSubmit.units | ||||
|         }".` | ||||
|       : `Will overwrite the contents of the current file with the contents from the URL.` | ||||
|   }, | ||||
| } | ||||
|  | ||||
| /** No disk-writing commands are available in the browser */ | ||||
| export const projectCommands = isDesktop() | ||||
|   ? [ | ||||
|       openProjectCommand, | ||||
|       createProjectCommand, | ||||
|       deleteProjectCommand, | ||||
|       renameProjectCommand, | ||||
|       importFileFromURL, | ||||
|     ] | ||||
|   : [importFileFromURL] | ||||
|  | ||||
| @ -85,10 +85,23 @@ export async function ensureProjectDirectoryExists( | ||||
|   return projectDir | ||||
| } | ||||
|  | ||||
| export async function mkdirOrNOOP(directoryPath: string): Promise<string> { | ||||
|   try { | ||||
|     await window.electron.stat(directoryPath) | ||||
|   } catch (e) { | ||||
|     if (e === 'ENOENT') { | ||||
|       await window.electron.mkdir(directoryPath, { recursive: true }) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   return directoryPath | ||||
| } | ||||
|  | ||||
| export async function createNewProjectDirectory( | ||||
|   projectName: string, | ||||
|   initialCode?: string, | ||||
|   configuration?: DeepPartial<Configuration> | Error | ||||
|   configuration?: DeepPartial<Configuration> | Error, | ||||
|   initialFileName?: string | ||||
| ): Promise<Project> { | ||||
|   if (!configuration) { | ||||
|     configuration = await readAppSettingsFile() | ||||
| @ -114,7 +127,8 @@ export async function createNewProjectDirectory( | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   const projectFile = window.electron.path.join(projectDir, PROJECT_ENTRYPOINT) | ||||
|   const kclFileName = initialFileName || PROJECT_ENTRYPOINT | ||||
|   const projectFile = window.electron.path.join(projectDir, kclFileName) | ||||
|   // When initialCode is present, we're loading existing code.  If it's not | ||||
|   // present, we're creating a new project, and we want to incorporate the | ||||
|   // user's settings. | ||||
|  | ||||
| @ -20,7 +20,7 @@ import type { | ||||
|   HomeLoaderData, | ||||
|   IndexLoaderData, | ||||
| } from '@src/lib/types' | ||||
| import { settingsActor } from '@src/machines/appMachine' | ||||
| import { settingsActor } from '@src/lib/singletons' | ||||
|  | ||||
| export const telemetryLoader: LoaderFunction = async ({ | ||||
|   params, | ||||
|  | ||||
| @ -43,7 +43,7 @@ import { | ||||
|   isOverlap, | ||||
|   uuidv4, | ||||
| } from '@src/lib/utils' | ||||
| import { engineStreamActor } from '@src/machines/appMachine' | ||||
| import { engineStreamActor } from '@src/lib/singletons' | ||||
| import type { ModelingMachineEvent } from '@src/machines/modelingMachine' | ||||
| import { showUnsupportedSelectionToast } from '@src/components/ToastUnsupportedSelection' | ||||
|  | ||||
|  | ||||
| @ -191,7 +191,7 @@ export function readLocalStorageAppSettingsFile(): | ||||
|   } | ||||
| } | ||||
|  | ||||
| function readLocalStorageProjectSettingsFile(): | ||||
| export function readLocalStorageProjectSettingsFile(): | ||||
|   | DeepPartial<ProjectConfiguration> | ||||
|   | Error { | ||||
|   // TODO: Remove backwards compatibility after a few releases. | ||||
| @ -456,7 +456,7 @@ export function getSettingInputType(setting: Setting) { | ||||
| export const jsAppSettings = async () => { | ||||
|   let jsAppSettings = default_app_settings() | ||||
|   if (!TEST) { | ||||
|     const settings = await import('@src/machines/appMachine').then((module) => | ||||
|     const settings = await import('@src/lib/singletons').then((module) => | ||||
|       module.getSettings() | ||||
|     ) | ||||
|     if (settings) { | ||||
|  | ||||
| @ -9,6 +9,22 @@ import { SceneEntities } from '@src/clientSideScene/sceneEntities' | ||||
| import { SceneInfra } from '@src/clientSideScene/sceneInfra' | ||||
| import type { BaseUnit } from '@src/lib/settings/settingsTypes' | ||||
|  | ||||
| import { useSelector } from '@xstate/react' | ||||
| import { createActor, setup, assign } from 'xstate' | ||||
|  | ||||
| import { isDesktop } from '@src/lib/isDesktop' | ||||
| import { createSettings } from '@src/lib/settings/initialSettings' | ||||
| import { authMachine } from '@src/machines/authMachine' | ||||
| import { | ||||
|   engineStreamContextCreate, | ||||
|   engineStreamMachine, | ||||
| } from '@src/machines/engineStreamMachine' | ||||
| import { ACTOR_IDS } from '@src/machines/machineConstants' | ||||
| import { settingsMachine } from '@src/machines/settingsMachine' | ||||
| import { systemIOMachineDesktop } from '@src/machines/systemIO/systemIOMachineDesktop' | ||||
| import { systemIOMachineWeb } from '@src/machines/systemIO/systemIOMachineWeb' | ||||
| import type { AppMachineContext } from '@src/lib/types' | ||||
|  | ||||
| export const codeManager = new CodeManager() | ||||
| export const engineCommandManager = new EngineCommandManager() | ||||
| export const rustContext = new RustContext(engineCommandManager) | ||||
| @ -90,3 +106,85 @@ if (typeof window !== 'undefined') { | ||||
|       }, | ||||
|     }) | ||||
| } | ||||
| const { AUTH, SETTINGS, SYSTEM_IO, ENGINE_STREAM } = ACTOR_IDS | ||||
| const appMachineActors = { | ||||
|   [AUTH]: authMachine, | ||||
|   [SETTINGS]: settingsMachine, | ||||
|   [SYSTEM_IO]: isDesktop() ? systemIOMachineDesktop : systemIOMachineWeb, | ||||
|   [ENGINE_STREAM]: engineStreamMachine, | ||||
| } as const | ||||
|  | ||||
| const appMachine = setup({ | ||||
|   types: {} as { | ||||
|     context: AppMachineContext | ||||
|   }, | ||||
|   actors: appMachineActors, | ||||
| }).createMachine({ | ||||
|   id: 'modeling-app', | ||||
|   context: { | ||||
|     codeManager: codeManager, | ||||
|     kclManager: kclManager, | ||||
|     engineCommandManager: engineCommandManager, | ||||
|     sceneInfra: sceneInfra, | ||||
|     sceneEntitiesManager: sceneEntitiesManager, | ||||
|   }, | ||||
|   entry: [ | ||||
|     assign({ | ||||
|       // Gotcha, if you use spawn, make sure you remove the ActorRef from context | ||||
|       // to prevent memory leaks when the spawned actor is no longer needed | ||||
|       authActor: ({ spawn }) => spawn(AUTH, { id: AUTH, systemId: AUTH }), | ||||
|       settingsActor: ({ spawn }) => | ||||
|         spawn(SETTINGS, { | ||||
|           id: SETTINGS, | ||||
|           systemId: SETTINGS, | ||||
|           input: createSettings(), | ||||
|         }), | ||||
|       systemIOActor: ({ spawn }) => | ||||
|         spawn(SYSTEM_IO, { id: SYSTEM_IO, systemId: SYSTEM_IO }), | ||||
|       engineStreamActor: ({ spawn }) => | ||||
|         spawn(ENGINE_STREAM, { | ||||
|           id: ENGINE_STREAM, | ||||
|           systemId: ENGINE_STREAM, | ||||
|           input: engineStreamContextCreate(), | ||||
|         }), | ||||
|     }), | ||||
|   ], | ||||
| }) | ||||
|  | ||||
| export const appActor = createActor(appMachine, { | ||||
|   systemId: 'root', | ||||
| }) | ||||
|  | ||||
| /** | ||||
|  * GOTCHA: the type coercion of this actor works because it is spawned for | ||||
|  * the lifetime of {appActor}, but would not work if it were invoked | ||||
|  * or if it were destroyed under any conditions during {appActor}'s life | ||||
|  */ | ||||
| export const authActor = appActor.getSnapshot().context.authActor! | ||||
| export const useAuthState = () => useSelector(authActor, (state) => state) | ||||
| export const useToken = () => | ||||
|   useSelector(authActor, (state) => state.context.token) | ||||
| export const useUser = () => | ||||
|   useSelector(authActor, (state) => state.context.user) | ||||
|  | ||||
| /** | ||||
|  * GOTCHA: the type coercion of this actor works because it is spawned for | ||||
|  * the lifetime of {appActor}, but would not work if it were invoked | ||||
|  * or if it were destroyed under any conditions during {appActor}'s life | ||||
|  */ | ||||
| export const settingsActor = appActor.getSnapshot().context.settingsActor! | ||||
| export const getSettings = () => { | ||||
|   const { currentProject: _, ...settings } = settingsActor.getSnapshot().context | ||||
|   return settings | ||||
| } | ||||
| export const useSettings = () => | ||||
|   useSelector(settingsActor, (state) => { | ||||
|     // We have to peel everything that isn't settings off | ||||
|     const { currentProject, ...settings } = state.context | ||||
|     return settings | ||||
|   }) | ||||
|  | ||||
| export const systemIOActor = appActor.getSnapshot().context.systemIOActor! | ||||
|  | ||||
| export const engineStreamActor = | ||||
|   appActor.getSnapshot().context.engineStreamActor! | ||||
|  | ||||
| @ -1,4 +1,14 @@ | ||||
| import type { FileEntry, Project } from '@src/lib/project' | ||||
| import type CodeManager from '@src/lang/codeManager' | ||||
| import type { EngineCommandManager } from '@src/lang/std/engineConnection' | ||||
| import type { KclManager } from '@src/lang/KclSingleton' | ||||
| import type { SceneInfra } from '@src/clientSideScene/sceneInfra' | ||||
| import type { SceneEntities } from '@src/clientSideScene/sceneEntities' | ||||
| import type { engineStreamMachine } from '@src/machines/engineStreamMachine' | ||||
| import type { authMachine } from '@src/machines/authMachine' | ||||
| import type { settingsMachine } from '@src/machines/settingsMachine' | ||||
| import type { systemIOMachine } from '@src/machines/systemIO/systemIOMachine' | ||||
| import type { ActorRefFrom } from 'xstate' | ||||
|  | ||||
| export type IndexLoaderData = { | ||||
|   code: string | null | ||||
| @ -111,3 +121,15 @@ export type AsyncFn<F extends (...args: any[]) => any> = WithReturnType< | ||||
|   F, | ||||
|   Promise<unknown> | ||||
| > | ||||
|  | ||||
| export type AppMachineContext = { | ||||
|   codeManager: CodeManager | ||||
|   kclManager: KclManager | ||||
|   engineCommandManager: EngineCommandManager | ||||
|   sceneInfra: SceneInfra | ||||
|   sceneEntitiesManager: SceneEntities | ||||
|   authActor?: ActorRefFrom<typeof authMachine> | ||||
|   settingsActor?: ActorRefFrom<typeof settingsMachine> | ||||
|   systemIOActor?: ActorRefFrom<typeof systemIOMachine> | ||||
|   engineStreamActor?: ActorRefFrom<typeof engineStreamMachine> | ||||
| } | ||||
|  | ||||
| @ -1,78 +0,0 @@ | ||||
| import { useSelector } from '@xstate/react' | ||||
| import { createActor, setup, spawnChild } from 'xstate' | ||||
|  | ||||
| import { createSettings } from '@src/lib/settings/initialSettings' | ||||
| import { authMachine } from '@src/machines/authMachine' | ||||
| import type { EngineStreamActor } from '@src/machines/engineStreamMachine' | ||||
| import { | ||||
|   engineStreamContextCreate, | ||||
|   engineStreamMachine, | ||||
| } from '@src/machines/engineStreamMachine' | ||||
| import { ACTOR_IDS } from '@src/machines/machineConstants' | ||||
| import { settingsMachine } from '@src/machines/settingsMachine' | ||||
|  | ||||
| const { AUTH, SETTINGS, ENGINE_STREAM } = ACTOR_IDS | ||||
| const appMachineActors = { | ||||
|   [AUTH]: authMachine, | ||||
|   [SETTINGS]: settingsMachine, | ||||
|   [ENGINE_STREAM]: engineStreamMachine, | ||||
| } as const | ||||
|  | ||||
| const appMachine = setup({ | ||||
|   types: {} as { | ||||
|     children: { | ||||
|       auth: typeof AUTH | ||||
|       settings: typeof SETTINGS | ||||
|     } | ||||
|   }, | ||||
|   actors: appMachineActors, | ||||
| }).createMachine({ | ||||
|   id: 'modeling-app', | ||||
|   entry: [ | ||||
|     spawnChild(AUTH, { id: AUTH, systemId: AUTH }), | ||||
|     spawnChild(SETTINGS, { | ||||
|       id: SETTINGS, | ||||
|       systemId: SETTINGS, | ||||
|       input: createSettings(), | ||||
|     }), | ||||
|     spawnChild(ENGINE_STREAM, { | ||||
|       id: ENGINE_STREAM, | ||||
|       systemId: ENGINE_STREAM, | ||||
|       input: engineStreamContextCreate(), | ||||
|     }), | ||||
|   ], | ||||
| }) | ||||
|  | ||||
| export const appActor = createActor(appMachine) | ||||
| /** | ||||
|  * GOTCHA: the type coercion of this actor works because it is spawned for | ||||
|  * the lifetime of {appActor}, but would not work if it were invoked | ||||
|  * or if it were destroyed under any conditions during {appActor}'s life | ||||
|  */ | ||||
| export const authActor = appActor.getSnapshot().children.auth! | ||||
| export const useAuthState = () => useSelector(authActor, (state) => state) | ||||
| export const useToken = () => | ||||
|   useSelector(authActor, (state) => state.context.token) | ||||
| export const useUser = () => | ||||
|   useSelector(authActor, (state) => state.context.user) | ||||
|  | ||||
| /** | ||||
|  * GOTCHA: the type coercion of this actor works because it is spawned for | ||||
|  * the lifetime of {appActor}, but would not work if it were invoked | ||||
|  * or if it were destroyed under any conditions during {appActor}'s life | ||||
|  */ | ||||
| export const settingsActor = appActor.getSnapshot().children.settings! | ||||
| export const getSettings = () => { | ||||
|   const { currentProject: _, ...settings } = settingsActor.getSnapshot().context | ||||
|   return settings | ||||
| } | ||||
| export const useSettings = () => | ||||
|   useSelector(settingsActor, (state) => { | ||||
|     // We have to peel everything that isn't settings off | ||||
|     const { currentProject, ...settings } = state.context | ||||
|     return settings | ||||
|   }) | ||||
|  | ||||
| export const engineStreamActor = appActor.system.get( | ||||
|   ENGINE_STREAM | ||||
| ) as EngineStreamActor | ||||
| @ -12,6 +12,7 @@ import type { | ||||
|   KclCommandValue, | ||||
| } from '@src/lib/commandTypes' | ||||
| import { getCommandArgumentKclValuesOnly } from '@src/lib/commandUtils' | ||||
| import { projectCommands } from '@src/lib/commandBarConfigs/projectsCommandConfig' | ||||
|  | ||||
| export type CommandBarContext = { | ||||
|   commands: Command[] | ||||
| @ -661,7 +662,7 @@ function sortCommands(a: Command, b: Command) { | ||||
|  | ||||
| export const commandBarActor = createActor(commandBarMachine, { | ||||
|   input: { | ||||
|     commands: [...authCommands], | ||||
|     commands: [...authCommands, ...projectCommands], | ||||
|   }, | ||||
| }).start() | ||||
|  | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| import { engineCommandManager, sceneInfra } from '@src/lib/singletons' | ||||
| import type { MutableRefObject } from 'react' | ||||
| import type { ActorRefFrom } from 'xstate' | ||||
| import { assign, fromPromise, setup } from 'xstate' | ||||
| import type { AppMachineContext } from '@src/lib/types' | ||||
|  | ||||
| export enum EngineStreamState { | ||||
|   Off = 'off', | ||||
| @ -79,9 +79,13 @@ export const engineStreamMachine = setup({ | ||||
|   actors: { | ||||
|     [EngineStreamTransition.Play]: fromPromise( | ||||
|       async ({ | ||||
|         input: { context, params }, | ||||
|         input: { context, params, rootContext }, | ||||
|       }: { | ||||
|         input: { context: EngineStreamContext; params: { zoomToFit: boolean } } | ||||
|         input: { | ||||
|           context: EngineStreamContext | ||||
|           params: { zoomToFit: boolean } | ||||
|           rootContext: AppMachineContext | ||||
|         } | ||||
|       }) => { | ||||
|         const canvas = context.canvasRef.current | ||||
|         if (!canvas) return false | ||||
| @ -98,7 +102,7 @@ export const engineStreamMachine = setup({ | ||||
|           return | ||||
|         } | ||||
|  | ||||
|         await sceneInfra.camControls.restoreRemoteCameraStateAndTriggerSync() | ||||
|         await rootContext.sceneInfra.camControls.restoreRemoteCameraStateAndTriggerSync() | ||||
|  | ||||
|         video.style.display = 'block' | ||||
|         canvas.style.display = 'none' | ||||
| @ -108,9 +112,9 @@ export const engineStreamMachine = setup({ | ||||
|     ), | ||||
|     [EngineStreamTransition.Pause]: fromPromise( | ||||
|       async ({ | ||||
|         input: { context }, | ||||
|         input: { context, rootContext }, | ||||
|       }: { | ||||
|         input: { context: EngineStreamContext } | ||||
|         input: { context: EngineStreamContext; rootContext: AppMachineContext } | ||||
|       }) => { | ||||
|         const video = context.videoRef.current | ||||
|         if (!video) return | ||||
| @ -123,7 +127,7 @@ export const engineStreamMachine = setup({ | ||||
|         await holdOntoVideoFrameInCanvas(video, canvas) | ||||
|         video.style.display = 'none' | ||||
|  | ||||
|         await sceneInfra.camControls.saveRemoteCameraState() | ||||
|         await rootContext.sceneInfra.camControls.saveRemoteCameraState() | ||||
|  | ||||
|         // Make sure we're on the next frame for no flickering between canvas | ||||
|         // and the video elements. | ||||
| @ -138,16 +142,20 @@ export const engineStreamMachine = setup({ | ||||
|               context.mediaStream = null | ||||
|               video.srcObject = null | ||||
|  | ||||
|               engineCommandManager.tearDown({ idleMode: true }) | ||||
|               rootContext.engineCommandManager.tearDown({ idleMode: true }) | ||||
|             })() | ||||
|         ) | ||||
|       } | ||||
|     ), | ||||
|     [EngineStreamTransition.StartOrReconfigureEngine]: fromPromise( | ||||
|       async ({ | ||||
|         input: { context, event }, | ||||
|         input: { context, event, rootContext }, | ||||
|       }: { | ||||
|         input: { context: EngineStreamContext; event: any } | ||||
|         input: { | ||||
|           context: EngineStreamContext | ||||
|           event: any | ||||
|           rootContext: AppMachineContext | ||||
|         } | ||||
|       }) => { | ||||
|         if (!context.authToken) return | ||||
|  | ||||
| @ -172,10 +180,10 @@ export const engineStreamMachine = setup({ | ||||
|           ...event.settings, | ||||
|         } | ||||
|  | ||||
|         engineCommandManager.settings = settingsNext | ||||
|         rootContext.engineCommandManager.settings = settingsNext | ||||
|  | ||||
|         window.requestAnimationFrame(() => { | ||||
|           engineCommandManager.start({ | ||||
|           rootContext.engineCommandManager.start({ | ||||
|             setMediaStream: event.onMediaStream, | ||||
|             setIsStreamReady: (isStreamReady: boolean) => { | ||||
|               event.setAppState({ isStreamReady }) | ||||
| @ -225,7 +233,12 @@ export const engineStreamMachine = setup({ | ||||
|       reenter: true, | ||||
|       invoke: { | ||||
|         src: EngineStreamTransition.StartOrReconfigureEngine, | ||||
|         input: (args) => args, | ||||
|         input: (args) => ({ | ||||
|           context: args.context, | ||||
|           rootContext: args.self.system.get('root').getSnapshot().context, | ||||
|           params: { zoomToFit: args.context.zoomToFit }, | ||||
|           event: args.event, | ||||
|         }), | ||||
|       }, | ||||
|       on: { | ||||
|         // Transition requested by engineConnection | ||||
| @ -246,6 +259,7 @@ export const engineStreamMachine = setup({ | ||||
|         src: EngineStreamTransition.Play, | ||||
|         input: (args) => ({ | ||||
|           context: args.context, | ||||
|           rootContext: args.self.system.get('root').getSnapshot().context, | ||||
|           params: { zoomToFit: args.context.zoomToFit }, | ||||
|         }), | ||||
|       }, | ||||
| @ -261,7 +275,11 @@ export const engineStreamMachine = setup({ | ||||
|     [EngineStreamState.Reconfiguring]: { | ||||
|       invoke: { | ||||
|         src: EngineStreamTransition.StartOrReconfigureEngine, | ||||
|         input: (args) => args, | ||||
|         input: (args) => ({ | ||||
|           context: args.context, | ||||
|           rootContext: args.self.system.get('root').getSnapshot().context, | ||||
|           event: args.event, | ||||
|         }), | ||||
|         onDone: { | ||||
|           target: EngineStreamState.Playing, | ||||
|         }, | ||||
| @ -270,7 +288,10 @@ export const engineStreamMachine = setup({ | ||||
|     [EngineStreamState.Paused]: { | ||||
|       invoke: { | ||||
|         src: EngineStreamTransition.Pause, | ||||
|         input: (args) => args, | ||||
|         input: (args) => ({ | ||||
|           context: args.context, | ||||
|           rootContext: args.self.system.get('root').getSnapshot().context, | ||||
|         }), | ||||
|       }, | ||||
|       on: { | ||||
|         [EngineStreamTransition.StartOrReconfigureEngine]: { | ||||
| @ -282,7 +303,11 @@ export const engineStreamMachine = setup({ | ||||
|       reenter: true, | ||||
|       invoke: { | ||||
|         src: EngineStreamTransition.StartOrReconfigureEngine, | ||||
|         input: (args) => args, | ||||
|         input: (args) => ({ | ||||
|           context: args.context, | ||||
|           rootContext: args.self.system.get('root').getSnapshot().context, | ||||
|           event: args.event, | ||||
|         }), | ||||
|       }, | ||||
|       on: { | ||||
|         // The stream can be paused as it's resuming. | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| export const ACTOR_IDS = { | ||||
|   AUTH: 'auth', | ||||
|   SETTINGS: 'settings', | ||||
|   SYSTEM_IO: 'systemIO', | ||||
|   ENGINE_STREAM: 'engine_stream', | ||||
| } as const | ||||
|  | ||||
| @ -1,337 +0,0 @@ | ||||
| import { assign, fromPromise, setup } from 'xstate' | ||||
|  | ||||
| import type { ProjectsCommandSchema } from '@src/lib/commandBarConfigs/projectsCommandConfig' | ||||
| import type { Project } from '@src/lib/project' | ||||
| import { isArray } from '@src/lib/utils' | ||||
|  | ||||
| export const projectsMachine = setup({ | ||||
|   types: { | ||||
|     context: {} as { | ||||
|       projects: Project[] | ||||
|       defaultProjectName: string | ||||
|       defaultDirectory: string | ||||
|       hasListedProjects: boolean | ||||
|     }, | ||||
|     events: {} as | ||||
|       | { type: 'Read projects'; data: ProjectsCommandSchema['Read projects'] } | ||||
|       | { type: 'Open project'; data: ProjectsCommandSchema['Open project'] } | ||||
|       | { | ||||
|           type: 'Rename project' | ||||
|           data: ProjectsCommandSchema['Rename project'] | ||||
|         } | ||||
|       | { | ||||
|           type: 'Create project' | ||||
|           data: ProjectsCommandSchema['Create project'] | ||||
|         } | ||||
|       | { | ||||
|           type: 'Delete project' | ||||
|           data: ProjectsCommandSchema['Delete project'] | ||||
|         } | ||||
|       | { | ||||
|           type: 'Import file from URL' | ||||
|           data: ProjectsCommandSchema['Import file from URL'] | ||||
|         } | ||||
|       | { type: 'navigate'; data: { name: string } } | ||||
|       | { | ||||
|           type: 'xstate.done.actor.read-projects' | ||||
|           output: Project[] | ||||
|         } | ||||
|       | { | ||||
|           type: 'xstate.done.actor.delete-project' | ||||
|           output: { message: string; name: string } | ||||
|         } | ||||
|       | { | ||||
|           type: 'xstate.done.actor.create-project' | ||||
|           output: { message: string; name: string } | ||||
|         } | ||||
|       | { | ||||
|           type: 'xstate.done.actor.rename-project' | ||||
|           output: { message: string; oldName: string; newName: string } | ||||
|         } | ||||
|       | { | ||||
|           type: 'xstate.done.actor.create-file' | ||||
|           output: { message: string; projectName: string; fileName: string } | ||||
|         } | ||||
|       | { type: 'assign'; data: { [key: string]: any } }, | ||||
|     input: {} as { | ||||
|       projects: Project[] | ||||
|       defaultProjectName: string | ||||
|       defaultDirectory: string | ||||
|       hasListedProjects: boolean | ||||
|     }, | ||||
|   }, | ||||
|   actions: { | ||||
|     setProjects: assign({ | ||||
|       projects: ({ context, event }) => | ||||
|         'output' in event && isArray(event.output) | ||||
|           ? event.output | ||||
|           : context.projects, | ||||
|     }), | ||||
|     setHasListedProjects: assign({ | ||||
|       hasListedProjects: () => true, | ||||
|     }), | ||||
|     toastSuccess: () => {}, | ||||
|     toastError: () => {}, | ||||
|     navigateToProject: () => {}, | ||||
|     navigateToProjectIfNeeded: () => {}, | ||||
|     navigateToFile: () => {}, | ||||
|   }, | ||||
|   actors: { | ||||
|     readProjects: fromPromise(() => Promise.resolve([] as Project[])), | ||||
|     createProject: fromPromise( | ||||
|       (_: { input: { name: string; projects: Project[] } }) => | ||||
|         Promise.resolve({ message: '' }) | ||||
|     ), | ||||
|     renameProject: fromPromise( | ||||
|       (_: { | ||||
|         input: { | ||||
|           oldName: string | ||||
|           newName: string | ||||
|           defaultProjectName: string | ||||
|           defaultDirectory: string | ||||
|           projects: Project[] | ||||
|         } | ||||
|       }) => | ||||
|         Promise.resolve({ | ||||
|           message: '', | ||||
|           oldName: '', | ||||
|           newName: '', | ||||
|         }) | ||||
|     ), | ||||
|     deleteProject: fromPromise( | ||||
|       (_: { input: { defaultDirectory: string; name: string } }) => | ||||
|         Promise.resolve({ | ||||
|           message: '', | ||||
|           name: '', | ||||
|         }) | ||||
|     ), | ||||
|     createFile: fromPromise( | ||||
|       (_: { | ||||
|         input: ProjectsCommandSchema['Import file from URL'] & { | ||||
|           projects: Project[] | ||||
|         } | ||||
|       }) => Promise.resolve({ message: '', projectName: '', fileName: '' }) | ||||
|     ), | ||||
|   }, | ||||
|   guards: { | ||||
|     'Has at least 1 project': ({ event }) => { | ||||
|       if (event.type !== 'xstate.done.actor.read-projects') return false | ||||
|       return event.output.length ? event.output.length >= 1 : false | ||||
|     }, | ||||
|   }, | ||||
| }).createMachine({ | ||||
|   /** @xstate-layout N4IgpgJg5mDOIC5QAkD2BbMACdBDAxgBYCWAdmAMS6yzFSkDaADALqKgAOqtALsaqXYgAHogAsAJgA0IAJ6IAjAHYAbADoArBJVMFTCQA4mTAMwmxAXwsy0mHARLkKASXRcATjywAzYgBtsb3cMLABVACUAGWY2JBAuXn5BONEESRl5BAUFFQM1HQUxJSYATmyFAyUTKxsMbDwiMjA1ZGosUlQsDmCAKzB8HlgKcLBcCC7e-sGYoQTiPgEhVIUy9SKSiQ0NEwkclRyMxGKJNRKlMRVDU23zpRqQW3qHJpa2jonUPoGhgGF3UZ42G6nymMzicwWyVAyzKJzECg0OiYGjERhK+kOWUMSlOGiUlTEJlMxRKBnuj3sjXIr1gHy+g2Go3GwPpsDBnG48ySS0QGj0+Q0JTOGkqBg2JhKmNRJy2OyUEn0Kml5LqlMczVatJZUyGI1IuDs2oG7PinMhPIQfKYAqFShF+PFkrkRwkYjU8OUShKKhMKg0pUs1geqoa6ppdJ1FD+AKBk2NrFmZu5KV5-L9tvtYokEsxelRagUCoMiMumyUdpVdlDL01Ee+FAAImAAoC6zwTRDk9DU9b08LRY7MWd1AZcoS-aoLiYFJWnlSNW0jQyAPIcMCkNsdpOLFOWtOC-sO7NOzLbGWFbY6acmFESWdql7R3B8UhQNsUCACZpkABuqAA1s0+D-M+YAALRLluiQ7t2WRMGIbryjeBgGBIEglNsCi5sY6hMOchQqEoCLFCh97VtST4vm+S4UGA7jBO4agcH4z7eKg7joGowExhBcbtgm4LblCIiKPBiHZiKqHoZhuaFhoBbGMYBhiPBCgStUQYUuRzR6gaZDUXxH5fmov4Ac0-z6pgvEgvGsQctBwnLGJahIZJaEYdOmKEfJuQSoRRKSRcZHPNSunoPp750QxTEsTwbEcWoFkGuBkECfZXIwSJcEIS5Ekoe5MnOggXqIaUqFiiUhIInemkhiFzRNi2EU0Z+1KmYBagQM2YCAtZ9JQRljmiTlrn5dJnlFShOLwcpZjKBs+KBrUVb1WojU9c1hlRexMWsexnFdS2KV8QN5q7laNqHlmOaTcpmjoWc8L6HoYrBfOagjGMm02QyrXfqQf4dSBEB9Tqp1dllebichUkeVhRXTiU+QecUKhCps4pvWGn0QN9rJGW1ANmYlTKg98DAKHZpoORanoyqh8oImU3pKJifJ5Ohdr7CYqjIvKWMvDjeORttjHMXtCXA2T0xpdTg20+W9MSIzgorIRXlpr6ORbPs+jwQLFEgVRPj+JQf0mUTHXcaBYG+AE4OZcsxbuts5hbCK+zFmImJgSc5zPV6BQVD6RQG80lERXblCi7tcX7VxRvgVHDtDQgaE4vK2gXKiTA6ItubmJo8IoqOylERUVhBh0XXwHEWn1YmNO7mBKg+2KpzK2IpIBUwBhqUtwYre9tbvEutfpWdsFFnkPpVJnQqz15Ozur36FoypREbGH4Zj438u7tO1qIu5qK5PBGyYjebpEkS44e9oVTbxHr5tnvk9ZeXajTuW+zaHNuzYRUmoeCEp-RKlUHJbeYVhYDDfhDVIxR5IGGnISQk6E+6EkxMUBQX9c66EMMg3Q5Zt7rWNkuOBjsjilDUAqQsZQzzgOkJNUk7o7SmF-tiXOUCmQwMGBQ1OF4TgVGzFUG8yIxQGDZiYPI4oqh+mPsrDSy05xhmfm+KO-CLT+iRucEUuwFQqWzMWXM2YTAuTtL6ZBylkRcMrkAA */ | ||||
|   id: 'Home machine', | ||||
|  | ||||
|   initial: 'Reading projects', | ||||
|  | ||||
|   context: ({ input }) => ({ | ||||
|     ...input, | ||||
|   }), | ||||
|  | ||||
|   on: { | ||||
|     assign: { | ||||
|       actions: assign(({ event }) => ({ | ||||
|         ...event.data, | ||||
|       })), | ||||
|     }, | ||||
|  | ||||
|     'Import file from URL': '.Creating file', | ||||
|   }, | ||||
|   states: { | ||||
|     'Has no projects': { | ||||
|       on: { | ||||
|         'Read projects': { | ||||
|           target: 'Reading projects', | ||||
|         }, | ||||
|         'Create project': { | ||||
|           target: 'Creating project', | ||||
|         }, | ||||
|       }, | ||||
|     }, | ||||
|  | ||||
|     'Has projects': { | ||||
|       on: { | ||||
|         'Read projects': { | ||||
|           target: 'Reading projects', | ||||
|         }, | ||||
|  | ||||
|         'Rename project': { | ||||
|           target: 'Renaming project', | ||||
|         }, | ||||
|  | ||||
|         'Create project': { | ||||
|           target: 'Creating project', | ||||
|         }, | ||||
|  | ||||
|         'Delete project': { | ||||
|           target: 'Deleting project', | ||||
|         }, | ||||
|  | ||||
|         'Open project': { | ||||
|           target: 'Reading projects', | ||||
|           actions: 'navigateToProject', | ||||
|           reenter: true, | ||||
|         }, | ||||
|       }, | ||||
|     }, | ||||
|  | ||||
|     'Creating project': { | ||||
|       invoke: { | ||||
|         id: 'create-project', | ||||
|         src: 'createProject', | ||||
|         input: ({ event, context }) => { | ||||
|           if ( | ||||
|             event.type !== 'Create project' && | ||||
|             event.type !== 'Import file from URL' | ||||
|           ) { | ||||
|             return { | ||||
|               name: '', | ||||
|               projects: context.projects, | ||||
|             } | ||||
|           } | ||||
|           return { | ||||
|             name: event.data.name, | ||||
|             projects: context.projects, | ||||
|           } | ||||
|         }, | ||||
|         onDone: [ | ||||
|           { | ||||
|             target: 'Reading projects', | ||||
|             actions: ['toastSuccess', 'navigateToProject'], | ||||
|           }, | ||||
|         ], | ||||
|         onError: [ | ||||
|           { | ||||
|             target: 'Reading projects', | ||||
|             actions: ['toastError'], | ||||
|           }, | ||||
|         ], | ||||
|       }, | ||||
|     }, | ||||
|  | ||||
|     'Renaming project': { | ||||
|       invoke: { | ||||
|         id: 'rename-project', | ||||
|         src: 'renameProject', | ||||
|         input: ({ event, context }) => { | ||||
|           if (event.type !== 'Rename project') { | ||||
|             // This is to make TS happy | ||||
|             return { | ||||
|               defaultProjectName: context.defaultProjectName, | ||||
|               defaultDirectory: context.defaultDirectory, | ||||
|               oldName: '', | ||||
|               newName: '', | ||||
|               projects: context.projects, | ||||
|             } | ||||
|           } | ||||
|           return { | ||||
|             defaultProjectName: context.defaultProjectName, | ||||
|             defaultDirectory: context.defaultDirectory, | ||||
|             oldName: event.data.oldName, | ||||
|             newName: event.data.newName, | ||||
|             projects: context.projects, | ||||
|           } | ||||
|         }, | ||||
|         onDone: [ | ||||
|           { | ||||
|             target: '#Home machine.Reading projects', | ||||
|             actions: ['toastSuccess', 'navigateToProjectIfNeeded'], | ||||
|           }, | ||||
|         ], | ||||
|         onError: [ | ||||
|           { | ||||
|             target: '#Home machine.Reading projects', | ||||
|             actions: ['toastError'], | ||||
|           }, | ||||
|         ], | ||||
|       }, | ||||
|     }, | ||||
|  | ||||
|     'Deleting project': { | ||||
|       invoke: { | ||||
|         id: 'delete-project', | ||||
|         src: 'deleteProject', | ||||
|         input: ({ event, context }) => { | ||||
|           if (event.type !== 'Delete project') { | ||||
|             // This is to make TS happy | ||||
|             return { | ||||
|               defaultDirectory: context.defaultDirectory, | ||||
|               name: '', | ||||
|             } | ||||
|           } | ||||
|           return { | ||||
|             defaultDirectory: context.defaultDirectory, | ||||
|             name: event.data.name, | ||||
|           } | ||||
|         }, | ||||
|         onDone: [ | ||||
|           { | ||||
|             actions: ['toastSuccess', 'navigateToProjectIfNeeded'], | ||||
|             target: '#Home machine.Reading projects', | ||||
|           }, | ||||
|         ], | ||||
|         onError: { | ||||
|           actions: ['toastError'], | ||||
|           target: '#Home machine.Has projects', | ||||
|         }, | ||||
|       }, | ||||
|     }, | ||||
|  | ||||
|     'Reading projects': { | ||||
|       invoke: { | ||||
|         id: 'read-projects', | ||||
|         src: 'readProjects', | ||||
|         onDone: [ | ||||
|           { | ||||
|             guard: 'Has at least 1 project', | ||||
|             target: 'Has projects', | ||||
|             actions: ['setProjects', 'setHasListedProjects'], | ||||
|           }, | ||||
|           { | ||||
|             target: 'Has no projects', | ||||
|             actions: ['setProjects', 'setHasListedProjects'], | ||||
|           }, | ||||
|         ], | ||||
|         onError: [ | ||||
|           { | ||||
|             target: 'Has no projects', | ||||
|             actions: ['toastError'], | ||||
|           }, | ||||
|         ], | ||||
|       }, | ||||
|     }, | ||||
|  | ||||
|     'Creating file': { | ||||
|       invoke: { | ||||
|         id: 'create-file', | ||||
|         src: 'createFile', | ||||
|         input: ({ event, context }) => { | ||||
|           if (event.type !== 'Import file from URL') { | ||||
|             return { | ||||
|               code: '', | ||||
|               name: '', | ||||
|               method: 'existingProject', | ||||
|               projects: context.projects, | ||||
|             } | ||||
|           } | ||||
|           return { | ||||
|             code: event.data.code || '', | ||||
|             name: event.data.name, | ||||
|             method: event.data.method, | ||||
|             projectName: event.data.projectName, | ||||
|             projects: context.projects, | ||||
|           } | ||||
|         }, | ||||
|         onDone: { | ||||
|           target: 'Reading projects', | ||||
|           actions: ['navigateToFile', 'toastSuccess'], | ||||
|         }, | ||||
|         onError: { | ||||
|           target: 'Reading projects', | ||||
|           actions: 'toastError', | ||||
|         }, | ||||
|       }, | ||||
|     }, | ||||
|   }, | ||||
| }) | ||||
| @ -35,13 +35,6 @@ import { | ||||
|   saveSettings, | ||||
|   setSettingsAtLevel, | ||||
| } from '@src/lib/settings/settingsUtils' | ||||
| import { | ||||
|   codeManager, | ||||
|   engineCommandManager, | ||||
|   kclManager, | ||||
|   sceneEntitiesManager, | ||||
|   sceneInfra, | ||||
| } from '@src/lib/singletons' | ||||
| import { | ||||
|   Themes, | ||||
|   darkModeMatcher, | ||||
| @ -95,13 +88,14 @@ export const settingsMachine = setup({ | ||||
|         doNotPersist: boolean | ||||
|         context: SettingsMachineContext | ||||
|         toastCallback?: () => void | ||||
|         rootContext: any | ||||
|       } | ||||
|     >(async ({ input }) => { | ||||
|       // Without this, when a user changes the file, it'd | ||||
|       // create a detection loop with the file-system watcher. | ||||
|       if (input.doNotPersist) return | ||||
|  | ||||
|       codeManager.writeCausedByAppCheckedInFileTreeFileSystemWatcher = true | ||||
|       input.rootContext.codeManager.writeCausedByAppCheckedInFileTreeFileSystemWatcher = true | ||||
|       const { currentProject, ...settings } = input.context | ||||
|  | ||||
|       const val = await saveSettings(settings, currentProject?.path) | ||||
| @ -190,20 +184,28 @@ export const settingsMachine = setup({ | ||||
|     }), | ||||
|   }, | ||||
|   actions: { | ||||
|     setEngineTheme: ({ context }) => { | ||||
|     setEngineTheme: ({ context, self }) => { | ||||
|       const rootContext = self.system.get('root').getSnapshot().context | ||||
|       const engineCommandManager = rootContext.engineCommandManager | ||||
|       if (engineCommandManager && context.app.theme.current) { | ||||
|         engineCommandManager | ||||
|           .setTheme(context.app.theme.current) | ||||
|           .catch(reportRejection) | ||||
|       } | ||||
|     }, | ||||
|     setClientTheme: ({ context }) => { | ||||
|     setClientTheme: ({ context, self }) => { | ||||
|       const rootContext = self.system.get('root').getSnapshot().context | ||||
|       const sceneInfra = rootContext.sceneInfra | ||||
|       const sceneEntitiesManager = rootContext.sceneEntitiesManager | ||||
|  | ||||
|       if (!sceneInfra || !sceneEntitiesManager) return | ||||
|       const opposingTheme = getOppositeTheme(context.app.theme.current) | ||||
|       sceneInfra.theme = opposingTheme | ||||
|       sceneEntitiesManager.updateSegmentBaseColor(opposingTheme) | ||||
|     }, | ||||
|     setAllowOrbitInSketchMode: ({ context }) => { | ||||
|     setAllowOrbitInSketchMode: ({ context, self }) => { | ||||
|       const rootContext = self.system.get('root').getSnapshot().context | ||||
|       const sceneInfra = rootContext.sceneInfra | ||||
|       if (!sceneInfra.camControls) return | ||||
|       sceneInfra.camControls._setting_allowOrbitInSketchMode = | ||||
|         context.app.allowOrbitInSketchMode.current | ||||
| @ -232,7 +234,9 @@ export const settingsMachine = setup({ | ||||
|         id: `${event.type}.success`, | ||||
|       }) | ||||
|     }, | ||||
|     'Execute AST': ({ context, event }) => { | ||||
|     'Execute AST': ({ context, event, self }) => { | ||||
|       const rootContext = self.system.get('root').getSnapshot().context | ||||
|       const kclManager = rootContext.kclManager | ||||
|       try { | ||||
|         const relevantSetting = (s: typeof settings) => { | ||||
|           return ( | ||||
| @ -345,8 +349,10 @@ export const settingsMachine = setup({ | ||||
|         currentTheme === Themes.System ? getSystemTheme() : currentTheme | ||||
|       ) | ||||
|     }, | ||||
|     setEngineCameraProjection: ({ context }) => { | ||||
|     setEngineCameraProjection: ({ context, self }) => { | ||||
|       const newCurrentProjection = context.modeling.cameraProjection.current | ||||
|       const rootContext = self.system.get('root').getSnapshot().context | ||||
|       const sceneInfra = rootContext.sceneInfra | ||||
|       sceneInfra.camControls.setEngineCameraProjection(newCurrentProjection) | ||||
|     }, | ||||
|     sendThemeToWatcher: sendTo('watchSystemTheme', ({ context }) => ({ | ||||
| @ -532,7 +538,7 @@ export const settingsMachine = setup({ | ||||
|             console.error('Error persisting settings') | ||||
|           }, | ||||
|         }, | ||||
|         input: ({ context, event }) => { | ||||
|         input: ({ context, event, self }) => { | ||||
|           if ( | ||||
|             event.type === 'set.app.namedViews' && | ||||
|             'toastCallback' in event.data | ||||
| @ -541,12 +547,14 @@ export const settingsMachine = setup({ | ||||
|               doNotPersist: event.doNotPersist ?? false, | ||||
|               context, | ||||
|               toastCallback: event.data.toastCallback, | ||||
|               rootContext: self.system.get('root').getSnapshot().context, | ||||
|             } | ||||
|           } | ||||
|  | ||||
|           return { | ||||
|             doNotPersist: event.doNotPersist ?? false, | ||||
|             context, | ||||
|             rootContext: self.system.get('root').getSnapshot().context, | ||||
|           } | ||||
|         }, | ||||
|       }, | ||||
|  | ||||
							
								
								
									
										21
									
								
								src/machines/systemIO/hooks.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/machines/systemIO/hooks.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,21 @@ | ||||
| import { systemIOActor } from '@src/lib/singletons' | ||||
| import { useSelector } from '@xstate/react' | ||||
| export const useRequestedProjectName = () => | ||||
|   useSelector(systemIOActor, (state) => state.context.requestedProjectName) | ||||
| export const useRequestedFileName = () => | ||||
|   useSelector(systemIOActor, (state) => state.context.requestedFileName) | ||||
| export const useProjectDirectoryPath = () => | ||||
|   useSelector(systemIOActor, (state) => state.context.projectDirectoryPath) | ||||
| export const useFolders = () => | ||||
|   useSelector(systemIOActor, (state) => state.context.folders) | ||||
| export const useState = () => useSelector(systemIOActor, (state) => state) | ||||
| export const useCanReadWriteProjectDirectory = () => | ||||
|   useSelector( | ||||
|     systemIOActor, | ||||
|     (state) => state.context.canReadWriteProjectDirectory | ||||
|   ) | ||||
| export const useHasListedProjects = () => | ||||
|   useSelector(systemIOActor, (state) => state.context.hasListedProjects) | ||||
|  | ||||
| export const useClearURLParams = () => | ||||
|   useSelector(systemIOActor, (state) => state.context.clearURLParams) | ||||
							
								
								
									
										11
									
								
								src/machines/systemIO/snapshotContext.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/machines/systemIO/snapshotContext.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,11 @@ | ||||
| import { systemIOActor } from '@src/lib/singletons' | ||||
|  | ||||
| export const folderSnapshot = () => { | ||||
|   const { folders } = systemIOActor.getSnapshot().context | ||||
|   return folders | ||||
| } | ||||
|  | ||||
| export const defaultProjectFolderNameSnapshot = () => { | ||||
|   const { defaultProjectFolderName } = systemIOActor.getSnapshot().context | ||||
|   return defaultProjectFolderName | ||||
| } | ||||
							
								
								
									
										78
									
								
								src/machines/systemIO/systemIOMachine.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								src/machines/systemIO/systemIOMachine.test.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,78 @@ | ||||
| import { DEFAULT_PROJECT_NAME } from '@src/lib/constants' | ||||
| import { systemIOMachineDesktop } from '@src/machines/systemIO/systemIOMachineDesktop' | ||||
| import { | ||||
|   NO_PROJECT_DIRECTORY, | ||||
|   SystemIOMachineEvents, | ||||
|   SystemIOMachineStates, | ||||
| } from '@src/machines/systemIO/utils' | ||||
| import path from 'node:path' | ||||
| import { createActor, waitFor } from 'xstate' | ||||
|  | ||||
| describe('systemIOMachine - XState', () => { | ||||
|   describe('desktop', () => { | ||||
|     describe('when initializied', () => { | ||||
|       it('should contain the default context values', () => { | ||||
|         const actor = createActor(systemIOMachineDesktop).start() | ||||
|         const context = actor.getSnapshot().context | ||||
|         expect(context.folders).toStrictEqual([]) | ||||
|         expect(context.defaultProjectFolderName).toStrictEqual( | ||||
|           DEFAULT_PROJECT_NAME | ||||
|         ) | ||||
|         expect(context.projectDirectoryPath).toBe(NO_PROJECT_DIRECTORY) | ||||
|         expect(context.hasListedProjects).toBe(false) | ||||
|         expect(context.requestedProjectName).toStrictEqual({ | ||||
|           name: NO_PROJECT_DIRECTORY, | ||||
|         }) | ||||
|         expect(context.requestedFileName).toStrictEqual({ | ||||
|           project: NO_PROJECT_DIRECTORY, | ||||
|           file: NO_PROJECT_DIRECTORY, | ||||
|         }) | ||||
|       }) | ||||
|       it('should be in idle state', () => { | ||||
|         const actor = createActor(systemIOMachineDesktop).start() | ||||
|         const state = actor.getSnapshot().value | ||||
|         expect(state).toBe(SystemIOMachineStates.idle) | ||||
|       }) | ||||
|     }) | ||||
|     describe('when reading projects', () => { | ||||
|       it('should exit early when project directory is empty string', async () => { | ||||
|         const actor = createActor(systemIOMachineDesktop).start() | ||||
|         actor.send({ | ||||
|           type: SystemIOMachineEvents.readFoldersFromProjectDirectory, | ||||
|         }) | ||||
|         await waitFor(actor, (state) => | ||||
|           state.matches(SystemIOMachineStates.readingFolders) | ||||
|         ) | ||||
|         await waitFor(actor, (state) => | ||||
|           state.matches(SystemIOMachineStates.idle) | ||||
|         ) | ||||
|         const context = actor.getSnapshot().context | ||||
|         expect(context.folders).toStrictEqual([]) | ||||
|       }) | ||||
|     }) | ||||
|     describe('when setting project directory path', () => { | ||||
|       it('should set new project directory path', async () => { | ||||
|         const kclSamplesPath = path.join('public', 'kcl-samples') | ||||
|         const actor = createActor(systemIOMachineDesktop).start() | ||||
|         actor.send({ | ||||
|           type: SystemIOMachineEvents.setProjectDirectoryPath, | ||||
|           data: { requestedProjectDirectoryPath: kclSamplesPath }, | ||||
|         }) | ||||
|         let context = actor.getSnapshot().context | ||||
|         expect(context.projectDirectoryPath).toBe(kclSamplesPath) | ||||
|       }) | ||||
|     }) | ||||
|     describe('when setting default project folder name', () => { | ||||
|       it('should set a new default project folder name', async () => { | ||||
|         const expected = 'coolcoolcoolProjectName' | ||||
|         const actor = createActor(systemIOMachineDesktop).start() | ||||
|         actor.send({ | ||||
|           type: SystemIOMachineEvents.setDefaultProjectFolderName, | ||||
|           data: { requestedDefaultProjectFolderName: expected }, | ||||
|         }) | ||||
|         let context = actor.getSnapshot().context | ||||
|         expect(context.defaultProjectFolderName).toBe(expected) | ||||
|       }) | ||||
|     }) | ||||
|   }) | ||||
| }) | ||||
							
								
								
									
										448
									
								
								src/machines/systemIO/systemIOMachine.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										448
									
								
								src/machines/systemIO/systemIOMachine.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,448 @@ | ||||
| import { DEFAULT_PROJECT_NAME } from '@src/lib/constants' | ||||
| import type { Project } from '@src/lib/project' | ||||
| import type { SystemIOContext } from '@src/machines/systemIO/utils' | ||||
| import { | ||||
|   NO_PROJECT_DIRECTORY, | ||||
|   SystemIOMachineActions, | ||||
|   SystemIOMachineActors, | ||||
|   SystemIOMachineEvents, | ||||
|   SystemIOMachineStates, | ||||
| } from '@src/machines/systemIO/utils' | ||||
| import toast from 'react-hot-toast' | ||||
| import { assertEvent, assign, fromPromise, setup } from 'xstate' | ||||
| import type { AppMachineContext } from '@src/lib/types' | ||||
|  | ||||
| /** | ||||
|  * Handles any system level I/O for folders and files | ||||
|  * This machine will be initializes once within the applications runtime | ||||
|  * and exist for the entire life cycle of the application and able to be access | ||||
|  * at a global level. | ||||
|  */ | ||||
| export const systemIOMachine = setup({ | ||||
|   types: { | ||||
|     context: {} as SystemIOContext, | ||||
|     events: {} as | ||||
|       | { | ||||
|           type: SystemIOMachineEvents.readFoldersFromProjectDirectory | ||||
|         } | ||||
|       | { | ||||
|           type: SystemIOMachineEvents.done_readFoldersFromProjectDirectory | ||||
|           output: Project[] | ||||
|         } | ||||
|       | { | ||||
|           type: SystemIOMachineEvents.done_checkReadWrite | ||||
|           output: { value: boolean; error: unknown } | ||||
|         } | ||||
|       | { | ||||
|           type: SystemIOMachineEvents.setProjectDirectoryPath | ||||
|           data: { requestedProjectDirectoryPath: string } | ||||
|         } | ||||
|       | { | ||||
|           type: SystemIOMachineEvents.navigateToProject | ||||
|           data: { requestedProjectName: string } | ||||
|         } | ||||
|       | { | ||||
|           type: SystemIOMachineEvents.navigateToFile | ||||
|           data: { requestedProjectName: string; requestedFileName: string } | ||||
|         } | ||||
|       | { | ||||
|           type: SystemIOMachineEvents.createProject | ||||
|           data: { requestedProjectName: string } | ||||
|         } | ||||
|       | { | ||||
|           type: SystemIOMachineEvents.renameProject | ||||
|           data: { requestedProjectName: string; projectName: string } | ||||
|         } | ||||
|       | { | ||||
|           type: SystemIOMachineEvents.deleteProject | ||||
|           data: { requestedProjectName: string } | ||||
|         } | ||||
|       | { | ||||
|           type: SystemIOMachineEvents.createKCLFile | ||||
|           data: { | ||||
|             requestedProjectName: string | ||||
|             requestedFileName: string | ||||
|             requestedCode: string | ||||
|           } | ||||
|         } | ||||
|       | { | ||||
|           type: SystemIOMachineEvents.importFileFromURL | ||||
|           data: { | ||||
|             requestedProjectName: string | ||||
|             requestedFileName: string | ||||
|             requestedCode: string | ||||
|           } | ||||
|         } | ||||
|       | { | ||||
|           type: SystemIOMachineEvents.setDefaultProjectFolderName | ||||
|           data: { requestedDefaultProjectFolderName: string } | ||||
|         }, | ||||
|   }, | ||||
|   actions: { | ||||
|     [SystemIOMachineActions.setFolders]: assign({ | ||||
|       folders: ({ event }) => { | ||||
|         assertEvent( | ||||
|           event, | ||||
|           SystemIOMachineEvents.done_readFoldersFromProjectDirectory | ||||
|         ) | ||||
|         return event.output | ||||
|       }, | ||||
|     }), | ||||
|     [SystemIOMachineActions.setProjectDirectoryPath]: assign({ | ||||
|       projectDirectoryPath: ({ event }) => { | ||||
|         assertEvent(event, SystemIOMachineEvents.setProjectDirectoryPath) | ||||
|         return event.data.requestedProjectDirectoryPath | ||||
|       }, | ||||
|     }), | ||||
|     [SystemIOMachineActions.setRequestedProjectName]: assign({ | ||||
|       requestedProjectName: ({ event }) => { | ||||
|         assertEvent(event, SystemIOMachineEvents.navigateToProject) | ||||
|         return { name: event.data.requestedProjectName } | ||||
|       }, | ||||
|     }), | ||||
|     [SystemIOMachineActions.setRequestedFileName]: assign({ | ||||
|       requestedFileName: ({ event }) => { | ||||
|         assertEvent(event, SystemIOMachineEvents.navigateToFile) | ||||
|         return { | ||||
|           project: event.data.requestedProjectName, | ||||
|           file: event.data.requestedFileName, | ||||
|         } | ||||
|       }, | ||||
|     }), | ||||
|     [SystemIOMachineActions.setDefaultProjectFolderName]: assign({ | ||||
|       defaultProjectFolderName: ({ event }) => { | ||||
|         assertEvent(event, SystemIOMachineEvents.setDefaultProjectFolderName) | ||||
|         return event.data.requestedDefaultProjectFolderName | ||||
|       }, | ||||
|     }), | ||||
|     [SystemIOMachineActions.toastSuccess]: ({ event }) => { | ||||
|       toast.success( | ||||
|         ('data' in event && typeof event.data === 'string' && event.data) || | ||||
|           ('output' in event && | ||||
|             'message' in event.output && | ||||
|             typeof event.output.message === 'string' && | ||||
|             event.output.message) || | ||||
|           '' | ||||
|       ) | ||||
|     }, | ||||
|     [SystemIOMachineActions.toastError]: ({ event }) => { | ||||
|       toast.error( | ||||
|         ('data' in event && typeof event.data === 'string' && event.data) || | ||||
|           ('output' in event && | ||||
|             typeof event.output === 'string' && | ||||
|             event.output) || | ||||
|           ('error' in event && | ||||
|             event.error instanceof Error && | ||||
|             event.error.message) || | ||||
|           '' | ||||
|       ) | ||||
|     }, | ||||
|     [SystemIOMachineActions.setReadWriteProjectDirectory]: assign({ | ||||
|       canReadWriteProjectDirectory: ({ event }) => { | ||||
|         assertEvent(event, SystemIOMachineEvents.done_checkReadWrite) | ||||
|         return event.output | ||||
|       }, | ||||
|     }), | ||||
|   }, | ||||
|   actors: { | ||||
|     [SystemIOMachineActors.readFoldersFromProjectDirectory]: fromPromise( | ||||
|       async ({ input: context }: { input: SystemIOContext }) => { | ||||
|         const folders: Project[] = [] | ||||
|         return folders | ||||
|       } | ||||
|     ), | ||||
|     [SystemIOMachineActors.createProject]: fromPromise( | ||||
|       async ({ | ||||
|         input: { context, requestedProjectName }, | ||||
|       }: { | ||||
|         input: { context: SystemIOContext; requestedProjectName: string } | ||||
|       }) => { | ||||
|         return { message: '', name: '' } | ||||
|       } | ||||
|     ), | ||||
|     [SystemIOMachineActors.deleteProject]: fromPromise( | ||||
|       async ({ | ||||
|         input: { context, requestedProjectName }, | ||||
|       }: { | ||||
|         input: { context: SystemIOContext; requestedProjectName: string } | ||||
|       }) => { | ||||
|         return { message: '', name: '' } | ||||
|       } | ||||
|     ), | ||||
|     [SystemIOMachineActors.renameProject]: fromPromise( | ||||
|       async ({ | ||||
|         input: { context, requestedProjectName, projectName }, | ||||
|       }: { | ||||
|         input: { | ||||
|           context: SystemIOContext | ||||
|           requestedProjectName: string | ||||
|           projectName: string | ||||
|         } | ||||
|       }): Promise<{ message: string; newName: string; oldName: string }> => { | ||||
|         return { message: '', newName: '', oldName: '' } | ||||
|       } | ||||
|     ), | ||||
|     [SystemIOMachineActors.createKCLFile]: fromPromise( | ||||
|       async ({ | ||||
|         input, | ||||
|       }: { | ||||
|         input: { | ||||
|           context: SystemIOContext | ||||
|           requestedProjectName: string | ||||
|           requestedFileName: string | ||||
|           requestedCode: string | ||||
|           rootContext: AppMachineContext | ||||
|         } | ||||
|       }): Promise<{ | ||||
|         message: string | ||||
|         fileName: string | ||||
|         projectName: string | ||||
|       }> => { | ||||
|         return { message: '', fileName: '', projectName: '' } | ||||
|       } | ||||
|     ), | ||||
|     [SystemIOMachineActors.checkReadWrite]: fromPromise( | ||||
|       async ({ | ||||
|         input: { context, requestedProjectDirectoryPath }, | ||||
|       }: { | ||||
|         input: { | ||||
|           context: SystemIOContext | ||||
|           requestedProjectDirectoryPath: string | ||||
|         } | ||||
|       }): Promise<{ value: boolean; error: unknown }> => { | ||||
|         return { value: true, error: undefined } | ||||
|       } | ||||
|     ), | ||||
|   }, | ||||
| }).createMachine({ | ||||
|   initial: SystemIOMachineStates.idle, | ||||
|   // Remember, this machine and change its projectDirectory at any point | ||||
|   // '' will be no project directory, aka clear this machine out! | ||||
|   // To be the absolute root of someones computer we should take the string of path.resolve() in node.js which is different for each OS | ||||
|   context: () => ({ | ||||
|     folders: [], | ||||
|     defaultProjectFolderName: DEFAULT_PROJECT_NAME, | ||||
|     projectDirectoryPath: NO_PROJECT_DIRECTORY, | ||||
|     hasListedProjects: false, | ||||
|     requestedProjectName: { name: NO_PROJECT_DIRECTORY }, | ||||
|     requestedFileName: { | ||||
|       project: NO_PROJECT_DIRECTORY, | ||||
|       file: NO_PROJECT_DIRECTORY, | ||||
|     }, | ||||
|     canReadWriteProjectDirectory: { value: true, error: undefined }, | ||||
|     clearURLParams: { value: false }, | ||||
|   }), | ||||
|   states: { | ||||
|     [SystemIOMachineStates.idle]: { | ||||
|       on: { | ||||
|         // on can be an action | ||||
|         [SystemIOMachineEvents.readFoldersFromProjectDirectory]: { | ||||
|           target: SystemIOMachineStates.readingFolders, | ||||
|         }, | ||||
|         [SystemIOMachineEvents.setProjectDirectoryPath]: { | ||||
|           target: SystemIOMachineStates.checkingReadWrite, | ||||
|           actions: [SystemIOMachineActions.setProjectDirectoryPath], | ||||
|         }, | ||||
|         [SystemIOMachineEvents.navigateToProject]: { | ||||
|           actions: [SystemIOMachineActions.setRequestedProjectName], | ||||
|         }, | ||||
|         [SystemIOMachineEvents.navigateToFile]: { | ||||
|           actions: [SystemIOMachineActions.setRequestedFileName], | ||||
|         }, | ||||
|         [SystemIOMachineEvents.createProject]: { | ||||
|           target: SystemIOMachineStates.creatingProject, | ||||
|         }, | ||||
|         [SystemIOMachineEvents.renameProject]: { | ||||
|           target: SystemIOMachineStates.renamingProject, | ||||
|         }, | ||||
|         [SystemIOMachineEvents.deleteProject]: { | ||||
|           target: SystemIOMachineStates.deletingProject, | ||||
|         }, | ||||
|         [SystemIOMachineEvents.createKCLFile]: { | ||||
|           target: SystemIOMachineStates.creatingKCLFile, | ||||
|         }, | ||||
|         [SystemIOMachineEvents.setDefaultProjectFolderName]: { | ||||
|           actions: [SystemIOMachineActions.setDefaultProjectFolderName], | ||||
|         }, | ||||
|         [SystemIOMachineEvents.importFileFromURL]: { | ||||
|           target: SystemIOMachineStates.importFileFromURL, | ||||
|         }, | ||||
|       }, | ||||
|     }, | ||||
|     [SystemIOMachineStates.readingFolders]: { | ||||
|       invoke: { | ||||
|         id: SystemIOMachineActors.readFoldersFromProjectDirectory, | ||||
|         src: SystemIOMachineActors.readFoldersFromProjectDirectory, | ||||
|         input: ({ context }) => { | ||||
|           return context | ||||
|         }, | ||||
|         onDone: { | ||||
|           target: SystemIOMachineStates.idle, | ||||
|           actions: [ | ||||
|             SystemIOMachineActions.setFolders, | ||||
|             assign({ hasListedProjects: true }), | ||||
|           ], | ||||
|         }, | ||||
|         onError: { | ||||
|           target: SystemIOMachineStates.idle, | ||||
|         }, | ||||
|       }, | ||||
|     }, | ||||
|     [SystemIOMachineStates.creatingProject]: { | ||||
|       invoke: { | ||||
|         id: SystemIOMachineActors.createProject, | ||||
|         src: SystemIOMachineActors.createProject, | ||||
|         input: ({ context, event }) => { | ||||
|           assertEvent(event, SystemIOMachineEvents.createProject) | ||||
|           return { | ||||
|             context, | ||||
|             requestedProjectName: event.data.requestedProjectName, | ||||
|           } | ||||
|         }, | ||||
|         onDone: { | ||||
|           target: SystemIOMachineStates.readingFolders, | ||||
|           actions: [ | ||||
|             assign({ | ||||
|               requestedProjectName: ({ event }) => { | ||||
|                 return { name: event.output.name } | ||||
|               }, | ||||
|             }), | ||||
|             SystemIOMachineActions.toastSuccess, | ||||
|           ], | ||||
|         }, | ||||
|         onError: { | ||||
|           target: SystemIOMachineStates.idle, | ||||
|           actions: [SystemIOMachineActions.toastError], | ||||
|         }, | ||||
|       }, | ||||
|     }, | ||||
|     [SystemIOMachineStates.renamingProject]: { | ||||
|       invoke: { | ||||
|         id: SystemIOMachineActors.renameProject, | ||||
|         src: SystemIOMachineActors.renameProject, | ||||
|         input: ({ context, event }) => { | ||||
|           assertEvent(event, SystemIOMachineEvents.renameProject) | ||||
|           return { | ||||
|             context, | ||||
|             requestedProjectName: event.data.requestedProjectName, | ||||
|             projectName: event.data.projectName, | ||||
|           } | ||||
|         }, | ||||
|         onDone: { | ||||
|           target: SystemIOMachineStates.readingFolders, | ||||
|           actions: [SystemIOMachineActions.toastSuccess], | ||||
|         }, | ||||
|         onError: { | ||||
|           target: SystemIOMachineStates.idle, | ||||
|           actions: [SystemIOMachineActions.toastError], | ||||
|         }, | ||||
|       }, | ||||
|     }, | ||||
|     [SystemIOMachineStates.deletingProject]: { | ||||
|       invoke: { | ||||
|         id: SystemIOMachineActors.deleteProject, | ||||
|         src: SystemIOMachineActors.deleteProject, | ||||
|         input: ({ context, event }) => { | ||||
|           assertEvent(event, SystemIOMachineEvents.deleteProject) | ||||
|           return { | ||||
|             context, | ||||
|             requestedProjectName: event.data.requestedProjectName, | ||||
|           } | ||||
|         }, | ||||
|         onDone: { | ||||
|           target: SystemIOMachineStates.readingFolders, | ||||
|           actions: [SystemIOMachineActions.toastSuccess], | ||||
|         }, | ||||
|         onError: { | ||||
|           target: SystemIOMachineStates.idle, | ||||
|           actions: [SystemIOMachineActions.toastError], | ||||
|         }, | ||||
|       }, | ||||
|     }, | ||||
|     [SystemIOMachineStates.creatingKCLFile]: { | ||||
|       invoke: { | ||||
|         id: SystemIOMachineActors.createKCLFile, | ||||
|         src: SystemIOMachineActors.createKCLFile, | ||||
|         input: ({ context, event, self }) => { | ||||
|           assertEvent(event, SystemIOMachineEvents.createKCLFile) | ||||
|           return { | ||||
|             context, | ||||
|             requestedProjectName: event.data.requestedProjectName, | ||||
|             requestedFileName: event.data.requestedFileName, | ||||
|             requestedCode: event.data.requestedCode, | ||||
|             rootContext: self.system.get('root').getSnapshot().context, | ||||
|           } | ||||
|         }, | ||||
|         onDone: { | ||||
|           target: SystemIOMachineStates.idle, | ||||
|         }, | ||||
|         onError: { | ||||
|           target: SystemIOMachineStates.idle, | ||||
|           actions: [SystemIOMachineActions.toastError], | ||||
|         }, | ||||
|       }, | ||||
|     }, | ||||
|     [SystemIOMachineStates.importFileFromURL]: { | ||||
|       invoke: { | ||||
|         id: SystemIOMachineActors.importFileFromURL, | ||||
|         src: SystemIOMachineActors.createKCLFile, | ||||
|         input: ({ context, event, self }) => { | ||||
|           assertEvent(event, SystemIOMachineEvents.importFileFromURL) | ||||
|           return { | ||||
|             context, | ||||
|             requestedProjectName: event.data.requestedProjectName, | ||||
|             requestedFileName: event.data.requestedFileName, | ||||
|             requestedCode: event.data.requestedCode, | ||||
|             rootContext: self.system.get('root').getSnapshot().context, | ||||
|           } | ||||
|         }, | ||||
|         onDone: { | ||||
|           target: SystemIOMachineStates.readingFolders, | ||||
|           // Clear on web? not desktop | ||||
|           actions: [ | ||||
|             assign({ | ||||
|               requestedFileName: ({ context, event }) => { | ||||
|                 assertEvent(event, SystemIOMachineEvents.done_importFileFromURL) | ||||
|                 // Not the entire path | ||||
|                 return { | ||||
|                   project: event.output.projectName, | ||||
|                   file: event.output.fileName + '.kcl', | ||||
|                 } | ||||
|               }, | ||||
|             }), | ||||
|             assign({ clearURLParams: { value: true } }), | ||||
|           ], | ||||
|         }, | ||||
|         onError: { | ||||
|           target: SystemIOMachineStates.idle, | ||||
|           actions: [SystemIOMachineActions.toastError], | ||||
|         }, | ||||
|       }, | ||||
|     }, | ||||
|     [SystemIOMachineStates.checkingReadWrite]: { | ||||
|       invoke: { | ||||
|         id: SystemIOMachineActors.checkReadWrite, | ||||
|         src: SystemIOMachineActors.checkReadWrite, | ||||
|         input: ({ context, event }) => { | ||||
|           assertEvent(event, SystemIOMachineEvents.setProjectDirectoryPath) | ||||
|           return { | ||||
|             context, | ||||
|             requestedProjectDirectoryPath: | ||||
|               event.data.requestedProjectDirectoryPath, | ||||
|           } | ||||
|         }, | ||||
|         onDone: { | ||||
|           target: SystemIOMachineStates.readingFolders, | ||||
|         }, | ||||
|         onError: { | ||||
|           target: SystemIOMachineStates.readingFolders, | ||||
|           actions: [SystemIOMachineActions.toastError], | ||||
|         }, | ||||
|       }, | ||||
|     }, | ||||
|   }, | ||||
| }) | ||||
|  | ||||
| // Watcher handler | ||||
| // look at projectDirectory useEffect then send this event if it changes or if we need to do this? | ||||
| // The handler needs to live somewhere... aka the provider? | ||||
							
								
								
									
										233
									
								
								src/machines/systemIO/systemIOMachineDesktop.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										233
									
								
								src/machines/systemIO/systemIOMachineDesktop.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,233 @@ | ||||
| import { | ||||
|   createNewProjectDirectory, | ||||
|   getProjectInfo, | ||||
|   mkdirOrNOOP, | ||||
|   readAppSettingsFile, | ||||
|   renameProjectDirectory, | ||||
| } from '@src/lib/desktop' | ||||
| import { | ||||
|   doesProjectNameNeedInterpolated, | ||||
|   getNextFileName, | ||||
|   getNextProjectIndex, | ||||
|   getUniqueProjectName, | ||||
|   interpolateProjectNameWithIndex, | ||||
| } from '@src/lib/desktopFS' | ||||
| import type { Project } from '@src/lib/project' | ||||
| import { systemIOMachine } from '@src/machines/systemIO/systemIOMachine' | ||||
| import type { SystemIOContext } from '@src/machines/systemIO/utils' | ||||
| import { | ||||
|   NO_PROJECT_DIRECTORY, | ||||
|   SystemIOMachineActors, | ||||
| } from '@src/machines/systemIO/utils' | ||||
| import { fromPromise } from 'xstate' | ||||
| import type { AppMachineContext } from '@src/lib/types' | ||||
|  | ||||
| export const systemIOMachineDesktop = systemIOMachine.provide({ | ||||
|   actors: { | ||||
|     [SystemIOMachineActors.readFoldersFromProjectDirectory]: fromPromise( | ||||
|       async ({ input: context }: { input: SystemIOContext }) => { | ||||
|         const projects = [] | ||||
|         const projectDirectoryPath = context.projectDirectoryPath | ||||
|         if (projectDirectoryPath === NO_PROJECT_DIRECTORY) { | ||||
|           // TODO | ||||
|           return [] | ||||
|         } | ||||
|         await mkdirOrNOOP(projectDirectoryPath) | ||||
|         // Gotcha: readdir will list all folders at this project directory even if you do not have readwrite access on the directory path | ||||
|         const entries = await window.electron.readdir(projectDirectoryPath) | ||||
|         const { value: canReadWriteProjectDirectory } = | ||||
|           await window.electron.canReadWriteDirectory(projectDirectoryPath) | ||||
|  | ||||
|         for (let entry of entries) { | ||||
|           // Skip directories that start with a dot | ||||
|           if (entry.startsWith('.')) { | ||||
|             continue | ||||
|           } | ||||
|           const projectPath = window.electron.path.join( | ||||
|             projectDirectoryPath, | ||||
|             entry | ||||
|           ) | ||||
|  | ||||
|           // if it's not a directory ignore. | ||||
|           // Gotcha: statIsDirectory will work even if you do not have read write permissions on the project path | ||||
|           const isDirectory = await window.electron.statIsDirectory(projectPath) | ||||
|           if (!isDirectory) { | ||||
|             continue | ||||
|           } | ||||
|           const project: Project = await getProjectInfo(projectPath) | ||||
|           if ( | ||||
|             project.kcl_file_count === 0 && | ||||
|             project.readWriteAccess && | ||||
|             canReadWriteProjectDirectory | ||||
|           ) { | ||||
|             continue | ||||
|           } | ||||
|           projects.push(project) | ||||
|         } | ||||
|         return projects | ||||
|       } | ||||
|     ), | ||||
|     [SystemIOMachineActors.createProject]: fromPromise( | ||||
|       async ({ | ||||
|         input, | ||||
|       }: { | ||||
|         input: { context: SystemIOContext; requestedProjectName: string } | ||||
|       }) => { | ||||
|         const folders = input.context.folders | ||||
|         const requestedProjectName = input.requestedProjectName | ||||
|         const uniqueName = getUniqueProjectName(requestedProjectName, folders) | ||||
|         await createNewProjectDirectory(uniqueName) | ||||
|         return { | ||||
|           message: `Successfully created "${uniqueName}"`, | ||||
|           name: uniqueName, | ||||
|         } | ||||
|       } | ||||
|     ), | ||||
|     [SystemIOMachineActors.renameProject]: fromPromise( | ||||
|       async ({ | ||||
|         input, | ||||
|       }: { | ||||
|         input: { | ||||
|           context: SystemIOContext | ||||
|           requestedProjectName: string | ||||
|           projectName: string | ||||
|         } | ||||
|       }) => { | ||||
|         const folders = input.context.folders | ||||
|         const requestedProjectName = input.requestedProjectName | ||||
|         const projectName = input.projectName | ||||
|         let newProjectName: string = requestedProjectName | ||||
|         if (doesProjectNameNeedInterpolated(requestedProjectName)) { | ||||
|           const nextIndex = getNextProjectIndex(requestedProjectName, folders) | ||||
|           newProjectName = interpolateProjectNameWithIndex( | ||||
|             requestedProjectName, | ||||
|             nextIndex | ||||
|           ) | ||||
|         } | ||||
|  | ||||
|         // Toast an error if the project name is taken | ||||
|         if (folders.find((p) => p.name === newProjectName)) { | ||||
|           return Promise.reject( | ||||
|             new Error(`Project with name "${newProjectName}" already exists`) | ||||
|           ) | ||||
|         } | ||||
|  | ||||
|         await renameProjectDirectory( | ||||
|           window.electron.path.join( | ||||
|             input.context.projectDirectoryPath, | ||||
|             projectName | ||||
|           ), | ||||
|           newProjectName | ||||
|         ) | ||||
|  | ||||
|         return { | ||||
|           message: `Successfully renamed "${projectName}" to "${newProjectName}"`, | ||||
|           oldName: projectName, | ||||
|           newName: newProjectName, | ||||
|         } | ||||
|       } | ||||
|     ), | ||||
|     [SystemIOMachineActors.deleteProject]: fromPromise( | ||||
|       async ({ | ||||
|         input, | ||||
|       }: { | ||||
|         input: { context: SystemIOContext; requestedProjectName: string } | ||||
|       }) => { | ||||
|         await window.electron.rm( | ||||
|           window.electron.path.join( | ||||
|             input.context.projectDirectoryPath, | ||||
|             input.requestedProjectName | ||||
|           ), | ||||
|           { | ||||
|             recursive: true, | ||||
|           } | ||||
|         ) | ||||
|  | ||||
|         return { | ||||
|           message: `Successfully deleted "${input.requestedProjectName}"`, | ||||
|           name: input.requestedProjectName, | ||||
|         } | ||||
|       } | ||||
|     ), | ||||
|     [SystemIOMachineActors.createKCLFile]: fromPromise( | ||||
|       async ({ | ||||
|         input, | ||||
|       }: { | ||||
|         input: { | ||||
|           context: SystemIOContext | ||||
|           requestedProjectName: string | ||||
|           requestedFileName: string | ||||
|           requestedCode: string | ||||
|           rootContext: AppMachineContext | ||||
|         } | ||||
|       }) => { | ||||
|         const requestedProjectName = input.requestedProjectName | ||||
|         const requestedFileName = input.requestedFileName | ||||
|         const requestedCode = input.requestedCode | ||||
|         const folders = input.context.folders | ||||
|  | ||||
|         let newProjectName = requestedProjectName | ||||
|  | ||||
|         if (!newProjectName) { | ||||
|           newProjectName = getUniqueProjectName( | ||||
|             input.context.defaultProjectFolderName, | ||||
|             input.context.folders | ||||
|           ) | ||||
|         } | ||||
|  | ||||
|         const needsInterpolated = | ||||
|           doesProjectNameNeedInterpolated(newProjectName) | ||||
|         if (needsInterpolated) { | ||||
|           const nextIndex = getNextProjectIndex(newProjectName, folders) | ||||
|           newProjectName = interpolateProjectNameWithIndex( | ||||
|             newProjectName, | ||||
|             nextIndex | ||||
|           ) | ||||
|         } | ||||
|  | ||||
|         const baseDir = window.electron.join( | ||||
|           input.context.projectDirectoryPath, | ||||
|           newProjectName | ||||
|         ) | ||||
|         const { name: newFileName } = getNextFileName({ | ||||
|           entryName: requestedFileName, | ||||
|           baseDir, | ||||
|         }) | ||||
|         const configuration = await readAppSettingsFile() | ||||
|  | ||||
|         // Create the project around the file if newProject | ||||
|         await createNewProjectDirectory( | ||||
|           newProjectName, | ||||
|           requestedCode, | ||||
|           configuration, | ||||
|           newFileName | ||||
|         ) | ||||
|  | ||||
|         return { | ||||
|           message: 'File created successfully', | ||||
|           fileName: input.requestedFileName, | ||||
|           projectName: newProjectName, | ||||
|         } | ||||
|       } | ||||
|     ), | ||||
|     [SystemIOMachineActors.checkReadWrite]: fromPromise( | ||||
|       async ({ | ||||
|         input, | ||||
|       }: { | ||||
|         input: { | ||||
|           context: SystemIOContext | ||||
|           requestedProjectDirectoryPath: string | ||||
|         } | ||||
|       }) => { | ||||
|         const requestProjectDirectoryPath = input.requestedProjectDirectoryPath | ||||
|         if (!requestProjectDirectoryPath) { | ||||
|           return { value: true, error: undefined } | ||||
|         } | ||||
|         const result = await window.electron.canReadWriteDirectory( | ||||
|           requestProjectDirectoryPath | ||||
|         ) | ||||
|         return result | ||||
|       } | ||||
|     ), | ||||
|   }, | ||||
| }) | ||||
							
								
								
									
										50
									
								
								src/machines/systemIO/systemIOMachineWeb.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								src/machines/systemIO/systemIOMachineWeb.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,50 @@ | ||||
| import { systemIOMachine } from '@src/machines/systemIO/systemIOMachine' | ||||
| import type { SystemIOContext } from '@src/machines/systemIO/utils' | ||||
| import { SystemIOMachineActors } from '@src/machines/systemIO/utils' | ||||
| import { fromPromise } from 'xstate' | ||||
| import { newKclFile } from '@src/lang/project' | ||||
| import { readLocalStorageProjectSettingsFile } from '@src/lib/settings/settingsUtils' | ||||
| import { err } from '@src/lib/trap' | ||||
| import { DEFAULT_DEFAULT_LENGTH_UNIT } from '@src/lib/constants' | ||||
| import type { AppMachineContext } from '@src/lib/types' | ||||
|  | ||||
| export const systemIOMachineWeb = systemIOMachine.provide({ | ||||
|   actors: { | ||||
|     [SystemIOMachineActors.createKCLFile]: fromPromise( | ||||
|       async ({ | ||||
|         input, | ||||
|       }: { | ||||
|         input: { | ||||
|           context: SystemIOContext | ||||
|           requestedProjectName: string | ||||
|           requestedFileName: string | ||||
|           requestedCode: string | ||||
|           rootContext: AppMachineContext | ||||
|         } | ||||
|       }) => { | ||||
|         // Browser version doesn't navigate, just overwrites the current file | ||||
|         // clearImportSearchParams() | ||||
|         const projectSettings = readLocalStorageProjectSettingsFile() | ||||
|         if (err(projectSettings)) { | ||||
|           return Promise.reject( | ||||
|             'Unable to read project settings from local storage' | ||||
|           ) | ||||
|         } | ||||
|         const codeToWrite = newKclFile( | ||||
|           input.requestedCode, | ||||
|           projectSettings?.settings?.modeling?.base_unit || | ||||
|             DEFAULT_DEFAULT_LENGTH_UNIT | ||||
|         ) | ||||
|         if (err(codeToWrite)) return Promise.reject(codeToWrite) | ||||
|         input.rootContext.codeManager.updateCodeStateEditor(codeToWrite) | ||||
|         await input.rootContext.codeManager.writeToFile() | ||||
|         await input.rootContext.kclManager.executeCode() | ||||
|         return { | ||||
|           message: 'File overwritten successfully', | ||||
|           fileName: input.requestedFileName, | ||||
|           projectName: '', | ||||
|         } | ||||
|       } | ||||
|     ), | ||||
|   }, | ||||
| }) | ||||
							
								
								
									
										73
									
								
								src/machines/systemIO/utils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								src/machines/systemIO/utils.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,73 @@ | ||||
| import type { Project } from '@src/lib/project' | ||||
|  | ||||
| export enum SystemIOMachineActors { | ||||
|   readFoldersFromProjectDirectory = 'read folders from project directory', | ||||
|   setProjectDirectoryPath = 'set project directory path', | ||||
|   createProject = 'create project', | ||||
|   renameProject = 'rename project', | ||||
|   deleteProject = 'delete project', | ||||
|   createKCLFile = 'create kcl file', | ||||
|   checkReadWrite = 'check read write', | ||||
|   importFileFromURL = 'import file from URL', | ||||
| } | ||||
|  | ||||
| export enum SystemIOMachineStates { | ||||
|   idle = 'idle', | ||||
|   readingFolders = 'readingFolders', | ||||
|   settingProjectDirectoryPath = 'settingProjectDirectoryPath', | ||||
|   creatingProject = 'creatingProject', | ||||
|   renamingProject = 'renamingProject', | ||||
|   deletingProject = 'deletingProject', | ||||
|   creatingKCLFile = 'creatingKCLFile', | ||||
|   checkingReadWrite = 'checkingReadWrite', | ||||
|   importFileFromURL = 'importFileFromURL', | ||||
| } | ||||
|  | ||||
| const donePrefix = 'xstate.done.actor.' | ||||
|  | ||||
| export enum SystemIOMachineEvents { | ||||
|   readFoldersFromProjectDirectory = 'read folders from project directory', | ||||
|   done_readFoldersFromProjectDirectory = donePrefix + | ||||
|     'read folders from project directory', | ||||
|   setProjectDirectoryPath = 'set project directory path', | ||||
|   navigateToProject = 'navigate to project', | ||||
|   navigateToFile = 'navigate to file', | ||||
|   createProject = 'create project', | ||||
|   renameProject = 'rename project', | ||||
|   deleteProject = 'delete project', | ||||
|   createKCLFile = 'create kcl file', | ||||
|   setDefaultProjectFolderName = 'set default project folder name', | ||||
|   done_checkReadWrite = donePrefix + 'check read write', | ||||
|   importFileFromURL = 'import file from URL', | ||||
|   done_importFileFromURL = donePrefix + 'import file from URL', | ||||
| } | ||||
|  | ||||
| export enum SystemIOMachineActions { | ||||
|   setFolders = 'set folders', | ||||
|   setProjectDirectoryPath = 'set project directory path', | ||||
|   setRequestedProjectName = 'set requested project name', | ||||
|   setRequestedFileName = 'set requested file name', | ||||
|   setDefaultProjectFolderName = 'set default project folder name', | ||||
|   toastSuccess = 'toastSuccess', | ||||
|   toastError = 'toastError', | ||||
|   setReadWriteProjectDirectory = 'set read write project directory', | ||||
| } | ||||
|  | ||||
| export const NO_PROJECT_DIRECTORY = '' | ||||
|  | ||||
| export type SystemIOContext = { | ||||
|   // Only store folders under the projectDirectory, do not maintain folders outside this directory | ||||
|   folders: Project[] | ||||
|   // For this machines runtime, this is the default string when creating a project | ||||
|   // A project is defined by creating a folder at the one level below the working project directory | ||||
|   defaultProjectFolderName: string | ||||
|   // working project directory that stores all the project folders | ||||
|   projectDirectoryPath: string | ||||
|   // has the application gone through the initialization of systemIOMachine at least once. | ||||
|   // this is required to prevent chokidar from spamming invalid events during initialization. | ||||
|   hasListedProjects: boolean | ||||
|   requestedProjectName: { name: string } | ||||
|   requestedFileName: { project: string; file: string } | ||||
|   canReadWriteProjectDirectory: { value: boolean; error: unknown } | ||||
|   clearURLParams: { value: boolean } | ||||
| } | ||||
| @ -10,7 +10,7 @@ import { | ||||
| } from '@src/lib/singletons' | ||||
| import { reportRejection } from '@src/lib/trap' | ||||
| import { uuidv4 } from '@src/lib/utils' | ||||
| import { authActor, settingsActor } from '@src/machines/appMachine' | ||||
| import { authActor, settingsActor } from '@src/lib/singletons' | ||||
| import { commandBarActor } from '@src/machines/commandBarMachine' | ||||
| import type { WebContentSendPayload } from '@src/menu/channels' | ||||
| import type { NavigateFunction } from 'react-router-dom' | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| import type { FormEvent } from 'react' | ||||
| import { useEffect, useRef, useState } from 'react' | ||||
| import { useEffect, useRef } from 'react' | ||||
| import { toast } from 'react-hot-toast' | ||||
| import { useHotkeys } from 'react-hotkeys-hook' | ||||
| import { Link, useNavigate, useSearchParams } from 'react-router-dom' | ||||
| @ -15,7 +15,6 @@ import { | ||||
| } from '@src/components/ProjectSearchBar' | ||||
| import { useCreateFileLinkQuery } from '@src/hooks/useCreateFileLinkQueryWatcher' | ||||
| import { useMenuListener } from '@src/hooks/useMenu' | ||||
| import { useProjectsContext } from '@src/hooks/useProjectsContext' | ||||
| import { isDesktop } from '@src/lib/isDesktop' | ||||
| import { PATHS } from '@src/lib/paths' | ||||
| import { markOnce } from '@src/lib/performance' | ||||
| @ -27,21 +26,24 @@ import { | ||||
|   getSortIcon, | ||||
| } from '@src/lib/sorting' | ||||
| import { reportRejection } from '@src/lib/trap' | ||||
| import { authActor, useSettings } from '@src/machines/appMachine' | ||||
| import { authActor, systemIOActor, useSettings } from '@src/lib/singletons' | ||||
| import { commandBarActor } from '@src/machines/commandBarMachine' | ||||
| import { | ||||
|   useCanReadWriteProjectDirectory, | ||||
|   useFolders, | ||||
|   useState as useSystemIOState, | ||||
| } from '@src/machines/systemIO/hooks' | ||||
| import { | ||||
|   SystemIOMachineEvents, | ||||
|   SystemIOMachineStates, | ||||
| } from '@src/machines/systemIO/utils' | ||||
| import type { WebContentSendPayload } from '@src/menu/channels' | ||||
|  | ||||
| // This route only opens in the desktop context for now, | ||||
| // as defined in Router.tsx, so we can use the desktop APIs and types. | ||||
| const Home = () => { | ||||
|   const { state, send } = useProjectsContext() | ||||
|   const [readWriteProjectDir, setReadWriteProjectDir] = useState<{ | ||||
|     value: boolean | ||||
|     error: unknown | ||||
|   }>({ | ||||
|     value: true, | ||||
|     error: undefined, | ||||
|   }) | ||||
|   const state = useSystemIOState() | ||||
|   const readWriteProjectDir = useCanReadWriteProjectDirectory() | ||||
|  | ||||
|   // Only create the native file menus on desktop | ||||
|   useEffect(() => { | ||||
| @ -156,40 +158,13 @@ const Home = () => { | ||||
|     } | ||||
|   ) | ||||
|   const ref = useRef<HTMLDivElement>(null) | ||||
|  | ||||
|   const projects = state?.context.projects ?? [] | ||||
|   const projects = useFolders() | ||||
|   const [searchParams, setSearchParams] = useSearchParams() | ||||
|   const { searchResults, query, setQuery } = useProjectSearch(projects) | ||||
|   const sort = searchParams.get('sort_by') ?? 'modified:desc' | ||||
|  | ||||
|   const isSortByModified = sort?.includes('modified') || !sort || sort === null | ||||
|  | ||||
|   // Update the default project name and directory in the home machine | ||||
|   // when the settings change | ||||
|   useEffect(() => { | ||||
|     send({ | ||||
|       type: 'assign', | ||||
|       data: { | ||||
|         defaultProjectName: settings.projects.defaultProjectName.current, | ||||
|         defaultDirectory: settings.app.projectDirectory.current, | ||||
|       }, | ||||
|     }) | ||||
|  | ||||
|     // Must be a truthy string, not '' or null or undefined | ||||
|     if (settings.app.projectDirectory.current) { | ||||
|       window.electron | ||||
|         .canReadWriteDirectory(settings.app.projectDirectory.current) | ||||
|         .then((res) => { | ||||
|           setReadWriteProjectDir(res) | ||||
|         }) | ||||
|         .catch(reportRejection) | ||||
|     } | ||||
|   }, [ | ||||
|     settings.app.projectDirectory.current, | ||||
|     settings.projects.defaultProjectName.current, | ||||
|     send, | ||||
|   ]) | ||||
|  | ||||
|   async function handleRenameProject( | ||||
|     e: FormEvent<HTMLFormElement>, | ||||
|     project: Project | ||||
| @ -204,17 +179,20 @@ const Home = () => { | ||||
|     } | ||||
|  | ||||
|     if (newProjectName !== project.name) { | ||||
|       send({ | ||||
|         type: 'Rename project', | ||||
|         data: { oldName: project.name, newName: newProjectName as string }, | ||||
|       systemIOActor.send({ | ||||
|         type: SystemIOMachineEvents.renameProject, | ||||
|         data: { | ||||
|           requestedProjectName: String(newProjectName), | ||||
|           projectName: project.name, | ||||
|         }, | ||||
|       }) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   async function handleDeleteProject(project: Project) { | ||||
|     send({ | ||||
|       type: 'Delete project', | ||||
|       data: { name: project.name || '' }, | ||||
|     systemIOActor.send({ | ||||
|       type: SystemIOMachineEvents.deleteProject, | ||||
|       data: { requestedProjectName: project.name }, | ||||
|     }) | ||||
|   } | ||||
|   /** Type narrowing function of unknown error to a string */ | ||||
| @ -246,9 +224,6 @@ const Home = () => { | ||||
|                     data: { | ||||
|                       groupId: 'projects', | ||||
|                       name: 'Create project', | ||||
|                       argDefaultValues: { | ||||
|                         name: settings.projects.defaultProjectName.current, | ||||
|                       }, | ||||
|                     }, | ||||
|                   }) | ||||
|                 } | ||||
| @ -345,7 +320,7 @@ const Home = () => { | ||||
|           data-testid="home-section" | ||||
|           className="flex-1 overflow-y-auto pr-2 pb-24" | ||||
|         > | ||||
|           {state?.matches('Reading projects') ? ( | ||||
|           {state?.matches(SystemIOMachineStates.readingFolders) ? ( | ||||
|             <Loading>Loading your Projects...</Loading> | ||||
|           ) : ( | ||||
|             <> | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| import { SettingsSection } from '@src/components/Settings/SettingsSection' | ||||
| import type { CameraSystem } from '@src/lib/cameraControls' | ||||
| import { cameraMouseDragGuards, cameraSystems } from '@src/lib/cameraControls' | ||||
| import { settingsActor, useSettings } from '@src/machines/appMachine' | ||||
| import { settingsActor, useSettings } from '@src/lib/singletons' | ||||
| import { onboardingPaths } from '@src/routes/Onboarding/paths' | ||||
|  | ||||
| import { | ||||
|  | ||||
| @ -13,7 +13,7 @@ import { codeManager, kclManager } from '@src/lib/singletons' | ||||
| import { Themes, getSystemTheme } from '@src/lib/theme' | ||||
| import { reportRejection } from '@src/lib/trap' | ||||
| import type { IndexLoaderData } from '@src/lib/types' | ||||
| import { useSettings } from '@src/machines/appMachine' | ||||
| import { useSettings } from '@src/lib/singletons' | ||||
| import { onboardingPaths } from '@src/routes/Onboarding/paths' | ||||
|  | ||||
| import { OnboardingButtons, useDemoCode } from '@src/routes/Onboarding/utils' | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| import { bracketThicknessCalculationLine } from '@src/lib/exampleKcl' | ||||
| import { isDesktop } from '@src/lib/isDesktop' | ||||
| import { Themes, getSystemTheme } from '@src/lib/theme' | ||||
| import { useSettings } from '@src/machines/appMachine' | ||||
| import { useSettings } from '@src/lib/singletons' | ||||
| import { onboardingPaths } from '@src/routes/Onboarding/paths' | ||||
|  | ||||
| import { OnboardingButtons, useDemoCode } from '@src/routes/Onboarding/utils' | ||||
|  | ||||
| @ -3,7 +3,7 @@ import { faArrowRight, faXmark } from '@fortawesome/free-solid-svg-icons' | ||||
| import { ActionButton } from '@src/components/ActionButton' | ||||
| import { SettingsSection } from '@src/components/Settings/SettingsSection' | ||||
| import { type BaseUnit, baseUnitsUnion } from '@src/lib/settings/settingsTypes' | ||||
| import { settingsActor, useSettings } from '@src/machines/appMachine' | ||||
| import { settingsActor, useSettings } from '@src/lib/singletons' | ||||
| import { onboardingPaths } from '@src/routes/Onboarding/paths' | ||||
|  | ||||
| import { useDismiss, useNextClick } from '@src/routes/Onboarding/utils' | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| import { useEffect, useState } from 'react' | ||||
|  | ||||
| import { useUser } from '@src/machines/appMachine' | ||||
| import { useUser } from '@src/lib/singletons' | ||||
| import { onboardingPaths } from '@src/routes/Onboarding/paths' | ||||
|  | ||||
| import { OnboardingButtons } from '@src/routes/Onboarding/utils' | ||||
|  | ||||
| @ -14,7 +14,7 @@ import makeUrlPathRelative from '@src/lib/makeUrlPathRelative' | ||||
| import { PATHS } from '@src/lib/paths' | ||||
| import { codeManager, editorManager, kclManager } from '@src/lib/singletons' | ||||
| import { reportRejection, trap } from '@src/lib/trap' | ||||
| import { settingsActor } from '@src/machines/appMachine' | ||||
| import { settingsActor } from '@src/lib/singletons' | ||||
| import { onboardingRoutes } from '@src/routes/Onboarding' | ||||
| import { onboardingPaths } from '@src/routes/Onboarding/paths' | ||||
| import { parse, resultIsOk } from '@src/lang/wasm' | ||||
|  | ||||
| @ -14,7 +14,7 @@ import { PATHS } from '@src/lib/paths' | ||||
| import { Themes, getSystemTheme } from '@src/lib/theme' | ||||
| import { reportRejection } from '@src/lib/trap' | ||||
| import { toSync } from '@src/lib/utils' | ||||
| import { authActor, useSettings } from '@src/machines/appMachine' | ||||
| import { authActor, useSettings } from '@src/lib/singletons' | ||||
| import { APP_VERSION, IS_NIGHTLY } from '@src/routes/utils' | ||||
|  | ||||
| const subtleBorder = | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	