324 lines
8.7 KiB
TypeScript
324 lines
8.7 KiB
TypeScript
import * as React from "react";
|
|
import type {
|
|
ViewProps,
|
|
TextProps,
|
|
PressableProps,
|
|
ScrollViewProps,
|
|
ModalProps,
|
|
StyleProp,
|
|
ViewStyle,
|
|
LinkingImpl,
|
|
TextInputProps,
|
|
} from "react-native";
|
|
import { useTerminalDimensions } from "@opentui/react";
|
|
import { BorderSides, RGBA } from "@opentui/core";
|
|
import { platform } from "node:os";
|
|
import { exec } from "node:child_process";
|
|
|
|
const RATIO_WIDTH = 8.433;
|
|
const RATIO_HEIGHT = 17;
|
|
|
|
function attr<K extends keyof ViewStyle>(
|
|
style: StyleProp<ViewStyle>,
|
|
name: K,
|
|
type: "string",
|
|
): Extract<ViewStyle[K], string> | undefined;
|
|
|
|
function attr<K extends keyof ViewStyle>(
|
|
style: StyleProp<ViewStyle>,
|
|
name: K,
|
|
type: "number",
|
|
): Extract<ViewStyle[K], number> | undefined;
|
|
|
|
function attr<K extends keyof ViewStyle>(
|
|
style: StyleProp<ViewStyle>,
|
|
name: K,
|
|
type: "boolean",
|
|
): Extract<ViewStyle[K], boolean> | undefined;
|
|
|
|
function attr<K extends keyof ViewStyle>(
|
|
style: StyleProp<ViewStyle>,
|
|
name: K,
|
|
type: "string" | "number" | "boolean",
|
|
) {
|
|
if (!style) return undefined;
|
|
|
|
const obj: ViewStyle = Array.isArray(style)
|
|
? Object.assign({}, ...style.filter(Boolean))
|
|
: (style as ViewStyle);
|
|
|
|
const v = obj[name];
|
|
return typeof v === type ? v : undefined;
|
|
}
|
|
|
|
export function View({ children, style }: ViewProps) {
|
|
const bg =
|
|
style && "backgroundColor" in style
|
|
? typeof style.backgroundColor == "string"
|
|
? style.backgroundColor.startsWith("rgba(")
|
|
? (() => {
|
|
const parts = style.backgroundColor.split("(")[1].split(")")[0];
|
|
const [r, g, b, a] = parts.split(",").map(parseFloat);
|
|
return RGBA.fromInts(r, g, b, a * 255);
|
|
})()
|
|
: style.backgroundColor
|
|
: undefined
|
|
: undefined;
|
|
|
|
const padding = attr(style, "padding", "number");
|
|
const paddingTop = attr(style, "paddingTop", "number");
|
|
const paddingLeft = attr(style, "paddingLeft", "number");
|
|
const paddingBottom = attr(style, "paddingBottom", "number");
|
|
const paddingRight = attr(style, "paddingRight", "number");
|
|
const gap = attr(style, "gap", "number");
|
|
|
|
const borderBottomWidth = attr(style, "borderBottomWidth", "number");
|
|
const borderTopWidth = attr(style, "borderTopWidth", "number");
|
|
const borderLeftWidth = attr(style, "borderLeftWidth", "number");
|
|
const borderRightWidth = attr(style, "borderRightWidth", "number");
|
|
|
|
const borderBottomColor = attr(style, "borderBottomColor", "string");
|
|
const borderTopColor = attr(style, "borderTopColor", "string");
|
|
const borderLeftColor = attr(style, "borderLeftColor", "string");
|
|
const borderRightColor = attr(style, "borderRightColor", "string");
|
|
|
|
const borderColor = attr(style, "borderColor", "string");
|
|
|
|
const top = attr(style, "top", "number");
|
|
|
|
const width = attr(style, "width", "number");
|
|
|
|
const props = {
|
|
overflow: attr(style, "overflow", "string"),
|
|
position: attr(style, "position", "string"),
|
|
alignSelf: attr(style, "alignSelf", "string"),
|
|
alignItems: attr(style, "alignItems", "string"),
|
|
justifyContent: attr(style, "justifyContent", "string"),
|
|
flexShrink: attr(style, "flexShrink", "number"),
|
|
flexDirection: attr(style, "flexDirection", "string"),
|
|
zIndex: attr(style, "zIndex", "number"),
|
|
left: attr(style, "left", "number"),
|
|
right: attr(style, "right", "number"),
|
|
bottom: attr(style, "bottom", "number"),
|
|
flexGrow:
|
|
attr(style, "flex", "number") || attr(style, "flexGrow", "number"),
|
|
};
|
|
|
|
const border = (() => {
|
|
const sides: BorderSides[] = [];
|
|
if (borderBottomWidth) sides.push("bottom");
|
|
if (borderTopWidth) sides.push("top");
|
|
if (borderLeftWidth) sides.push("left");
|
|
if (borderRightWidth) sides.push("right");
|
|
if (!sides.length) return undefined;
|
|
return sides;
|
|
})();
|
|
|
|
return (
|
|
<box
|
|
backgroundColor={bg}
|
|
paddingTop={
|
|
(paddingTop && Math.round(paddingTop / RATIO_HEIGHT)) ||
|
|
(padding && Math.round(padding / RATIO_HEIGHT))
|
|
}
|
|
paddingBottom={
|
|
(paddingBottom && Math.round(paddingBottom / RATIO_HEIGHT)) ||
|
|
(padding && Math.round(padding / RATIO_HEIGHT))
|
|
}
|
|
paddingLeft={
|
|
(paddingLeft && Math.round(paddingLeft / RATIO_WIDTH)) ||
|
|
(padding && Math.round(padding / RATIO_WIDTH))
|
|
}
|
|
paddingRight={
|
|
(paddingRight && Math.round(paddingRight / RATIO_WIDTH)) ||
|
|
(padding && Math.round(padding / RATIO_WIDTH))
|
|
}
|
|
gap={gap && Math.round(gap / RATIO_HEIGHT)}
|
|
border={border}
|
|
borderColor={borderColor}
|
|
width={width && Math.round(width / RATIO_WIDTH)}
|
|
top={top && Math.round(top / RATIO_HEIGHT)}
|
|
{...props}
|
|
>
|
|
{children}
|
|
</box>
|
|
);
|
|
}
|
|
|
|
export function Pressable({
|
|
children: childrenRaw,
|
|
style,
|
|
onPress,
|
|
}: PressableProps) {
|
|
const bg =
|
|
style && "backgroundColor" in style
|
|
? typeof style.backgroundColor == "string"
|
|
? style.backgroundColor.startsWith("rgba(")
|
|
? (() => {
|
|
const parts = style.backgroundColor.split("(")[1].split(")")[0];
|
|
const [r, g, b, a] = parts.split(",").map(parseFloat);
|
|
return RGBA.fromInts(r, g, b, a * 255);
|
|
})()
|
|
: style.backgroundColor
|
|
: undefined
|
|
: undefined;
|
|
const flexDirection =
|
|
style && "flexDirection" in style
|
|
? typeof style.flexDirection == "string"
|
|
? style.flexDirection
|
|
: undefined
|
|
: undefined;
|
|
const flex =
|
|
style && "flex" in style
|
|
? typeof style.flex == "number"
|
|
? style.flex
|
|
: undefined
|
|
: undefined;
|
|
const flexShrink =
|
|
style && "flexShrink" in style
|
|
? typeof style.flexShrink == "number"
|
|
? style.flexShrink
|
|
: undefined
|
|
: undefined;
|
|
const overflow =
|
|
style && "overflow" in style
|
|
? typeof style.overflow == "string"
|
|
? style.overflow
|
|
: undefined
|
|
: undefined;
|
|
const position =
|
|
style && "position" in style
|
|
? typeof style.position == "string"
|
|
? style.position
|
|
: undefined
|
|
: undefined;
|
|
const justifyContent =
|
|
style && "justifyContent" in style
|
|
? typeof style.justifyContent == "string"
|
|
? style.justifyContent
|
|
: undefined
|
|
: undefined;
|
|
const alignItems =
|
|
style && "alignItems" in style
|
|
? typeof style.alignItems == "string"
|
|
? style.alignItems
|
|
: undefined
|
|
: undefined;
|
|
|
|
const padding =
|
|
style && "padding" in style
|
|
? typeof style.padding == "number"
|
|
? style.padding
|
|
: undefined
|
|
: undefined;
|
|
|
|
const children =
|
|
childrenRaw instanceof Function
|
|
? childrenRaw({ pressed: true })
|
|
: childrenRaw;
|
|
|
|
return (
|
|
<box
|
|
onMouseDown={
|
|
onPress
|
|
? (_event) => {
|
|
// @ts-ignore
|
|
onPress();
|
|
}
|
|
: undefined
|
|
}
|
|
backgroundColor={bg}
|
|
flexDirection={flexDirection}
|
|
flexGrow={flex}
|
|
overflow={overflow}
|
|
flexShrink={flexShrink}
|
|
position={position}
|
|
justifyContent={justifyContent}
|
|
alignItems={alignItems}
|
|
paddingTop={padding && Math.round(padding / RATIO_HEIGHT)}
|
|
paddingBottom={padding && Math.round(padding / RATIO_HEIGHT)}
|
|
paddingLeft={padding && Math.round(padding / RATIO_WIDTH)}
|
|
paddingRight={padding && Math.round(padding / RATIO_WIDTH)}
|
|
>
|
|
{children}
|
|
</box>
|
|
);
|
|
}
|
|
|
|
export function Text({ style, children }: TextProps) {
|
|
const fg =
|
|
style && "color" in style
|
|
? typeof style.color == "string"
|
|
? style.color
|
|
: undefined
|
|
: undefined;
|
|
return <text fg={fg || "black"}>{children}</text>;
|
|
}
|
|
|
|
export function ScrollView({ children }: ScrollViewProps) {
|
|
return <scrollbox>{children}</scrollbox>;
|
|
}
|
|
|
|
export function Modal({ children, visible }: ModalProps) {
|
|
const { width, height } = useTerminalDimensions();
|
|
return (
|
|
<box
|
|
visible={visible}
|
|
position="absolute"
|
|
width={width}
|
|
height={height}
|
|
zIndex={10}
|
|
>
|
|
{children}
|
|
</box>
|
|
);
|
|
}
|
|
|
|
export function TextInput({
|
|
defaultValue,
|
|
onChangeText,
|
|
onKeyPress,
|
|
}: TextInputProps) {
|
|
return (
|
|
<input
|
|
minWidth={20}
|
|
minHeight={1}
|
|
backgroundColor="white"
|
|
textColor="black"
|
|
focused={true}
|
|
cursorColor={"black"}
|
|
onInput={onChangeText}
|
|
onKeyDown={(key) =>
|
|
// @ts-ignore
|
|
onKeyPress({
|
|
nativeEvent: {
|
|
key: key.name == "return" ? "Enter" : key.name,
|
|
},
|
|
})
|
|
}
|
|
placeholder={defaultValue}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export const Platform = {
|
|
OS: "tui",
|
|
};
|
|
|
|
export const Linking = {
|
|
openURL: async (url: string) => {
|
|
const cmd =
|
|
platform() == "darwin"
|
|
? `open ${url}`
|
|
: platform() == "win32"
|
|
? `start "" "${url}"`
|
|
: `xdg-open "${url}"`;
|
|
exec(cmd);
|
|
},
|
|
} satisfies Partial<LinkingImpl>;
|
|
|
|
export default {
|
|
View,
|
|
Text,
|
|
};
|