Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions src/editor/EditorContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useFileSystemBackedText } from "../fs/fs-hooks";
import { useProjectFileText } from "../project/project-hooks";
import { useSettings } from "../settings/settings";
import Editor from "./codemirror/CodeMirror";

Expand All @@ -12,7 +12,7 @@ interface EditorContainerProps {
*/
const EditorContainer = ({ filename }: EditorContainerProps) => {
const [{ fontSize, highlightCodeStructure }] = useSettings();
const [defaultValue, onFileChange] = useFileSystemBackedText(filename);
const [defaultValue, onFileChange] = useProjectFileText(filename);
return typeof defaultValue === "undefined" ? null : (
<Editor
defaultValue={defaultValue}
Expand Down
30 changes: 4 additions & 26 deletions src/files/FileRow.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { Button, HStack, IconButton } from "@chakra-ui/react";
import { useCallback } from "react";
import { RiDeleteBinLine, RiDownload2Line } from "react-icons/ri";
import useActionFeedback from "../common/use-action-feedback";
import { File, MAIN_FILE } from "../fs/fs";
import { useFileSystem } from "../fs/fs-hooks";
import { useProjectActions } from "../project/project-hooks";

interface FileRowProps {
projectName: string;
Expand All @@ -18,27 +16,7 @@ const FileRow = ({ projectName, value, onClick }: FileRowProps) => {
const { name } = value;
const isMainFile = name === MAIN_FILE;
const prettyName = isMainFile ? `${projectName} (${name})` : name;
const downloadName = isMainFile ? `${projectName}.py` : name;

const fs = useFileSystem();
const actionFeedback = useActionFeedback();
const handleDownload = useCallback(() => {
try {
const content = fs.read(name);
const blob = new Blob([content], { type: "text/x-python" });
saveAs(blob, downloadName);
} catch (e) {
actionFeedback.unexpectedError(e);
}
}, [fs, name, actionFeedback, downloadName]);

const handleDelete = useCallback(() => {
try {
fs.remove(name);
} catch (e) {
actionFeedback.unexpectedError(e);
}
}, [fs, name, actionFeedback]);
const actions = useProjectActions();

return (
<HStack justify="space-between" lineHeight={2}>
Expand All @@ -60,14 +38,14 @@ const FileRow = ({ projectName, value, onClick }: FileRowProps) => {
aria-label="Delete the file. The main Python file cannot be deleted."
variant="ghost"
disabled={isMainFile}
onClick={handleDelete}
onClick={() => actions.deleteFile(name)}
/>
<IconButton
size="sm"
icon={<RiDownload2Line />}
aria-label={`Download ${name}`}
variant="ghost"
onClick={handleDownload}
onClick={() => actions.downloadFile(name)}
/>
</HStack>
</HStack>
Expand Down
2 changes: 1 addition & 1 deletion src/files/FilesArea.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { List, ListItem, VStack } from "@chakra-ui/react";
import { useProject } from "../fs/fs-hooks";
import OpenButton from "../project/OpenButton";
import { useProject } from "../project/project-hooks";
import FileRow from "./FileRow";

interface FilesProps {
Expand Down
64 changes: 9 additions & 55 deletions src/fs/fs-hooks.ts
Original file line number Diff line number Diff line change
@@ -1,67 +1,21 @@
import { Text } from "@codemirror/state";
import {
createContext,
useCallback,
useContext,
useEffect,
useState,
} from "react";
import useIsUnmounted from "../common/use-is-unmounted";
import { EVENT_STATE, FileSystem, Project } from "./fs";
import { createContext, useContext } from "react";
import { FileSystem } from "./fs";

export const FileSystemContext = createContext<FileSystem | undefined>(
undefined
);

/**
* Hook exposing the file system.
*
* Most code should use the project instead of using the file system directly.
*
* @returns The file system.
*/
export const useFileSystem = () => {
const fs = useContext(FileSystemContext);
if (!fs) {
throw new Error("Missing provider");
}
return fs;
};

export const useProject = (): Project => {
const fs = useFileSystem();
const isUnmounted = useIsUnmounted();
const [state, setState] = useState<Project>(fs.state);
useEffect(() => {
const listener = (x: any) => {
if (!isUnmounted()) {
setState(x);
}
};
fs.on(EVENT_STATE, listener);
return () => {
fs.removeListener(EVENT_STATE, listener);
};
}, [fs, isUnmounted]);
return state;
};

/**
* Reads an initial value from the file system and synchronises back to it.
*/
export const useFileSystemBackedText = (
filename: string
): [Text | undefined, (text: Text) => void] => {
const fs = useFileSystem();
const [initialValue, setInitialValue] = useState<Text | undefined>();

useEffect(() => {
const string = fs.read(filename);
setInitialValue(Text.of(string.split("\n")));
}, [fs, filename]);

const handleChange = useCallback(
(text: Text) => {
const content = text.sliceString(0, undefined, "\n");
// If we fill up the FS it seems to cope and error when we
// ask for a hex.
fs.write(filename, content);
},
[fs, filename]
);

return [initialValue, handleChange];
};
49 changes: 7 additions & 42 deletions src/project/DeviceConnection.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import { HStack, Switch, Text, Tooltip } from "@chakra-ui/react";
import { useCallback } from "react";
import Separate, { br } from "../common/Separate";
import useActionFeedback, {
ActionFeedback,
} from "../common/use-action-feedback";
import { ConnectionStatus, WebUSBError } from "../device/device";
import { useConnectionStatus, useDevice } from "../device/device-hooks";
import { ConnectionStatus } from "../device/device";
import { useConnectionStatus } from "../device/device-hooks";
import DownloadButton from "./DownloadButton";
import FlashButton from "./FlashButton";
import { useProjectActions } from "./project-hooks";

/**
* The device connection area.
Expand All @@ -18,31 +15,14 @@ import FlashButton from "./FlashButton";
const DeviceConnection = () => {
const connectionStatus = useConnectionStatus();
const connected = connectionStatus === ConnectionStatus.CONNECTED;
const supported = connectionStatus !== ConnectionStatus.NOT_SUPPORTED;
const actionFeedback = useActionFeedback();
const device = useDevice();
const actions = useProjectActions();
const handleToggleConnected = useCallback(async () => {
if (connected) {
try {
await device.disconnect();
} catch (e) {
handleWebUSBError(actionFeedback, e);
}
await actions.disconnect();
} else {
if (!supported) {
actionFeedback.expectedError({
title: "WebUSB not supported",
description: "Download the hex file or try Chrome or Microsoft Edge",
});
} else {
try {
await device.connect();
} catch (e) {
handleWebUSBError(actionFeedback, e);
}
}
await actions.connect();
}
}, [device, connected, actionFeedback, supported]);
}, [connected, actions]);
const buttonWidth = "10rem";
return (
<HStack>
Expand Down Expand Up @@ -70,19 +50,4 @@ const DeviceConnection = () => {
);
};

const handleWebUSBError = (actionFeedback: ActionFeedback, e: any) => {
if (e instanceof WebUSBError) {
actionFeedback.expectedError({
title: e.title,
description: (
<Separate separator={br}>
{[e.message, e.description].filter(Boolean)}
</Separate>
),
});
} else {
actionFeedback.unexpectedError(e);
}
};

export default DeviceConnection;
30 changes: 4 additions & 26 deletions src/project/DownloadButton.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
import React, { useCallback } from "react";
import { Tooltip } from "@chakra-ui/react";
import { RiDownload2Line } from "react-icons/ri";
import useActionFeedback from "../common/use-action-feedback";
import { DownloadData } from "../fs/fs";
import { useFileSystem } from "../fs/fs-hooks";
import { saveAs } from "file-saver";
import CollapsableButton, {
CollapsibleButtonProps,
} from "../common/CollapsibleButton";
import { Tooltip } from "@chakra-ui/react";
import { useProjectActions } from "./project-hooks";

interface DownloadButtonProps
extends Omit<CollapsibleButtonProps, "onClick" | "text" | "icon"> {}
Expand All @@ -21,31 +17,13 @@ interface DownloadButtonProps
* Otherwise it's a more minor action.
*/
const DownloadButton = (props: DownloadButtonProps) => {
const fs = useFileSystem();
const actionFeedback = useActionFeedback();
const handleDownload = useCallback(async () => {
let download: DownloadData | undefined;
try {
download = await fs.toHexForDownload();
} catch (e) {
actionFeedback.expectedError({
title: "Failed to build the hex file",
description: e.message,
});
return;
}
const blob = new Blob([download.intelHex], {
type: "application/octet-stream",
});
saveAs(blob, download.filename);
}, [fs, actionFeedback]);

const actions = useProjectActions();
return (
<Tooltip hasArrow placement="top-start" label="Download a hex file">
<CollapsableButton
{...props}
icon={<RiDownload2Line />}
onClick={handleDownload}
onClick={actions.download}
text="Download"
/>
</Tooltip>
Expand Down
69 changes: 7 additions & 62 deletions src/project/FlashButton.tsx
Original file line number Diff line number Diff line change
@@ -1,63 +1,23 @@
import React, { useCallback, useState } from "react";
import { Tooltip } from "@chakra-ui/react";
import { useCallback, useState } from "react";
import { RiFlashlightFill } from "react-icons/ri";
import Separate, { br } from "../common/Separate";
import useActionFeedback, {
ActionFeedback,
} from "../common/use-action-feedback";
import { ConnectionStatus, WebUSBError } from "../device/device";
import { BoardId } from "../device/board-id";
import { useConnectionStatus, useDevice } from "../device/device-hooks";
import { useFileSystem } from "../fs/fs-hooks";
import CollapsableButton, {
CollapsibleButtonProps,
} from "../common/CollapsibleButton";
import { Tooltip } from "@chakra-ui/react";
import FlashProgress from "./FlashProgress";

class HexGenerationError extends Error {}
import { useProjectActions } from "./project-hooks";

/**
* Flash button.
*/
const FlashButton = (
props: Omit<CollapsibleButtonProps, "onClick" | "text" | "icon">
) => {
const fs = useFileSystem();
const actionFeedback = useActionFeedback();
const device = useDevice();
const status = useConnectionStatus();
const actions = useProjectActions();
const [progress, setProgress] = useState<number | undefined>();

const handleFlash = useCallback(async () => {
if (status === ConnectionStatus.NOT_SUPPORTED) {
actionFeedback.expectedError({
title: "WebUSB not supported",
description: "Download the hex file or try Chrome or Microsoft Edge",
});
return;
}

const dataSource = async (boardId: BoardId) => {
try {
return await fs.toHexForFlash(boardId);
} catch (e) {
throw new HexGenerationError(e.message);
}
};

try {
await device.flash(dataSource, { partial: true, progress: setProgress });
} catch (e) {
if (e instanceof HexGenerationError) {
actionFeedback.expectedError({
title: "Failed to build the hex file",
description: e.message,
});
} else {
handleWebUSBError(actionFeedback, e);
}
}
}, [fs, device, actionFeedback, status]);
const handleFlash = useCallback(() => {
actions.flash(setProgress);
}, [actions]);
return (
<>
<FlashProgress progress={progress} />
Expand All @@ -78,19 +38,4 @@ const FlashButton = (
);
};

const handleWebUSBError = (actionFeedback: ActionFeedback, e: any) => {
if (e instanceof WebUSBError) {
actionFeedback.expectedError({
title: e.title,
description: (
<Separate separator={br}>
{[e.message, e.description].filter(Boolean)}
</Separate>
),
});
} else {
actionFeedback.unexpectedError(e);
}
};

export default FlashButton;
2 changes: 1 addition & 1 deletion src/project/OpenButton.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Button, ButtonProps, Input } from "@chakra-ui/react";
import React, { useCallback, useRef } from "react";
import { RiFolderOpenLine } from "react-icons/ri";
import { useProjectActions } from "./use-project-actions";
import { useProjectActions } from "./project-hooks";

interface OpenButtonProps extends ButtonProps {
text?: string;
Expand Down
2 changes: 1 addition & 1 deletion src/project/Project.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useProject } from "../fs/fs-hooks";
import Workbench from "../workbench/Workbench";
import { useProject } from "./project-hooks";

const Project = () => {
const { projectId } = useProject();
Expand Down
Loading