Skip to content

Commit

Permalink
chore: version 0.2.1
Browse files Browse the repository at this point in the history
  • Loading branch information
imp-dance committed Nov 6, 2024
1 parent d7f4712 commit 98ef81a
Show file tree
Hide file tree
Showing 18 changed files with 360 additions and 110 deletions.
41 changes: 41 additions & 0 deletions docs/docs/apps.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,39 @@ There are 5 apps by default that come with RadixOS:
| Settings | `settings` | System customization and formatting | Tab-id |
| Image Viewer | `image` | Can open images | data:image or SVG |

### Configuration

You can configure some apps by passing a second argument to `setupApps`:

```tsx
const apps = setupApps([], {
terminalPlugins: [],
defaultAppsOnDesktop: ["terminal", "explorer"],
});
```

### Terminal plugins

You can pass extra matchers as plugins to the terminal application. This allows you to respond to commands and push output back to the terminal.

```tsx
const terminalPluginOpen: TerminalPlugin = {
matcher: (command) => command === "open",
exec: (pushOutput, command, args) => {
// do some stuff, then
pushOutput(
<Code size="sm" variant="soft">
Opening "{args.join(" ")}"
</Code>
);
},
};

const apps = setupApps([], {
terminalPlugins: [terminalPluginOpen],
});
```

## Creating your own applications

You can create apps by using `createApp`:
Expand Down Expand Up @@ -80,6 +113,14 @@ const { launch } = useAppLauncher();
launch("some-app", { ...settings });
```

### Set default focus element

To configure which element should be focused when the title-bar is clicked, or when the app is initially opened, you can set the following attribute on the HTML element: `data-returnfocus="true"`. Example:

```tsx
<Button data-returnfocus="true">Call to action</Button>
```

## Application Hooks

The app components are mounted inside `RadixOS`, which gives them access to a few hooks to control the operating system.
Expand Down
21 changes: 13 additions & 8 deletions docs/docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,25 @@ sidebar_position: 1

# Radix OS

Radix OS is a operating system simulated on the web, with a modular file system that can be swapped out with any async source, able to run your own custom built applications. Designed to be flexible and easily extendable so it can fit your needs.
Radix OS is an operating system simulated on the web, with a modular file system that can be swapped out with any async source, able to run your own custom built applications. Designed to be flexible and easily extendable so it can fit your needs.

[![Preview](/sh.jpg)](https://imp-dance.github.io/radix-os/)

:::tip So what it, really?

A React component, coupled with a few helper functions and hooks - published to NPM. Together, this package lets you create an OS-like environment, and inject custom applications of your own.
:::

## Features

- Window management
- Modular file system
- Customizable UI
- App launcher
- Keyboard shortcuts
- Context menus
- System UI components
- Drag 'n drop file upload
- Drag 'n drop user interface
- Set of default applications
- Explorer, terminal, code, image viewer + more.

## Getting started

Expand All @@ -41,24 +46,24 @@ We also recommend that you install `@radix-ui/react-icons` if you plan on extend

```tsx title="lib/radix-os.ts"
import {
fsZustandIntegration,
createZustandFsIntegration,
setupApps,
createUseAppLauncher
} from "radix-os;

export const applications = setupApps();
export const useAppLauncher = createUseAppLauncher(applications);
export const fs = createZustandFsIntegration();
```

```tsx title="App.tsx"
import '@radix-ui/themes/styles.css';
import {
RadixOS,
fsZustandIntegration
} from "radix-os;
import { applications } from "./lib/radix-os";
import { applications, fs } from "./lib/radix-os";

export default function App(){
return <RadixOS fs={fsZustandIntegration} applications={applications} />
return <RadixOS fs={fs} applications={applications} />
}
```
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"dependencies": {
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/themes": "^3.1.4",
"radix-os": "^0.2.0",
"radix-os": "^0.2.1",
"radix-ui": "^1.0.1",
"react": "^18.3.1",
"react-confetti": "^6.1.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/radix-os/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "radix-os",
"version": "0.2.0",
"version": "0.2.1",
"description": "A simulated operating system built with React and Radix UI",
"main": "dist/index.js",
"module": "dist/index.mjs",
Expand Down
16 changes: 11 additions & 5 deletions packages/radix-os/src/components/Desktop/Desktop.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ import {
useState,
} from "react";
import { DesktopShortcut } from "../../services/applications/desktop-shortcuts";
import { useUntypedAppContext } from "../../services/applications/launcher";
import {
UseAppLauncherReturn,
useUntypedAppContext,
} from "../../services/applications/launcher";
import { useSettingsStore } from "../../stores/settings";
import { RadixOsApp } from "../../stores/window";
import { throttle } from "../../utils";
Expand Down Expand Up @@ -64,9 +67,11 @@ export function Desktop(props: {
icon: app.defaultWindowSettings?.icon ?? <HomeIcon />,
title: app.appName,
id: app.appId,
onClick: () => {
onClick: (() => {
launch(app.appId);
},
}) as (
appLauncher: UseAppLauncherReturn<string>
) => void,
position: {
x: gridPad,
y: i > 0 ? gridSize * i + gridPad : gridPad,
Expand Down Expand Up @@ -248,9 +253,10 @@ function Application(props: {
icon: ReactNode;
title: string;
id: string;
onClick: () => void;
onClick: (appLauncher: UseAppLauncherReturn<string>) => void;
position: { x: number; y: number };
}) {
const appLauncher = useUntypedAppContext();
const [mousePosition, setMousePosition] = useState<{
x: number;
y: number;
Expand Down Expand Up @@ -294,7 +300,7 @@ function Application(props: {
style={style}
{...draggable.attributes}
{...draggable.listeners}
onClick={props.onClick}
onClick={() => props.onClick(appLauncher)}
>
{props.icon}
</Button>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
DndContext,
MouseSensor,
pointerWithin,
useSensor,
} from "@dnd-kit/core";
import { ReactNode } from "react";
Expand All @@ -24,6 +25,7 @@ export function WindowDragManager(props: {
<DndContext
modifiers={[restrictToDesktopEdges]}
sensors={[mouseSensor]}
collisionDetection={pointerWithin}
onDragEnd={(event) => {
setIsDragging(false);
const window = windows.find(
Expand Down
33 changes: 32 additions & 1 deletion packages/radix-os/src/components/apps/Settings/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,6 @@ function StorageTab() {
</Flex>
</AlertDialog.Content>
</AlertDialog.Root>

<AlertDialog.Root>
<AlertDialog.Trigger>
<Button color="crimson" variant="outline">
Expand Down Expand Up @@ -279,7 +278,39 @@ function StorageTab() {
</Flex>
</AlertDialog.Content>
</AlertDialog.Root>
<Text size="1" color="gray">
Used space:{" "}
{getByteSize(localStorage.getItem(FS_LS_KEY) ?? "")}
</Text>
</Flex>
</Tabs.Content>
);
}

function getByteSize(s: string) {
return formatBytes(new Blob([s]).size);
}

function formatBytes(bytes: number, decimals = 2) {
if (!+bytes) return "0 Bytes";

const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = [
"Bytes",
"KiB",
"MiB",
"GiB",
"TiB",
"PiB",
"EiB",
"ZiB",
"YiB",
];

const i = Math.floor(Math.log(bytes) / Math.log(k));

return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${
sizes[i]
}`;
}
58 changes: 55 additions & 3 deletions packages/radix-os/src/components/apps/Terminal/Terminal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
TextField,
} from "@radix-ui/themes";
import React, {
ComponentProps,
ReactNode,
useLayoutEffect,
useRef,
Expand All @@ -29,9 +30,37 @@ import { FsFolder } from "../../../stores/fs";
import { RadixOsAppComponent } from "../../../stores/window";
import { Command, helpText } from "./constants";
import { parseFs } from "./modules/fs";
import { joinQuotedArgs } from "./utils";
import { parseOpen } from "./modules/open";
import { extractFlags, joinQuotedArgs } from "./utils";

export const Terminal: RadixOsAppComponent = (props) => {
export type TerminalPlugin = {
matcher: (command: string, args: string[]) => boolean;
exec: (
pushOutput: (...output: ReactNode[]) => void,
command: string,
args: string[]
) => void;
/** Continue checking for other commands after exec, even if command is matched */
passThrough?: boolean;
};

function findLeafString(node: ReactNode): string {
if (typeof node === "string") return node;
if (typeof node === "number") return node.toString();
if (Array.isArray(node))
return node.map(findLeafString).join("");
if (node === null || node === undefined) return "";
if (typeof node === "object" && "props" in node) {
return findLeafString(node.props.children);
}
return "";
}

export const Terminal = (
props: ComponentProps<RadixOsAppComponent> & {
plugins?: TerminalPlugin[];
}
) => {
const { openFile } = useUntypedAppContext();
const currentCommandIndex = useRef(0);
const prevCommands = useRef<string[]>([]);
Expand All @@ -50,6 +79,11 @@ export const Terminal: RadixOsAppComponent = (props) => {
]);

function pushOutput(...output: ReactNode[]) {
/*const asText = output
.map((e) => findLeafString(e))
.join("\n")
.trim(); */

setOutput((prev) => [...prev, ...output]);
}

Expand Down Expand Up @@ -85,8 +119,27 @@ export const Terminal: RadixOsAppComponent = (props) => {
const parts = input.split(" ");
const [command, ...args_] = parts;
const args = joinQuotedArgs(args_);
const flags = extractFlags(args);
const plugins = props.plugins ?? [];
for (const plugin of plugins) {
if (plugin.matcher(command, args)) {
plugin.exec(pushOutput, command, args);
if (!plugin.passThrough) return;
}
}
if (command === "") return pushOutput(<></>);
switch (command) {
case "open": {
return parseOpen({
tree,
args,
cd: path.current.join("/"),
flags,
openFile,
pushOutput,
});
break;
}
case "echo": {
pushOutput(
<Code
Expand Down Expand Up @@ -199,7 +252,6 @@ export const Terminal: RadixOsAppComponent = (props) => {
updateFile: (path, file) =>
updateFile.mutateAsync({ path, file }),
tree: tree as FsFolder,
openFile,
});
}
case "cd": {
Expand Down
13 changes: 12 additions & 1 deletion packages/radix-os/src/components/apps/Terminal/constants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,22 @@ mkdir [name] - Create a new directory
ls - List files in current directory
cd [path] - Change directory
mv [path1] [path2] - Move file/folder from [path1] to [path2]
open [path] - Open a file
open --help - Show help for opening files
fs [path] -R [name] - Rename a file or folder
fs [path] -O [launcher?] - Open a file
fs [path] -L [launcher] - Set a file's default launcher
fs [path] --ll - List a file's available launchers
fs [path] --ex [launcher] - Make file executable in given launcher
Relative paths are supported. Use quotes for paths with spaces.`;

export const openHelpText = `Available flags for command "open":
--help - Show this help message
-l [launcher] - Open file with specific launcher
-x [number] - Horizontal position of window
-y [number] - Vertical position of window
-w [number] - Width of window
-h [number] - Height of window
-r [true/false] - Resizable
-s [true/false] - Scrollable`;
Loading

0 comments on commit 98ef81a

Please sign in to comment.