Frontend improvements + Docker updates (#167, #169)

Co-authored-by: Mark Street <22226349+mkst@users.noreply.github.com>
This commit is contained in:
Alex Bates
2021-10-14 12:52:28 +01:00
committed by GitHub
parent 7ee5acf8d8
commit 0e8b01841a
78 changed files with 2435 additions and 901 deletions

View File

@@ -4,7 +4,6 @@ on:
branches:
- main
pull_request:
pull_request_target:
jobs:
reviewdog:
name: reviewdog

1
.gitignore vendored
View File

@@ -12,6 +12,7 @@ sandbox/
*.log
/frontend/.next
/frontend/.cache
/frontend/cache
/frontend/public/sw.*
/frontend/public/workbox-*
/frontend/storybook-static

View File

@@ -2,6 +2,9 @@ runner:
eslint:
cmd: cd frontend && yarn -s run eslint src --ext .js,.jsx,.ts,.tsx -f=rdjson
format: rdjson
stylelint:
cmd: cd frontend && yarn -s run stylelint 'src/**/*.css' 'src/**/*.scss'
format: stylelint
tsc:
cmd: cd frontend && yarn -s run tsc
format: tsc

7
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,7 @@
{
"recommendations": [
"EditorConfig.EditorConfig",
"dbaeumer.vscode-eslint",
"stylelint.vscode-stylelint",
]
}

15
.vscode/settings.json vendored
View File

@@ -14,4 +14,19 @@
"editor.defaultFormatter": "dbaeumer.vscode-eslint",
"editor.formatOnSave": true,
},
// stylelint
"[css]": {
"editor.defaultFormatter": "stylelint.vscode-stylelint",
"editor.formatOnSave": true,
},
"[scss]": {
"editor.defaultFormatter": "stylelint.vscode-stylelint",
"editor.formatOnSave": true,
},
"[json]": {
"editor.defaultFormatter": "vscode.json-language-features",
"editor.formatOnSave": true,
},
}

View File

@@ -36,6 +36,13 @@ RUN python3 -m pip install -r /backend/requirements.txt --no-cache-dir
COPY --from=nsjail /bin/nsjail /bin/nsjail
# nsjail wants to mount this so ensure it exists
RUN mkdir -p /lib32
COPY compilers/download.sh /compilers/
RUN bash /compilers/download.sh
WORKDIR /backend
ENTRYPOINT ["/backend/docker_entrypoint.sh"]

View File

@@ -1,4 +1,4 @@
*
!gcc2.8.1/
!ido5.3/
!ido7.1/
!gcc2.8.1
!ido5.3
!ido7.1

View File

@@ -13,11 +13,14 @@ else
fi
# gcc2.8.1
mkdir -p "$compiler_dir/gcc2.8.1"
curl -L "https://github.com/pmret/gcc-papermario/releases/download/master/$os.tar.gz" | tar zx -C "$compiler_dir/gcc2.8.1"
curl -L "https://github.com/pmret/binutils-papermario/releases/download/master/$os.tar.gz" | tar zx -C "$compiler_dir/gcc2.8.1"
# ido5.3
curl -L "https://github.com/ethteck/ido-static-recomp/releases/download/master/ido-5.3-recomp-$ido_os-latest.tar.gz" | tar zx -C "$compiler_dir/ido5.3/"
mkdir -p "$compiler_dir/ido5.3"
curl -L "https://github.com/ethteck/ido-static-recomp/releases/download/master/ido-5.3-recomp-$ido_os-latest.tar.gz" | tar zx -C "$compiler_dir/ido5.3"
# ido7.1
curl -L "https://github.com/ethteck/ido-static-recomp/releases/download/master/ido-7.1-recomp-$ido_os-latest.tar.gz" | tar zx -C "$compiler_dir/ido7.1/"
mkdir -p "$compiler_dir/ido7.1"
curl -L "https://github.com/ethteck/ido-static-recomp/releases/download/master/ido-7.1-recomp-$ido_os-latest.tar.gz" | tar zx -C "$compiler_dir/ido7.1"

View File

@@ -1,4 +1,5 @@
from typing import Dict, List, Optional, Set, Tuple
from collections import OrderedDict
from coreapp.models import Asm, Assembly, Compilation
from coreapp import util
from coreapp.sandbox import Sandbox
@@ -38,9 +39,10 @@ else:
def load_compilers() -> Dict[str, Dict[str, str]]:
ret = {}
compiler_dirs = next(os.walk(settings.COMPILER_BASE_PATH))
compilers_base = settings.BASE_DIR / "compilers"
compiler_dirs = next(os.walk(compilers_base))
for compiler_id in compiler_dirs[1]:
config_path = Path(settings.COMPILER_BASE_PATH / compiler_id / "config.json")
config_path = Path(compilers_base / compiler_id / "config.json")
if config_path.exists():
with open(config_path) as f:
ret[compiler_id] = json.load(f)
@@ -51,6 +53,7 @@ def load_compilers() -> Dict[str, Dict[str, str]]:
@dataclass
class Arch:
name: str
description: str
assemble_cmd: Optional[str] = None
objdump_cmd: Optional[str] = None
nm_cmd: Optional[str] = None
@@ -59,13 +62,15 @@ class Arch:
def load_arches() -> Dict[str, Arch]:
return {
"mips": Arch(
"MIPS (Nintendo 64)",
"Nintendo 64",
"MIPS (big-endian)",
assemble_cmd='mips-linux-gnu-as -march=vr4300 -mabi=32 -o "$OUTPUT" "$INPUT"',
objdump_cmd="mips-linux-gnu-objdump",
nm_cmd="mips-linux-gnu-nm",
),
"mipsel": Arch(
"MIPS (LE)",
"PlayStation 2",
"MIPS (little-endian)",
assemble_cmd='mips-linux-gnu-as -march=mips64 -mabi=64 -o "$OUTPUT" "$INPUT"',
objdump_cmd="mips-linux-gnu-objdump",
nm_cmd="mips-linux-gnu-nm",
@@ -120,17 +125,18 @@ class CompilerWrapper:
return {k: {"arch": CompilerWrapper.arch_from_compiler(k)} for k in CompilerWrapper.available_compiler_ids()}
@staticmethod
def available_arches() -> List[Tuple[str, str]]:
def available_arches() -> OrderedDict[str, Dict[str, str]]:
a_set: Set[str] = set()
ret = []
ret = OrderedDict()
for id in CompilerWrapper.available_compiler_ids():
a_set.add(_compilers[id]["arch"])
for a in a_set:
ret.append((a, _arches[a].name))
ret.sort(key=lambda x: x[0])
for a in sorted(a_set):
ret[a] = {
"name": _arches[a].name,
"description": _arches[a].description,
}
return ret

View File

@@ -55,6 +55,7 @@ class Sandbox(contextlib.AbstractContextManager["Sandbox"]):
"--bindmount_ro", "/lib32",
"--bindmount_ro", "/lib64",
"--bindmount_ro", "/usr",
"--bindmount_ro", str(settings.COMPILER_BASE_PATH),
"--env", "PATH=/usr/bin:/bin",
"--disable_proc", # Needed for running inside Docker
"--time_limit", "30", # seconds

View File

@@ -25,6 +25,7 @@ env = environ.Env(
SESSION_COOKIE_SECURE=(bool, True),
GITHUB_CLIENT_ID=(str, ""),
GITHUB_CLIENT_SECRET=(str, ""),
COMPILER_BASE_PATH=(str, BASE_DIR / "compilers")
)
for stem in [".env.local", ".env"]:
@@ -150,7 +151,7 @@ if DEBUG:
else:
SESSION_COOKIE_SAMESITE = "Lax"
COMPILER_BASE_PATH = BASE_DIR / "compilers"
COMPILER_BASE_PATH = Path(env("COMPILER_BASE_PATH"))
LOCAL_FILE_DIR = BASE_DIR / "local_files"
USE_SANDBOX_JAIL = env("USE_SANDBOX_JAIL")

View File

@@ -21,8 +21,9 @@ services:
DATABASE_URL: psql://decompme:decompme@postgres:5432/decompme
SECRET_KEY: "django-insecure-nm#!8%z$$hc0wwi#m_*l9l)=m*6gs4&o_^-e5b5vj*k05&yaqc1"
DEBUG: "on"
ALLOWED_HOSTS: "localhost,127.0.0.1"
ALLOWED_HOSTS: "backend,localhost,127.0.0.1"
USE_SANDBOX_JAIL: "on"
COMPILER_BASE_PATH: /compilers
ports:
- "8000:8000"
security_opt:
@@ -38,6 +39,8 @@ services:
size: 64M
frontend:
build: frontend
environment:
INTERNAL_API_BASE: http://backend:8000/api
ports:
- "8080:8080"
volumes:

3
frontend/.dockerignore Normal file
View File

@@ -0,0 +1,3 @@
.next/
cache/
node_modules/

View File

@@ -1,18 +1,55 @@
{
"extends": "next/core-web-vitals",
"parser": "@typescript-eslint/parser",
"plugins": [
"@typescript-eslint",
"css-modules"
],
"extends": [
"eslint:recommended",
"next/core-web-vitals",
"plugin:@typescript-eslint/recommended"
],
"rules": {
"semi": ["error", "never", { "beforeStatementContinuationChars": "always" }],
"indent": ["error", 4],
"quotes": ["error", "double"],
"quote-props": ["error", "consistent"],
"brace-style": "error",
"brace-style": "off",
"@typescript-eslint/brace-style": ["error"],
"object-curly-spacing": ["error", "always"],
"array-bracket-spacing": ["error", "never"],
"no-else-return": "off",
"no-trailing-spaces": "error",
"no-multi-spaces": "error",
"no-multiple-empty-lines": "error",
"comma-dangle": "off",
"@typescript-eslint/comma-dangle": ["error", "always-multiline"],
"comma-spacing": "off",
"@typescript-eslint/comma-spacing": ["error"],
"prefer-const": ["warn", { "destructuring": "all" }],
"arrow-parens": ["error", "as-needed"],
"no-confusing-arrow": ["error", { "allowParens": true }],
"@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }],
"no-extra-semi": "off",
"@typescript-eslint/no-extra-semi": ["error"],
"no-empty-function": "off",
"@typescript-eslint/no-empty-function": "off",
"no-unused-expressions": "off",
"@typescript-eslint/no-unused-expressions": ["warn"],
"@typescript-eslint/ban-ts-comment": "off",
"keyword-spacing": "off",
"@typescript-eslint/keyword-spacing": ["error"],
"@typescript-eslint/member-delimiter-style": ["error", {
"multiline": {
"delimiter": "none",
"requireLast": true
},
"singleline": {
"delimiter": "comma",
"requireLast": false
},
"multilineDetection": "brackets"
}],
"import/newline-after-import": "error",
"import/no-duplicates": "error",
"import/first": "error",
@@ -42,7 +79,14 @@
"pathGroupsExcludedImportTypes": ["react"],
"alphabetize": { "order": "asc", "caseInsensitive": true }
}
]
],
"import/no-unresolved": "error",
"import/export": "warn",
"import/no-named-as-default": "warn",
"import/no-named-as-default-member": "warn",
"import/no-unused-modules": "warn",
"css-modules/no-unused-class": "warn",
"css-modules/no-undef-class": "warn"
},
"overrides": [
{

View File

@@ -0,0 +1,12 @@
{
"extends": [
"stylelint-config-standard",
"stylelint-config-css-modules"
],
"rules": {
"indentation": 4,
"declaration-empty-line-before": null,
"at-rule-no-unknown": null,
"no-descending-specificity": null
}
}

View File

@@ -12,7 +12,14 @@ for (const envFile of [".env.local", ".env"]) {
process.env.NEXT_PUBLIC_API_BASE = process.env.API_BASE
process.env.NEXT_PUBLIC_GITHUB_CLIENT_ID = process.env.GITHUB_CLIENT_ID
process.env.NEXT_PUBLIC_COMMIT_HASH = execSync("git rev-parse HEAD").toString().trim()
let git_hash
try {
git_hash = execSync("git rev-parse HEAD").toString().trim()
} catch (error) {
console.log("Unable to get git hash, assume running inside Docker")
git_hash = "abc123"
}
process.env.NEXT_PUBLIC_COMMIT_HASH = git_hash
const withPWA = require('next-pwa')
const runtimeCaching = require('next-pwa/cache')
@@ -22,9 +29,14 @@ module.exports = removeImports(withPWA({
return [
{
source: "/",
destination: "/scratch",
destination: "/scratch/new",
permanent: false,
},
{
source: "/scratch",
destination: "/scratch/new",
permanent: true,
},
]
},
async rewrites() {

View File

@@ -4,7 +4,7 @@
"dev": "next dev --port 8080",
"build": "next build",
"start": "next start --port 8080",
"lint": "next lint",
"lint": "next lint && yarn stylelint 'src/**/*.css' 'src/**/*.scss'",
"postinstall": "next telemetry disable > /dev/null",
"storybook": "start-storybook -p 6006 --ci",
"build-storybook": "build-storybook"
@@ -43,6 +43,8 @@
"@storybook/preset-scss": "^1.0.3",
"@storybook/react": "^6.3.10",
"@svgr/webpack": "^5.5.0",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
"autoprefixer": "^10.3.1",
"babel-loader": "^8.2.2",
"css-loader": "^6.4.0",
@@ -51,6 +53,7 @@
"eslint": "7",
"eslint-config-next": "11.1.2",
"eslint-formatter-rdjson": "^1.0.5",
"eslint-plugin-css-modules": "^2.11.0",
"eslint-plugin-react": "^7.26.1",
"eslint-plugin-react-hooks": "^4.2.0",
"monaco-editor-webpack-plugin": "^5.0.0",
@@ -61,6 +64,9 @@
"sass-loader": "^12.1.0",
"storybook-dark-mode": "^1.0.8",
"style-loader": "^3.3.0",
"stylelint": "^13.13.1",
"stylelint-config-css-modules": "^2.2.0",
"stylelint-config-standard": "^22.0.0",
"typescript": "^4.4.2",
"webpack": "5"
}

View File

@@ -0,0 +1,68 @@
.container {
list-style: none;
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 8px;
}
.arch {
flex: 1;
display: flex;
align-items: center;
gap: 16px;
padding: 16px;
border: 1px solid transparent;
border-radius: 6px;
background: var(--g300);
color: var(--g1200);
cursor: pointer;
user-select: none;
white-space: nowrap;
transition: border-color 0.1s, background 0.15s, color 0.2s;
svg {
width: 40px;
height: 40px;
will-change: filter;
filter: grayscale(100%);
transition: filter 0.2s;
}
&:hover,
&.selected {
border-color: var(--g400);
color: var(--g2000);
svg {
filter: grayscale(0%);
}
}
&.selected {
background: var(--g400);
}
}
.labelContainer {
display: flex;
flex-direction: column;
gap: 2px;
}
.consoleName {
font-weight: 500;
}
.archName {
font-size: 0.8em;
opacity: 0.6;
}

View File

@@ -0,0 +1,42 @@
import classNames from "classnames"
import styles from "./ArchSelect.module.scss"
import LogoN64 from "./n64.svg"
import LogoPS2 from "./ps2.svg"
const ICONS = {
"mips": <LogoN64 />,
"mipsel": <LogoPS2 />,
}
export type Props = {
arches: {
[key: string]: {
name: string
description: string
}
}
value: string
className?: string
onChange: (value: string) => void
}
export default function ArchSelect({ arches, value, onChange, className }: Props) {
if (!value)
onChange("mips")
return <ul className={classNames(styles.container, className)}>
{Object.entries(arches).map(([key, arch]) => <li
key={key}
className={classNames(styles.arch, { [styles.selected]: value === key })}
onClick={() => onChange(key)}
>
{ICONS[key]}
<div className={styles.labelContainer}>
<div className={styles.consoleName}>{arch.name}</div>
<div className={styles.archName}>{arch.description}</div>
</div>
</li>)}
</ul>
}

View File

@@ -0,0 +1,4 @@
import ArchSelect, { Props as ArchSelectProps } from "./ArchSelect"
export type Props = ArchSelectProps
export default ArchSelect

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg version="1.1" viewBox="0 0 240 240" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(-142.51 -9.4602)" stroke-width=".46815">
<path d="m318.38 63.882-14.986 23.662 43.806 62.326v-74.89l-28.819-11.099z" color="#000000" fill="#069330" style="-inkscape-stroke:none"/>
<path d="m226.95 82.877-11.174-22.255-31.32 12.39 32.183 61.844v-47.206z" color="#000000" fill="#fe2015" style="-inkscape-stroke:none"/>
<g fill="#ffc001">
<path d="m318.38 56.631 32.148 11.826 31.821-11.827-29.506-11.827-34.463 11.827z" color="#000000" style="-inkscape-stroke:none"/>
<path d="m300.65 21.922-30.971-12.308-30.969 12.157 30.971 12.308z" color="#000000" style="-inkscape-stroke:none"/>
<path d="m175.43 40.375-32.766 13.162 36.995 12.858 30.479-12.858z" color="#000000" style="-inkscape-stroke:none"/>
<path d="m262.74 104.58 34.725-13.448-34.725-14.403-34.725 15.358z" color="#000000" style="-inkscape-stroke:none"/>
</g>
<path d="m244.5 76.038 19.291-36.262-29.506-12.471-14.177 26.232 12.991 27.215z" color="#000000" fill="#069330" style="-inkscape-stroke:none"/>
<path d="m262.74 68.877 34.917 13.531 7.989-13.531v-41.316l-32.329 12.288-10.576 19.304v9.7234z" color="#000000" fill="#011da9" style="-inkscape-stroke:none"/>
<path d="m346.53 215.52-40.778-53.751v22.406l15.34 20.983 25.438 10.362z" color="#000000" fill="#fe2015" style="-inkscape-stroke:none"/>
<path d="m257.42 249.31v-136.24l-33.787-13.602v67.877l-49.048-93.951-31.927-12.509v139.25l31.79 13.734v-75.901l49.055 96.825z" color="#000000" fill="#069330" style="-inkscape-stroke:none"/>
<path d="m200.48 204.94-18.37-34.309v43.239z" color="#000000" fill="#011da9" style="-inkscape-stroke:none"/>
<path d="m350.85 211.61-51.2-68.088v90.06l-32.486 15.723v-136.24l34.637-13.602 52.511 70.095v-94.581l28.036-10.359v134.25l-31.499 12.738z" color="#000000" fill="#011da9" style="-inkscape-stroke:none"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg data-name="Layer 1" version="1.1" viewBox="0 0 235 235" xmlns="http://www.w3.org/2000/svg">
<defs>
<style>.cls-1, .cls-2, .cls-3, .cls-4 {
fill-rule: evenodd;
}
.cls-1 {
fill: url(#linear-gradient);
}
.cls-2 {
fill: url(#linear-gradient-2);
}
.cls-3 {
fill: url(#linear-gradient-3);
}</style>
<linearGradient id="linear-gradient" x1="116.84" x2="116.84" y1="129.72" gradientUnits="userSpaceOnUse">
<stop stop-color="#23bcee" offset="0"/>
<stop stop-color="#24b0e5" offset=".07"/>
<stop stop-color="#2785c3" offset=".36"/>
<stop stop-color="#2965aa" offset=".63"/>
<stop stop-color="#2b529b" offset=".85"/>
<stop stop-color="#2b4b96" offset="1"/>
</linearGradient>
<linearGradient id="linear-gradient-2" x1="634.51" x2="634.51" y1="125.33" y2="3.46" gradientTransform="translate(.245 52.64)" gradientUnits="userSpaceOnUse">
<stop stop-color="#09b2cf" offset="0"/>
<stop stop-color="#304e97" offset="1"/>
</linearGradient>
<linearGradient id="linear-gradient-3" x1="358" x2="358" y1="129.72" gradientUnits="userSpaceOnUse">
<stop stop-color="#09b2cf" offset="0"/>
<stop stop-color="#178fbb" offset=".2"/>
<stop stop-color="#2273ab" offset=".4"/>
<stop stop-color="#2a5ea0" offset=".6"/>
<stop stop-color="#2e5299" offset=".8"/>
<stop stop-color="#304e97" offset="1"/>
</linearGradient>
</defs>
<title>PlayStation 2 logo</title>
<g transform="translate(-517.25)">
<path class="cls-2" d="m517.5 52.64v9.15h225.37v50.72h-225.37v69.85h234.51v-9.14h-224.52v-51.56h224.52v-69.02z" fill="url(#linear-gradient-2)" fill-rule="evenodd"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -1,6 +1,6 @@
.errorPopup {
font-size: 0.8rem;
background: #bb4444;
background: #b44;
color: var(--a800);
padding: 1rem;
border-radius: 0.4rem;
@@ -20,11 +20,11 @@
.label {
opacity: 1;
transform: initial;
transition: opacity .1s ease, transform .1s ease;
transition: opacity 0.1s ease, transform 0.1s ease;
.isLoading & {
opacity: 0;
transform:scale(0.9);
transform: scale(0.9);
}
}
@@ -41,7 +41,7 @@
opacity: 0;
transform: scale(0.9);
transition: opacity .1s ease, transform .2s ease;
transition: opacity 0.1s ease, transform 0.2s ease;
.isLoading & {
opacity: initial;

View File

@@ -9,10 +9,10 @@ import Button, { Props as ButtonProps } from "./Button"
import LoadingSpinner from "./loading.svg"
export interface Props extends ButtonProps {
onClick: () => Promise<unknown>,
forceLoading?: boolean,
errorPlacement?: import("react-laag/dist/PlacementType").PlacementType,
children: ReactNode,
onClick: () => Promise<unknown>
forceLoading?: boolean
errorPlacement?: import("react-laag/dist/PlacementType").PlacementType
children: ReactNode
}
export default function AsyncButton(props: Props) {

View File

@@ -1,21 +1,22 @@
.btn {
border-radius: 4px;
padding: .6em 1em;
padding: 0.6em 1em;
font-size: .8rem;
font-size: 0.8rem;
font-weight: 500;
user-select: none;
appearance: none;
display: inline-flex;
gap: .5em;
gap: 0.5em;
align-items: center;
background: transparent;
color: var(--a800);
border: 1px solid var(--g500);
transition: color .2s ease, background .15s ease, border-color .15s ease;
transition: color 0.2s ease, background 0.15s ease, border-color 0.15s ease;
&:disabled {
opacity: 0.7;

View File

@@ -28,11 +28,11 @@ const Button = forwardRef(function Button({
})
export type Props = {
children: React.ReactNode,
onClick: (event: React.MouseEvent<HTMLButtonElement>) => void,
className?: string,
disabled?: boolean,
primary?: boolean,
children: React.ReactNode
onClick: (event: React.MouseEvent<HTMLButtonElement>) => void
className?: string
disabled?: boolean
primary?: boolean
}
export default Button

View File

@@ -0,0 +1,60 @@
.container {
padding: 8px;
overflow: auto;
width: 100%;
scrollbar-color: #fff3 transparent;
scrollbar-width: thin;
}
.diff {
border-spacing: 1em 0;
}
.diff,
.log {
display: block;
user-select: text;
border: none;
font-family: monospace;
font-size: 12px;
white-space: pre;
width: 100%;
}
.lineNumber {
color: grey;
font-size: 0.7em;
font-style: italic;
}
.immediate { color: #6d6dff; }
.stack { color: #e3fc45; }
.register { color: #e3fc45; }
.delay_slot {
font-weight: bold;
color: #969896;
}
.diff_change { color: #6d6dff; }
.diff_add { color: #45bd00; }
.diff_remove { color: #c82829; }
.source_filename { font-weight: bold; }
.source_function {
font-weight: bold;
text-decoration: underline;
}
.source_other { font-style: italic; }
.source_line_num { font-style: italic; }
.rotation0 { color: magenta; }
.rotation1 { color: cyan; }
.rotation2 { color: rgb(0, 212, 0); }
.rotation3 { color: red; }
.rotation4 { color: lightyellow; }
.rotation5 { color: lightpink; }
.rotation6 { color: lightcyan; }
.rotation7 { color: lightgreen; }
.rotation8 { color: grey; }

View File

@@ -1,3 +1,4 @@
/* eslint css-modules/no-unused-class: off */
import * as api from "../../lib/api"
@@ -18,7 +19,7 @@ function FormatDiffText({ texts }: { texts: api.DiffText[] }) {
}
export type Props = {
compilation: api.Compilation,
compilation: api.Compilation
}
export default function Diff({ compilation }: Props) {

View File

@@ -0,0 +1,4 @@
import Diff, { Props as DiffProps } from "./Diff"
export type Props = DiffProps
export default Diff

View File

@@ -14,8 +14,8 @@ import getTheme from "./monacoTheme"
const isMobile = mobile()
interface Props extends MonacoEditorProps {
bubbleSuspense?: boolean,
useLoadingSpinner?: boolean,
bubbleSuspense?: boolean
useLoadingSpinner?: boolean
}
const MonacoEditor = isMobile ? null : dynamic(() => import("./MonacoEditor"), {

View File

@@ -1,9 +1,9 @@
.container {
flex-grow: 1;
user-select: initial;
/* fix border-radius overflow */
overflow: hidden;
& :global(.monaco-editor),
& :global(.monaco-editor-background),
& :global(.margin),
@@ -15,6 +15,14 @@
& :global(.monaco-hover) {
display: none;
}
& :global(.mac .monaco-mouse-cursor-text) {
cursor: text !important;
}
& :global(.view-lines) {
user-select: initial;
}
}
.readonly {

View File

@@ -1,8 +1,7 @@
import { useEffect, useState, useRef } from "react"
import { useEffect, useState, useRef, MutableRefObject } from "react"
import classNames from "classnames"
import * as monaco from "monaco-editor"
import { editor } from "monaco-editor"
import { editor, languages } from "monaco-editor"
import * as c from "./language/c"
import * as mips from "./language/mips"
@@ -15,13 +14,13 @@ if (typeof window === "undefined") {
throw new Error("Editor component does not work with SSR, use next/dynamic with { ssr: false }")
}
monaco.languages.register({ id: "decompme_c" })
monaco.languages.setLanguageConfiguration("decompme_c", c.conf)
monaco.languages.setMonarchTokensProvider("decompme_c", c.language)
languages.register({ id: "decompme_c" })
languages.setLanguageConfiguration("decompme_c", c.conf)
languages.setMonarchTokensProvider("decompme_c", c.language)
monaco.languages.register({ id: "decompme_mips" })
monaco.languages.setLanguageConfiguration("decompme_mips", mips.conf)
monaco.languages.setMonarchTokensProvider("decompme_mips", mips.language)
languages.register({ id: "decompme_mips" })
languages.setLanguageConfiguration("decompme_mips", mips.conf)
languages.setMonarchTokensProvider("decompme_mips", mips.language)
function convertLanguage(language: string) {
if (language === "c")
@@ -32,30 +31,34 @@ function convertLanguage(language: string) {
return "plaintext"
}
export type EditorInstance = editor.IStandaloneCodeEditor;
export type Props = {
className?: string,
className?: string
// This is a controlled component
value: string,
onChange?: (value: string) => void,
value: string
onChange?: (value: string) => void
instanceRef?: MutableRefObject<editor.IStandaloneCodeEditor>
// Options
language: "c" | "mips",
lineNumbers?: boolean,
showMargin?: boolean,
padding?: number, // css
language: "c" | "mips"
lineNumbers?: boolean
showMargin?: boolean
padding?: number // css
}
export default function Editor({ value, onChange, className, showMargin, padding, language, lineNumbers }: Props) {
export default function Editor({ value, onChange, className, showMargin, padding, language, lineNumbers, instanceRef }: Props) {
const isReadOnly = typeof onChange === "undefined"
const containerRef = useRef<HTMLDivElement>(null)
const [editor, setEditor] = useState<editor.IStandaloneCodeEditor | null>(null)
const [editorInstance, setEditorInstance] = useState<editor.IStandaloneCodeEditor | null>(null)
// Effect to set up the editor. This is run once when the component is mounted.
useEffect(() => {
monaco.editor.defineTheme("custom", monacoTheme())
editor.defineTheme("custom", monacoTheme())
const editor = monaco.editor.create(containerRef.current, {
const editorInstance = editor.create(containerRef.current, {
language: convertLanguage(language),
value,
theme: "custom",
@@ -83,12 +86,14 @@ export default function Editor({ value, onChange, className, showMargin, padding
glyphMargin: !!showMargin,
folding: !!showMargin,
lineDecorationsWidth: padding ?? (showMargin ? 10 : 0),
lineNumbersMinChars: showMargin ? 5 : 0,
lineNumbersMinChars: showMargin ? 2 : 0,
automaticLayout: true,
})
setEditor(editor)
setEditorInstance(editorInstance)
if (instanceRef)
instanceRef.current = editorInstance
const model = editor.getModel()
const model = editorInstance.getModel()
if (model) {
model.onDidChangeContent(() => {
if (onChange)
@@ -98,33 +103,42 @@ export default function Editor({ value, onChange, className, showMargin, padding
console.error("monaco editor has no model")
}
return () => editor.dispose()
return () => editorInstance.dispose()
}, []) // eslint-disable-line react-hooks/exhaustive-deps
// Update value.
useEffect(() => {
const model = editor?.getModel()
const model = editorInstance?.getModel()
// Only update the model value if it is different; otherwise, the
// model state will be reset every time the user types!
if (model && model.getValue() !== value) {
console.log("editor value reset")
console.warn("editor value reset")
model.setValue(value)
}
}, [editor, value])
}, [editorInstance, value])
// Update language.
useEffect(() => {
const model = editor?.getModel()
const model = editorInstance?.getModel()
if (model) {
monaco.editor.setModelLanguage(model, convertLanguage(language))
editor.setModelLanguage(model, convertLanguage(language))
}
}, [editor, language])
}, [editorInstance, language])
useEffect(() => {
editor?.updateOptions({ lineNumbers: lineNumbers ? "on" : "off" })
}, [editor, lineNumbers])
editorInstance?.updateOptions({ lineNumbers: lineNumbers ? "on" : "off" })
}, [editorInstance, lineNumbers])
useEffect(() => {
editorInstance?.updateOptions({
glyphMargin: !!showMargin,
folding: !!showMargin,
lineDecorationsWidth: padding ?? (showMargin ? 10 : 0),
lineNumbersMinChars: showMargin ? 2 : 0,
})
}, [editorInstance, padding, showMargin])
return <div
ref={containerRef}
@@ -154,7 +168,7 @@ export default function Editor({ value, onChange, className, showMargin, padding
e.stopPropagation()
if (e.shiftKey)
editor?.trigger("", "editor.action.quickCommand", "")
editorInstance?.trigger("", "editor.action.quickCommand", "")
//console.log(editor.getSupportedActions())
}
}

View File

@@ -3,7 +3,7 @@ import type { languages } from "monaco-editor"
export const conf: languages.LanguageConfiguration = {
comments: {
lineComment: "//",
blockComment: ["/*", "*/"]
blockComment: ["/*", "*/"],
},
brackets: [
["{", "}"],
@@ -15,20 +15,20 @@ export const conf: languages.LanguageConfiguration = {
{ open: "{", close: "}" },
{ open: "(", close: ")" },
{ open: "'", close: "'", notIn: ["string", "comment"] },
{ open: "\"", close: "\"", notIn: ["string"] }
{ open: "\"", close: "\"", notIn: ["string"] },
],
surroundingPairs: [
{ open: "{", close: "}" },
{ open: "[", close: "]" },
{ open: "(", close: ")" },
{ open: "\"", close: "\"" },
{ open: "'", close: "'" }
{ open: "'", close: "'" },
],
folding: {
markers: {
start: new RegExp("^\\s*#pragma\\s+region\\b"),
end: new RegExp("^\\s*#pragma\\s+endregion\\b")
}
end: new RegExp("^\\s*#pragma\\s+endregion\\b"),
},
},
}
@@ -40,7 +40,7 @@ export const language: languages.IMonarchLanguage = {
{ token: "delimiter.curly", open: "{", close: "}" },
{ token: "delimiter.parenthesis", open: "(", close: ")" },
{ token: "delimiter.square", open: "[", close: "]" },
{ token: "delimiter.angle", open: "<", close: ">" }
{ token: "delimiter.angle", open: "<", close: ">" },
],
keywords: [
@@ -309,9 +309,9 @@ export const language: languages.IMonarchLanguage = {
cases: {
"@keywords": { token: "keyword.$0" },
"@types": { token: "storage.type.$0" },
"@default": "identifier"
}
}
"@default": "identifier",
},
},
],
// The preprocessor checks must be before whitespace as they check /^\s*#/ which
@@ -337,9 +337,9 @@ export const language: languages.IMonarchLanguage = {
cases: {
"@comparisonOperators": { token: "operator.comparison" },
"@operators": { token: "operator" },
"@default": { token: "delimiter" }
}
}
"@default": { token: "delimiter" },
},
},
],
// numbers
@@ -361,7 +361,7 @@ export const language: languages.IMonarchLanguage = {
// characters
[/'[^\\']'/, "string"],
[/(')(@escapes)(')/, ["string", "string.escape", "string"]],
[/'/, "string.invalid"]
[/'/, "string.invalid"],
],
whitespace: [
@@ -369,33 +369,33 @@ export const language: languages.IMonarchLanguage = {
[/\/\*\*(?!\/)/, "comment.doc", "@doccomment"],
[/\/\*/, "comment", "@comment"],
[/\/\/.*\\$/, "comment", "@linecomment"],
[/\/\/.*$/, "comment"]
[/\/\/.*$/, "comment"],
],
comment: [
[/[^/*]+/, "comment"],
[/\*\//, "comment", "@pop"],
[/[/*]/, "comment"]
[/[/*]/, "comment"],
],
//For use with continuous line comments
linecomment: [
[/.*[^\\]$/, "comment", "@pop"],
[/[^]+/, "comment"]
[/[^]+/, "comment"],
],
//Identical copy of comment above, except for the addition of .doc
doccomment: [
[/[^/*]+/, "comment.doc"],
[/\*\//, "comment.doc", "@pop"],
[/[/*]/, "comment.doc"]
[/[/*]/, "comment.doc"],
],
string: [
[/[^\\"]+/, "string"],
[/@escapes/, "string.escape"],
[/\\./, "string.escape.invalid"],
[/"/, "string", "@pop"]
[/"/, "string", "@pop"],
],
raw: [
@@ -407,13 +407,13 @@ export const language: languages.IMonarchLanguage = {
"string.raw",
"string.raw.end",
"string.raw.end",
{ token: "string.raw.end", next: "@pop" }
{ token: "string.raw.end", next: "@pop" },
],
"@default": ["string.raw", "string.raw", "string.raw", "string.raw"]
}
}
"@default": ["string.raw", "string.raw", "string.raw", "string.raw"],
},
},
],
[/.*/, "string.raw"]
[/.*/, "string.raw"],
],
annotation: [
@@ -422,7 +422,7 @@ export const language: languages.IMonarchLanguage = {
[/[a-zA-Z0-9_]+/, "annotation"],
[/[,:]/, "delimiter"],
[/[()]/, "@brackets"],
[/\]\s*\]/, { token: "annotation", next: "@pop" }]
[/\]\s*\]/, { token: "annotation", next: "@pop" }],
],
include: [
@@ -432,8 +432,8 @@ export const language: languages.IMonarchLanguage = {
"",
"keyword.directive.include.begin",
"string.include.identifier",
{ token: "keyword.directive.include.end", next: "@pop" }
] as languages.IMonarchLanguageAction
{ token: "keyword.directive.include.end", next: "@pop" },
] as languages.IMonarchLanguageAction,
],
[
/(\s*)(")([^"]*)(")/,
@@ -441,9 +441,9 @@ export const language: languages.IMonarchLanguage = {
"",
"keyword.directive.include.begin",
"string.include.identifier",
{ token: "keyword.directive.include.end", next: "@pop" }
] as languages.IMonarchLanguageAction
]
]
}
{ token: "keyword.directive.include.end", next: "@pop" },
] as languages.IMonarchLanguageAction,
],
],
},
}

View File

@@ -3,14 +3,14 @@ import type { languages } from "monaco-editor"
export const conf: languages.LanguageConfiguration = {
comments: {
lineComment: "#",
blockComment: ["/*", "*/"]
blockComment: ["/*", "*/"],
},
brackets: [
["(", ")"],
],
autoClosingPairs: [
{ open: "(", close: ")" },
{ open: "\"", close: "\"", notIn: ["string"] }
{ open: "\"", close: "\"", notIn: ["string"] },
],
surroundingPairs: [
{ open: "(", close: ")" },
@@ -32,7 +32,7 @@ export const language: languages.IMonarchLanguage = {
instructions: [
"lb", "lbu", "ld", "ldl", "ldr", "lh", "lhu", "ll", "lld", "lw", "lwl", "lwr", "lwu", "sb", "sc", "scd", "sd", "sdl", "sdr", "sh", "sw", "swl", "swr", "sync", "add", "addi", "addiu", "addu", "and", "andi", "dadd", "daddi", "daddiu", "daddu", "ddiv", "ddivu", "div", "divu", "dmult", "dmultu", "dsll", "dsll32", "dsllv", "dsra", "dsra32", "dsrav", "dsrl", "dsrl32", "dsrlv", "dsub", "dsubu", "lui", "mfhi", "mflo", "mthi", "mtlo", "mult", "multu", "nor", "or", "ori", "sll", "sllv", "slt", "slti", "sltiu", "sltu", "sra", "srav", "srl", "srlv", "sub", "subu", "xor", "xori", "beq", "beql", "bgez", "bgezal", "bgezall", "bgezl", "bgtz", "bgtzl", "blez", "blezl", "bltz", "bltzal", "bltzall", "bltzl", "bne", "bnel", "j", "jal", "jalr", "jr", "break", "syscall", "teq", "teqi", "tge", "tgei", "tgeiu", "tgeu", "tlt", "tlti", "tltiu", "tltu", "tne", "tnei", "cache", "dmfc0", "dmtc0", "eret", "mfc0", "mtc0", "tlbp", "tlbr", "tlbwi", "tlbwr", "bc1f", "bc1fl", "bc1t", "bc1tl", "cfc1", "ctc1", "dmfc1", "dmtc1", "ldc1", "lwc1", "mfc1", "mtc1", "sdc1", "swc1",
"beqz", "bnez", "negu",
"beqz", "bnez", "negu", "nop",
],
registers: [
@@ -58,18 +58,18 @@ export const language: languages.IMonarchLanguage = {
"jal": { token: "function" },
"@instructions": { token: "support.function.$0" },
"@keywords": { token: "keyword.$0" },
"@default": "identifier"
}
}
"@default": "identifier",
},
},
],
[
/\$\w+/,
{
cases: {
"@registers": { token: "entity.name.register.$0" },
"@default": "identifier"
}
}
"@default": "identifier",
},
},
],
[/%(hi|lo)/, "macro"],
[/\.\w+/, { token: "keyword.directive" }],
@@ -96,33 +96,33 @@ export const language: languages.IMonarchLanguage = {
// characters
[/'[^\\']'/, "string"],
[/(')(@escapes)(')/, ["string", "string.escape", "string"]],
[/'/, "string.invalid"]
[/'/, "string.invalid"],
],
whitespace: [
[/[ \t\r\n]+/, ""],
[/\/\*/, "comment", "@comment"],
[/#.*\\$/, "comment", "@linecomment"],
[/#.*$/, "comment"]
[/#.*$/, "comment"],
],
comment: [
[/[^/*]+/, "comment"],
[/\*\//, "comment", "@pop"],
[/[/*]/, "comment"]
[/[/*]/, "comment"],
],
//For use with continuous line comments
linecomment: [
[/.*[^#]$/, "comment", "@pop"],
[/[^]+/, "comment"]
[/[^]+/, "comment"],
],
string: [
[/[^\\"]+/, "string"],
[/@escapes/, "string.escape"],
[/\\./, "string.escape.invalid"],
[/"/, "string", "@pop"]
[/"/, "string", "@pop"],
],
raw: [
@@ -134,13 +134,13 @@ export const language: languages.IMonarchLanguage = {
"string.raw",
"string.raw.end",
"string.raw.end",
{ token: "string.raw.end", next: "@pop" }
{ token: "string.raw.end", next: "@pop" },
],
"@default": ["string.raw", "string.raw", "string.raw", "string.raw"]
}
}
"@default": ["string.raw", "string.raw", "string.raw", "string.raw"],
},
},
],
[/.*/, "string.raw"]
[/.*/, "string.raw"],
],
include: [
@@ -150,8 +150,8 @@ export const language: languages.IMonarchLanguage = {
"",
"keyword.directive.include.begin",
"string.include.identifier",
{ token: "keyword.directive.include.end", next: "@pop" }
] as languages.IMonarchLanguageAction
{ token: "keyword.directive.include.end", next: "@pop" },
] as languages.IMonarchLanguageAction,
],
[
/(\s*)(")([^"]*)(")/,
@@ -159,9 +159,9 @@ export const language: languages.IMonarchLanguage = {
"",
"keyword.directive.include.begin",
"string.include.identifier",
{ token: "keyword.directive.include.end", next: "@pop" }
] as languages.IMonarchLanguageAction
]
]
}
{ token: "keyword.directive.include.end", next: "@pop" },
] as languages.IMonarchLanguageAction,
],
],
},
}

View File

@@ -23,7 +23,7 @@ export default function getTheme(): editor.IStandaloneThemeData {
{ "token": "entity.other.attribute-name", "foreground": "FF4A98" },
{ "token": "support.function", "foreground": "45B8FF" },
{ "token": "storage", "foreground": "45B8FF" },
{ "token": "macro", "foreground": "3bff6c" }
{ "token": "macro", "foreground": "3bff6c" },
],
"colors": {
"editor.foreground": "#8599b3",
@@ -32,7 +32,7 @@ export default function getTheme(): editor.IStandaloneThemeData {
"editor.lineHighlightBackground": "#ccccff07",
"editorCursor.foreground": "#c9cbfc",
"editorWhitespace.foreground": "#c9cbfc11",
"editorLineNumber.foreground":"#ccccff31"
}
"editorLineNumber.foreground":"#ccccff31",
},
}
}

View File

@@ -19,12 +19,12 @@
.links {
display: flex;
justify-content: space-around;
justify-content: space-evenly;
}
.link {
display: inline-flex;
gap: .5em;
gap: 0.5em;
align-items: center;
padding: 1em;
@@ -37,7 +37,7 @@
.commitHash {
text-align: center;
font-size: .7em;
font-size: 0.7em;
background: var(--g200);
color: var(--g600);

View File

@@ -31,6 +31,12 @@ export default function Footer() {
Chat on Discord
</a>
</Link>
<Link href="/credits">
<a className={styles.link}>
Credits
</a>
</Link>
</div>
<div className={styles.commitHash}>

View File

@@ -8,7 +8,3 @@
.avatar {
border-radius: 24px;
}
.loginContainer, .user {
margin-right: .5em;
}

View File

@@ -11,7 +11,7 @@ import GitHubLoginButton from "../GitHubLoginButton"
import styles from "./LoginState.module.scss"
export type Props = {
onChange: (user: api.AnonymousUser | api.User) => void,
onChange: (user: api.AnonymousUser | api.User) => void
}
export default function LoginState({ onChange }: Props) {
@@ -39,11 +39,11 @@ export default function LoginState({ onChange }: Props) {
height={24}
priority
/>}
<span>{user.name}</span>
{/*<span>{user.username}</span>*/}
</a>
</Link>
} else {
return <div className={styles.loginContainer}>
return <div>
<GitHubLoginButton label="Sign in" />
</div>
}

View File

@@ -8,15 +8,19 @@
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: .5em;
gap: 0.5em;
padding: 1vh 0.5vw;
padding: 4px;
background: var(--g400);
border-bottom: 1px solid var(--g600);
font-size: 90%;
font-weight: 600;
@media screen and (min-width: 800px) and (min-height: 800px) {
padding: 8px;
}
}
.item {
@@ -26,7 +30,7 @@
height: 100%;
padding: 0 8px;
opacity: 1.0;
opacity: 1;
&:hover {
opacity: 0.8;
@@ -36,7 +40,7 @@
.logo {
@extend .item;
margin: 0 .5vw;
margin: 0 0.5vw;
padding: 0;
svg {
@@ -46,11 +50,30 @@
/* Frog blinks when you load the page or stop hovering */
&:not(:hover) {
@keyframes blink {
0% { transform: scaleY(1.0); opacity: 1 }
25% { transform: scaleY(0.1); opacity: 0 }
50% { transform: scaleY(1.0); opacity: 1 }
75% { transform: scaleY(0.1); opacity: 0 }
100% { transform: scaleY(1.0); opacity: 1 }
0% {
transform: scaleY(1);
opacity: 1;
}
25% {
transform: scaleY(0.2);
opacity: 0;
}
50% {
transform: scaleY(1);
opacity: 1;
}
75% {
transform: scaleY(0.2);
opacity: 0;
}
100% {
transform: scaleY(1);
opacity: 1;
}
}
:global(.frog_svg__pupilR),
@@ -58,7 +81,7 @@
:global(.frog_svg__eyeR),
:global(.frog_svg__eyeL) {
transform-origin: 0 7px;
animation: blink .4s 2s ease;
animation: blink 0.4s 2s ease;
@media (prefers-reduced-motion) {
animation: none;
@@ -70,7 +93,3 @@
.grow {
flex: 1;
}
.link:hover, .linkActive {
opacity: 0.8;
}

View File

@@ -4,11 +4,11 @@ import Frog from "./frog.svg"
import LoginState, { Props as LoginStateProps } from "./LoginState"
import styles from "./Nav.module.scss"
const onUserChangeNop = (_user: any) => {}
const onUserChangeNop: LoginStateProps["onChange"] = _user => {}
export type Props = {
children?: React.ReactNode,
onUserChange?: LoginStateProps["onChange"],
children?: React.ReactNode
onUserChange?: LoginStateProps["onChange"]
}
export default function Nav({ children, onUserChange }: Props) {
@@ -19,7 +19,7 @@ export default function Nav({ children, onUserChange }: Props) {
</a>
</Link>
{children ?? <Link href="/scratch">
{children ?? <Link href="/scratch/new">
<a className={styles.item}>New scratch</a>
</Link>}

View File

@@ -0,0 +1,36 @@
.badge {
display: inline-flex;
flex-direction: row;
align-items: center;
justify-content: center;
gap: 0.5em;
min-width: 2em;
height: 2em;
font-size: 0.8em;
font-weight: 400;
padding: 0.4em 0.8em;
border-radius: 999px;
color: var(--a800);
background: var(--a100);
// circle when using icons
&.error,
&.match {
padding: 0;
}
&.error {
background: #b93636;
}
&.match {
background: var(--accent);
}
}
.icon {
width: 1.2em;
}

View File

@@ -0,0 +1,24 @@
import { AlertIcon, CheckIcon } from "@primer/octicons-react"
import classNames from "classnames"
import styles from "./ScoreBadge.module.scss"
export type Props = {
score: number
}
export default function ScoreBadge({ score }: Props) {
if (score === -1) {
return <div className={classNames(styles.badge, { [styles.error]: true })}>
<AlertIcon className={styles.icon} />
</div>
} else if (score === 0) {
return <div className={classNames(styles.badge, { [styles.match]: true })}>
<CheckIcon className={styles.icon} />
</div>
} else {
return <div className={styles.badge} aria-label="Score">
{score}
</div>
}
}

View File

@@ -0,0 +1,100 @@
.container {
display: flex;
flex-direction: column;
gap: 1em;
cursor: default;
height: 100%;
}
.rule {
border: 0;
background: var(--g300);
height: 1px;
}
.label {
display: block;
margin: 0;
padding: 10px;
color: var(--g1000);
font-size: 0.9em;
font-weight: 500;
small {
color: var(--g1000);
font-size: 0.8em;
}
}
.horizontalField {
display: flex;
align-items: center;
.label {
width: 100px;
}
}
.scratchLinkContainer {
display: inline-flex;
align-items: center;
justify-content: center;
height: 100%;
color: var(--g800);
}
.scratchLink {
cursor: pointer;
color: var(--g1400);
&:hover {
color: var(--g1700);
text-decoration: underline;
}
}
.scratchLinkByText {
padding-left: 6px;
padding-right: 4px;
}
.textArea {
flex: 1;
padding: 10px;
border: 1px solid transparent;
border-radius: 4px;
background: transparent;
outline: none !important;
color: var(--g1800);
resize: none;
width: 100%;
cursor: auto;
user-select: text;
&::-webkit-input-placeholder {
color: var(--g500);
}
&:not(:disabled) {
cursor: auto;
transition: border-color 0.15s;
&:hover,
&:active {
border-color: var(--g300);
}
}
}
.grow {
display: flex;
flex-direction: inherit;
flex: 1;
}

View File

@@ -0,0 +1,54 @@
import Link from "next/link"
import * as api from "../../lib/api"
import UserLink from "../user/UserLink"
import styles from "./AboutScratch.module.scss"
function ScratchLink({ slug }: { slug: string }) {
const { scratch } = api.useScratch(slug)
return <span className={styles.scratchLinkContainer}>
<Link href={`/scratch/${scratch.slug}`}>
<a className={styles.scratchLink}>
{scratch.name || "Untitled scratch"}
</a>
</Link>
<span className={styles.scratchLinkByText}>by</span>
<UserLink user={scratch.owner} />
</span>
}
export type Props = {
scratch: api.Scratch
setScratch?: (scratch: Partial<api.Scratch>) => void
}
export default function AboutScratch({ scratch, setScratch }: Props) {
return <div className={styles.container}>
<div>
<div className={styles.horizontalField}>
<p className={styles.label}>Owner</p>
<UserLink user={scratch.owner} />
</div>
{scratch.parent &&<div className={styles.horizontalField}>
<p className={styles.label}>Fork of</p>
<ScratchLink slug={scratch.parent} />
</div>}
</div>
<hr className={styles.rule} />
{setScratch || scratch.description ? <div className={styles.grow}>
<p className={styles.label}>Description</p>
<textarea
className={styles.textArea}
value={scratch.description}
disabled={!setScratch}
onChange={event => setScratch && setScratch({ description: event.target.value })}
maxLength={5000}
placeholder="Add any notes about the scratch here"
/>
</div> : <div />}
</div>
}

View File

@@ -0,0 +1,95 @@
.container {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
}
.resizer {
flex: 1;
overflow: hidden;
}
.context {
display: flex;
flex-direction: column;
}
.diffSection {
display: flex;
flex-direction: column;
scrollbar-color: #fff3 transparent;
scrollbar-width: thin;
}
.chooseACompiler {
display: flex;
flex-direction: column;
}
.chooseACompilerActions {
padding: 1em;
align-self: center;
}
.editor {
height: 100%;
}
.toolbar {
display: flex;
flex-direction: row;
align-items: center;
justify-content: initial;
gap: 4px;
padding: 4px;
background: var(--g300);
border-bottom: 1px solid var(--a100);
@media screen and (min-width: 800px) and (min-height: 800px) {
gap: 8px;
padding: 8px;
}
}
.scratchName {
flex: 1;
min-width: 0;
height: 100%;
padding: 4px 8px;
border: 1px solid transparent;
border-radius: 4px;
background: transparent;
outline: none !important;
color: var(--g1800);
text-overflow: ellipsis;
transition: border-color 0.15s;
&::-webkit-input-placeholder {
color: var(--g800);
}
&:not(:disabled) {
&:hover,
&:active {
border-color: var(--g400);
}
}
}
.diffTab {
display: flex;
overflow: auto;
}
.about {
padding: 16px;
max-width: 500px;
}

View File

@@ -1,6 +1,4 @@
import { useState, useEffect, useCallback, useRef } from "react"
import Link from "next/link"
import { useState, useEffect, useRef } from "react"
import { RepoForkedIcon, SyncIcon, UploadIcon, ArrowRightIcon } from "@primer/octicons-react"
import * as resizer from "react-simple-resizer"
@@ -10,14 +8,15 @@ import * as api from "../../lib/api"
import { useSize, useWarnBeforeUnload } from "../../lib/hooks"
import AsyncButton from "../AsyncButton"
import Button from "../Button"
import CompilerButton from "../compiler/CompilerButton"
import CompilerOpts, { CompilerOptsT } from "../compiler/CompilerOpts"
import Diff from "../diff/Diff"
import Diff from "../Diff"
import Editor from "../Editor"
import { EditorInstance } from "../Editor/MonacoEditor"
import ScoreBadge from "../ScoreBadge"
import Tabs, { Tab } from "../Tabs"
import UserLink from "../user/UserLink"
import styles from "./Scratch.module.css"
import AboutScratch from "./AboutScratch"
import styles from "./Scratch.module.scss"
const LEFT_PANE_MIN_WIDTH = 400
const RIGHT_PANE_MIN_WIDTH = 400
@@ -25,8 +24,8 @@ const RIGHT_PANE_MIN_WIDTH = 400
let isClaiming = false
function ChooseACompiler({ arch, onCommit }: {
arch: string,
onCommit: (opts: CompilerOptsT) => void,
arch: string
onCommit: (opts: CompilerOptsT) => void
}) {
const [compiler, setCompiler] = useState<CompilerOptsT>()
@@ -47,26 +46,18 @@ function ChooseACompiler({ arch, onCommit }: {
</div>
}
function ScratchLink({ slug }: { slug: string }) {
const { scratch } = api.useScratch(slug)
if (!scratch) {
return <span />
}
return <Link href={`/scratch/${scratch.slug}`}>
<a className={styles.scratchLink}>
{nameScratch(scratch)}
</a>
</Link>
}
function renderRightTabs({ compilation }: {
compilation?: api.Compilation,
compilation?: api.Compilation
}): React.ReactElement<typeof Tab>[] {
console.log(compilation)
return [
<Tab key="diff" label="Diff">
<Tab
key="diff"
label={<>
Diff
{compilation && <ScoreBadge score={compilation?.diff_output?.current_score ?? -1} />}
</>}
className={styles.diffTab}
>
{compilation && <Diff compilation={compilation} />}
</Tab>,
/*<Tab key="options" label="Options">
@@ -75,17 +66,27 @@ function renderRightTabs({ compilation }: {
]
}
function renderLeftTabs({ scratch, isSaved, setScratch, saveScratch, forkScratch, compile }: {
scratch: api.Scratch,
isSaved: boolean,
setScratch: (s: Partial<api.Scratch>) => void,
saveScratch: () => Promise<void>,
forkScratch: () => Promise<void>,
compile: () => Promise<void>,
function renderLeftTabs({ scratch, setScratch }: {
scratch: api.Scratch
setScratch: (s: Partial<api.Scratch>) => void
}): React.ReactElement<typeof Tab>[] {
const sourceEditor = useRef<EditorInstance>() // eslint-disable-line react-hooks/rules-of-hooks
const contextEditor = useRef<EditorInstance>() // eslint-disable-line react-hooks/rules-of-hooks
return [
<Tab key="source" label="Source code">
<Tab key="about" label="About" className={styles.about}>
<AboutScratch
scratch={scratch}
setScratch={scratch.owner?.is_you ? setScratch : null}
/>
</Tab>,
<Tab
key="source"
label="Source code"
onSelect={() => sourceEditor.current.focus()}
>
<Editor
instanceRef={sourceEditor}
className={styles.editor}
language="c"
value={scratch.source_code}
@@ -97,8 +98,14 @@ function renderLeftTabs({ scratch, isSaved, setScratch, saveScratch, forkScratch
bubbleSuspense
/>
</Tab>,
<Tab key="context" label="Context" className={styles.context}>
<Tab
key="context"
label="Context"
className={styles.context}
onSelect={() => contextEditor.current.focus()}
>
<Editor
instanceRef={contextEditor}
className={styles.editor}
language="c"
value={scratch.context}
@@ -110,50 +117,38 @@ function renderLeftTabs({ scratch, isSaved, setScratch, saveScratch, forkScratch
bubbleSuspense
/>
</Tab>,
<Tab key="about" label={scratch.owner.is_you ? "Scratch settings" : "About this scratch" }>
<div className={styles.metadata}>
{scratch.owner && <div>
Owner
<UserLink user={scratch.owner} />
</div>}
{scratch.parent && <div>
Fork of <ScratchLink slug={scratch.parent} />
</div>}
</div>
<Tab key="settings" label="Scratch settings">
<CompilerOpts
arch={scratch.arch}
value={scratch}
onChange={setScratch}
/>
</Tab>,
]
}
export function nameScratch({ slug, owner }: api.Scratch): string {
if (owner?.is_you) {
return "Your scratch"
} else if (owner && !api.isAnonUser(owner) && owner?.name) {
return `${owner?.name}'s scratch`
} else {
return "Untitled scratch"
}
}
export type Props = {
slug: string,
tryClaim?: boolean, // note: causes page reload after claiming
slug: string
tryClaim?: boolean // note: causes page reload after claiming
}
export default function Scratch({ slug, tryClaim }: Props) {
const container = useSize<HTMLDivElement>()
const { scratch, savedScratch, isSaved, setScratch, saveScratch } = api.useScratch(slug)
const { compilation, isCompiling, compile } = api.useCompilation(scratch, savedScratch, true)
const forkScratch = api.useForkScratchAndGo(scratch)
const forkScratch = api.useForkScratchAndGo(savedScratch, scratch)
const [leftTab, setLeftTab] = useState("source")
const [rightTab, setRightTab] = useState("diff")
const [isForking, setIsForking] = useState(false)
// TODO: remove once scratch.compiler is no longer nullable
const setCompilerOpts = ({ compiler, cc_opts }: CompilerOptsT) => {
setScratch({
compiler,
cc_opts,
})
saveScratch()
if (scratch.owner?.is_you)
saveScratch()
}
useEffect(() => {
@@ -173,7 +168,7 @@ export default function Scratch({ slug, tryClaim }: Props) {
useDeepCompareEffect(() => {
if (scratch) {
document.title = nameScratch(scratch)
document.title = scratch.name || "Untitled scratch"
if (!isSaved) {
document.title += " (unsaved changes)"
@@ -183,7 +178,12 @@ export default function Scratch({ slug, tryClaim }: Props) {
}
}, [scratch || {}, isSaved])
useWarnBeforeUnload(!isSaved, "You have unsaved changes. Are you sure you want to leave?")
useWarnBeforeUnload(
!isSaved && !isForking,
scratch.owner?.is_you
? "You have not saved your changes to this scratch. Discard changes?"
: "You have edited this scratch but not saved it in a fork. Discard changes?",
)
// Claim the scratch
if (tryClaim && !savedScratch?.owner && typeof window !== "undefined") {
@@ -202,18 +202,13 @@ export default function Scratch({ slug, tryClaim }: Props) {
})
.catch(console.error)
.then(() => {
// Reload the entire page
window.location.href = window.location.href
window.location.reload()
})
}
const leftTabs = renderLeftTabs({
scratch,
isSaved,
setScratch,
saveScratch,
forkScratch,
compile,
})
const rightTabs = renderRightTabs({
@@ -222,11 +217,16 @@ export default function Scratch({ slug, tryClaim }: Props) {
return <div ref={container.ref} className={styles.container}>
<div className={styles.toolbar}>
<div className={styles.scratchName}>
{nameScratch(scratch)}
</div>
<span className={styles.grow} />
<input
className={styles.scratchName}
type="text"
value={scratch.name}
onChange={event => setScratch({ name: event.target.value })}
disabled={!scratch.owner?.is_you}
spellCheck="false"
maxLength={100}
placeholder={"Untitled scratch"}
/>
<AsyncButton onClick={compile} forceLoading={isCompiling}>
<SyncIcon size={16} /> Compile
@@ -234,12 +234,15 @@ export default function Scratch({ slug, tryClaim }: Props) {
{scratch.owner?.is_you && <AsyncButton onClick={() => {
return Promise.all([
saveScratch(),
compile(),
compile().catch(() => {}), // Ignore errors
])
}} disabled={isSaved}>
<UploadIcon size={16} /> Save
</AsyncButton>}
<AsyncButton onClick={forkScratch}>
<AsyncButton onClick={() => {
setIsForking(true)
return forkScratch()
}} primary={!isSaved && !scratch.owner?.is_you}>
<RepoForkedIcon size={16} /> Fork
</AsyncButton>
</div>

View File

@@ -0,0 +1,4 @@
import Scratch, { Props as ScratchProps } from "./Scratch"
export type Props = ScratchProps
export default Scratch

View File

@@ -10,7 +10,7 @@
padding: 8px 10px;
font-size: .8rem;
font-size: 0.8rem;
user-select: none;
}
@@ -24,7 +24,8 @@
padding-right: 2em;
}
.group option, .group optgroup {
.group option,
.group optgroup {
color: var(--g1600);
background: var(--g200);
}
@@ -32,7 +33,7 @@
.icon {
pointer-events: none;
position: absolute;
right: .6em;
right: 0.6em;
top: 50%;
transform: translateY(-50%);
}

View File

@@ -5,9 +5,9 @@ import { ChevronDownIcon } from "@primer/octicons-react"
import styles from "./Select.module.scss"
export type Props = {
className?: string,
onChange: ChangeEventHandler<HTMLSelectElement>,
children: ReactNode,
className?: string
onChange: ChangeEventHandler<HTMLSelectElement>
children: ReactNode
}
export default function Select({ onChange, children, className }: Props) {

View File

@@ -3,10 +3,10 @@ import { ChevronDownIcon } from "@primer/octicons-react"
import styles from "./Select.module.scss"
export type Props = {
options: { [key: string]: string },
value: string,
className?: string,
onChange: (value: string) => void,
options: { [key: string]: string }
value: string
className?: string
onChange: (value: string) => void
}
export default function Select({ options, value, onChange, className }: Props) {

View File

@@ -3,11 +3,14 @@
flex-direction: column;
flex: 1;
height: 0;
}
.tabButtons {
position: relative;
height: 48px;
min-height: 48px;
white-space: nowrap;
overflow-x: auto;
border-bottom: 1px solid var(--a100);
}
@@ -16,7 +19,7 @@
position: relative;
display: inline-flex;
gap: .5em;
gap: 0.5em;
align-items: center;
justify-content: center;
@@ -24,7 +27,7 @@
min-width: 60px;
height: 100%;
font-size: .8rem;
font-size: 0.8rem;
font-weight: 500;
user-select: none;
@@ -34,7 +37,7 @@
background: transparent;
border: 0;
transition: color .2s ease;
transition: color 0.2s ease;
&:disabled {
opacity: 0.7;
@@ -44,7 +47,8 @@
&:not(:disabled) {
cursor: pointer;
&:hover, &[aria-selected="true"] {
&:hover,
&[aria-selected="true"] {
color: var(--a900);
}
}
@@ -80,7 +84,7 @@
transition: transform, width;
// left, width, transition-duration, opacity set by JS
// transform, width, transition-duration, opacity set by JS
// must be separate element for a different transition-duration
&::after {
@@ -95,12 +99,13 @@
border-radius: 4px;
opacity: inherit;
transition: opacity .1s;
transition: opacity 0.1s;
}
}
.tabPanel {
flex: 1;
height: 0;
display: none;
@@ -111,4 +116,5 @@
.tabPanelContent {
flex: 1;
overflow: hidden;
}

View File

@@ -5,21 +5,22 @@ import classNames from "classnames"
import styles from "./Tabs.module.scss"
type Context = {
activeTab: string | undefined,
setActive: (tab: string | undefined) => void,
hover: string | undefined,
setHover: (tab: string | undefined) => void,
setTabRef: (tab: string, ref: RefObject<HTMLButtonElement>) => void,
activeTab: string | undefined
setActive: (tab: string | undefined) => void
hover: string | undefined
setHover: (tab: string | undefined) => void
setTabRef: (tab: string, ref: RefObject<HTMLButtonElement>) => void
}
const TABS_CTX = createContext<Context>(null)
export type TabProps = {
children: ReactNode,
className?: string,
key: string, // react doesn't actually give us this as a prop, but this forces ts into requiring it
label?: ReactNode,
disabled?: boolean,
children: ReactNode
className?: string
key: string // react doesn't actually give us this as a prop, but this forces ts into requiring it
label?: ReactNode
disabled?: boolean
onSelect?: () => void
}
export class Tab extends Component<TabProps> {
@@ -42,7 +43,16 @@ export class Tab extends Component<TabProps> {
aria-selected={ctx.activeTab === key}
className={styles.tabButton}
disabled={this.props.disabled}
onClick={() => ctx.setActive(key)}
onClick={() => {
ctx.setActive(key)
if (this.props.onSelect) {
// run after layout
setTimeout(() => {
this.props.onSelect()
}, 0)
}
}}
onMouseMove={event => {
ctx.setHover(key)
event.stopPropagation()
@@ -57,18 +67,23 @@ export class Tab extends Component<TabProps> {
}
export type Props = {
className?: string,
children: ReactElement<typeof Tab> | ReactElement<typeof Tab>[] | ReactElement<typeof Tab>[][];
activeTab: string | undefined;
onChange: (tab: string) => void;
className?: string
children: ReactElement<typeof Tab> | ReactElement<typeof Tab>[] | ReactElement<typeof Tab>[][]
activeTab: string | undefined
onChange: (tab: string) => void
}
export default function Tabs<Key>({ children, activeTab, onChange, className }: Props) {
export default function Tabs({ children, activeTab, onChange, className }: Props) {
const [hover, _setHover] = useState<string>()
const bgRef = useRef<HTMLDivElement>()
const isMovingBetweenButtons = useRef(false)
const tabs: { [key: string]: { el: any, ref?: RefObject<HTMLButtonElement> } } = {}
const tabs: {
[key: string]: {
el: ReactElement<typeof Tab>
ref?: RefObject<HTMLButtonElement>
}
} = {}
if (Array.isArray(children)) {
for (const child of children) {
@@ -97,7 +112,7 @@ export default function Tabs<Key>({ children, activeTab, onChange, className }:
if (button) {
Object.assign(bgRef.current.style, {
opacity: 1,
transform: `translateX(${button.offsetLeft}px)`,
transform: `translate(${button.offsetLeft}px, ${button.offsetTop}px)`,
width: `${button.offsetWidth}px`,
})
} else {
@@ -138,19 +153,21 @@ export default function Tabs<Key>({ children, activeTab, onChange, className }:
className={styles.tabButtonsBackground}
/>
</div>
{Object.entries(tabs).map(([key, { el }]) => (
<div
{Object.entries(tabs).map(([key, { el }]) => {
const props = el.props as unknown as TabProps
return <div
role="tabpanel"
className={classNames(styles.tabPanel, {
[styles.active]: key === activeTab,
})}
key={key}
>
<div className={classNames(styles.tabPanelContent, el.props.className)}>
{el.props.children}
<div className={classNames(styles.tabPanelContent, props.className)}>
{props.children}
</div>
</div>
))}
})}
</div>
</TABS_CTX.Provider>
}

View File

@@ -1,7 +1,7 @@
.popover {
border: 1px solid var(--g500);
border-radius: 1em;
box-shadow: 0 2px 8px 0 #00000088;
box-shadow: 0 2px 8px 0 #0008;
max-width: 50em;
z-index: 999;

View File

@@ -11,10 +11,10 @@ import styles from "./CompilerButton.module.css"
import CompilerOpts, { Props as CompilerOptsProps } from "./CompilerOpts"
export type Props = {
arch: CompilerOptsProps["arch"],
value: CompilerOptsProps["value"],
onChange: CompilerOptsProps["onChange"],
disabled?: boolean,
arch: CompilerOptsProps["arch"]
value: CompilerOptsProps["value"]
onChange: CompilerOptsProps["onChange"]
disabled?: boolean
}
export default function CompilerButton({ arch, value, onChange, disabled }: Props) {

View File

@@ -11,6 +11,8 @@
flex-direction: row;
align-items: center;
justify-content: space-between;
cursor: default;
}
.container {
@@ -20,10 +22,12 @@
background: var(--g300);
}
.header:not([data-is-popup]), .container:not([data-is-popup]) {
.header:not([data-is-popup]),
.container:not([data-is-popup]) {
border-radius: 0;
background: transparent;
/*border-bottom: 1px solid var(--g400);*/
/* border-bottom: 1px solid var(--g400); */
}
.row {
@@ -47,20 +51,26 @@
color: var(--frog-secondary);
border-top-left-radius: 0;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
font-family: monospace;
font-size: .8rem;
font-size: 0.8rem;
padding: .6em 1em;
padding: 0.6em 1em;
outline: none !important;
}
.textbox::-webkit-input-placeholder {
color: var(--g500);
}
.flags {
display: flex;
flex-wrap: wrap;
gap: .5em;
gap: 0.5em;
margin-top: 1em;
}
@@ -68,17 +78,20 @@
flex-grow: 1;
display: inline-block;
cursor: default;
padding: .75em;
padding: 0.75em;
border-radius: 12px;
border: 1px solid transparent;
}
.flag:hover {
background: var(--g400);
background: var(--g250);
border-color: var(--g500);
}
.flag label {
font-size: .9rem;
margin-left: .5em;
font-size: 0.9rem;
margin-left: 0.5em;
}
.flag input {
@@ -88,7 +101,7 @@
.flagDescription {
display: block;
font-size: 0.8em;
opacity: 0.5;
color: var(--g800);
}
.flagSet {
@@ -98,13 +111,13 @@
flex-direction: column;
width: 20em;
border-radius: .5em;
border-radius: 0.5em;
}
.flagSetName {
cursor: default;
font-size: .8rem;
padding: .6em 1em;
padding-bottom: .2em;
opacity: 0.5;
font-size: 0.8rem;
padding: 0.6em 1em;
padding-bottom: 0.2em;
color: var(--g800);
}

View File

@@ -1,14 +1,14 @@
import { createContext, useState, useContext, useEffect } from "react"
import { createContext, useContext } from "react"
import Select from "../Select"
import styles from "./CompilerOpts.module.css"
import { useCompilersForArch } from "./compilers"
import PresetSelect, { PRESETS } from "./PresetSelect"
import PresetSelect from "./PresetSelect"
interface IOptsContext {
checkFlag(flag: string): boolean,
setFlag(flag: string, value: boolean): void,
checkFlag(flag: string): boolean
setFlag(flag: string, value: boolean): void
}
const OptsContext = createContext<IOptsContext>(undefined)
@@ -56,28 +56,35 @@ export function FlagOption({ flag, description }: { flag: string, description?:
}
export type CompilerOptsT = {
compiler: string,
cc_opts: string,
compiler: string
cc_opts: string
}
export type Props = {
arch?: string,
value?: CompilerOptsT,
onChange: (value: CompilerOptsT) => void,
title?: string,
isPopup?: boolean,
arch?: string
value: CompilerOptsT
onChange: (value: CompilerOptsT) => void
title?: string
isPopup?: boolean
}
export default function CompilerOpts({ arch, value, onChange, title, isPopup }: Props) {
const [compiler, setCompiler] = useState((value && value.compiler) || PRESETS[0].compiler)
let [opts, setOpts] = useState((value && value.cc_opts) || PRESETS[0].opts)
const compiler = value.compiler
let opts = value.cc_opts
useEffect(() => {
const setCompiler = (compiler: string) => {
onChange({
compiler,
cc_opts: opts,
})
}, [compiler, opts]) // eslint-disable-line react-hooks/exhaustive-deps
}
const setOpts = (opts: string) => {
onChange({
compiler,
cc_opts: opts,
})
}
return <OptsContext.Provider value={{
checkFlag(flag: string) {
@@ -108,11 +115,11 @@ export default function CompilerOpts({ arch, value, onChange, title, isPopup }:
}
export function OptsEditor({ arch, compiler, setCompiler, opts, setOpts }: {
arch?: string,
compiler: string,
setCompiler: (compiler: string) => void,
opts: string,
setOpts: (opts: string) => void,
arch?: string
compiler: string
setCompiler: (compiler: string) => void
opts: string
setOpts: (opts: string) => void
}) {
const compilers = useCompilersForArch(arch)
const compilerModule = compilers?.find(c => c.id === compiler)

View File

@@ -4,41 +4,43 @@ import Select from "../Select"
import { useCompilersForArch } from "./compilers"
export const PRESETS = [
{
name: "Super Mario 64",
compiler: "ido5.3",
opts: "-O1 -g -mips2",
},
{
name: "Paper Mario",
compiler: "gcc2.8.1",
opts: "-O2 -fforce-addr",
},
{
name: "OOT",
name: "Ocarina of Time",
compiler: "ido7.1",
opts: "-O2 -mips2",
},
{
name: "MM",
name: "Majora's Mask",
compiler: "ido7.1",
opts: "-O2 -g3 -mips2",
},
{
name: "SM64",
compiler: "ido5.3",
opts: "-O1 -g -mips2",
},
]
export default function PresetSelect({ arch, compiler, opts, setCompiler, setOpts }: {
arch: string,
compiler: string,
opts: string,
setCompiler: (compiler: string) => void,
setOpts: (opts: string) => void,
export default function PresetSelect({ className, arch, compiler, opts, setCompiler, setOpts, serverCompilers }: {
className?: string
arch: string
compiler: string
opts: string
setCompiler: (compiler: string) => void
setOpts: (opts: string) => void
serverCompilers?: Record<string, { arch: string | null }>
}) {
const compilers = useCompilersForArch(arch)
const compilers = useCompilersForArch(arch, serverCompilers)
const presets = PRESETS.filter(p => compilers?.find(c => c.id === p.compiler) !== undefined)
const selectedPreset = PRESETS.find(p => p.compiler === compiler && p.opts === opts)
return <Select onChange={e => {
return <Select className={className} onChange={e => {
if ((e.target as HTMLSelectElement).value === "custom") {
return
}

View File

@@ -18,11 +18,9 @@ export type CompilerModule = { id: string, name: string, Flags: FunctionComponen
export default COMPILERS
export function useCompilersForArch(arch?: string) {
const serverCompilers = api.useCompilers()
export function useCompilersForArch(arch?: string, serverCompilers?: Record<string, { arch: string | null }>) {
if (!serverCompilers)
return null
serverCompilers = api.useCompilers()
if (arch)
return COMPILERS.filter(compiler => serverCompilers[compiler.id]?.arch === arch) // compiler supports this arch

View File

@@ -1,45 +0,0 @@
.container {
padding: 1rem;
overflow: auto;
flex: 1;
}
.diff {
border-spacing: 1em 0;
}
.diff, .log {
display: block;
user-select: text;
border: none;
font-family: monospace;
font-size: var(--s-1);
white-space: pre;
}
.lineNumber {
color: grey;
font-size: 0.7em;
font-style: italic;
}
.immediate { color: #6D6DFF }
.stack { color: #e3fc45 }
.register { color: #e3fc45 }
.delay_slot { font-weight: bold; color: #969896 }
.diff_change { color: #6D6DFF }
.diff_add { color: #45bd00 }
.diff_remove { color: #c82829 }
.source_filename { font-weight: bold }
.source_function { font-weight: bold; text-decoration: underline }
.source_other { font-style: italic }
.source_line_num { font-style: italic }
.rotation0 { color: magenta}
.rotation1 { color: cyan }
.rotation2 { color: rgb(0, 212, 0) }
.rotation3 { color: red }
.rotation4 { color: lightyellow }
.rotation5 { color: lightpink }
.rotation6 { color: lightcyan }
.rotation7 { color: lightgreen }
.rotation8 { color: grey }

View File

@@ -1,102 +0,0 @@
.container {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
}
.toolbar {
display: flex;
padding-bottom: var(--s1);
padding-top: var(--s1);
align-items: center;
justify-content: space-evenly;
}
.resizer {
flex: 1;
overflow: hidden;
}
.context, .sourceCode {
display: flex;
flex-direction: column;
}
.grow { flex: 1 }
.diffSection {
display: flex;
flex-direction: column;
scrollbar-color: #ffffff33 transparent;
scrollbar-width: thin;
}
.diffExplanation {
opacity: 0.3;
}
.output {
padding: 1em;
flex: 1;
overflow: auto;
user-select: text;
}
.metadata {
font-size: 0.8em;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 0.5rem;
padding-left: 1rem;
gap: 1em;
color: var(--g1200);
cursor: default;
}
.metadata > div {
display: flex;
gap: 1ch;
align-items: center;
}
.chooseACompiler {
display: flex;
flex-direction: column;
}
.chooseACompilerActions {
padding: 1em;
align-self: center;
}
.scratchLink {
color: var(--g1400);
}
.editor {
height: 100%;
}
.toolbar {
display: flex;
flex-direction: row;
align-items: center;
justify-content: initial;
gap: .25em;
height: 48px;
padding: 0 14px;
background: var(--g300);
border-bottom: 1px solid var(--a100);
}
.scratchName {
user-select: initial;
}

View File

@@ -1,19 +1,23 @@
.user {
display: flex;
display: inline-flex;
align-items: center;
justify-content: center;
gap: .5em;
gap: 6px;
padding: .6em;
padding-right: .9em;
padding-left: 3px;
color: var(--g1400);
vertical-align: bottom;
}
.user:any-link {
cursor: pointer;
}
.user:any-link:hover {
color: var(--g1700);
text-decoration: underline;
}
.avatar {
border-radius: 999px;
width: 1.5em;

View File

@@ -5,20 +5,30 @@ import * as api from "../../lib/api"
import styles from "./UserLink.module.css"
export function GitHubUserLink({ user }: { user: { login: string, avatar_url?: string } }) {
return <Link href={`https://github.com/${user.login}`}>
<a className={styles.user}>
{user.avatar_url && <Image className={styles.avatar} src={user.avatar_url} alt="User avatar" width={24} height={24} />}
<span>{user.login}</span>
</a>
</Link>
}
export type Props = {
user: api.User | api.AnonymousUser,
user: api.User | api.AnonymousUser
}
export default function UserLink({ user }: Props) {
if (api.isAnonUser(user)) {
return <a className={styles.user}>
<span>{user.is_you ? "you" : "anon" }</span>
<span>{user.is_you ? "You" : "Anonymous" }</span>
</a>
} else {
return <Link href={`/u/${user.username}`}>
<a title={`@${user.username}`} className={styles.user}>
<a className={styles.user}>
{user.avatar_url && <Image className={styles.avatar} src={user.avatar_url} alt="User avatar" width={24} height={24} />}
<span>{user.name} {user.is_you && <i>(you)</i>}</span>
<span>{user.username}</span>
</a>
</Link>
}

View File

@@ -1,13 +1,11 @@
import { useState, useCallback, useEffect, useTransition, useRef } from "react"
import { useState, useCallback, useEffect } from "react"
import { useRouter } from "next/router"
import { dequal } from "dequal/lite"
import useSWR, { Revalidator, RevalidatorOptions } from "swr"
import { useDebouncedCallback } from "use-debounce"
import useDeepCompareEffect from "use-deep-compare-effect"
const API_BASE = process.env.NEXT_PUBLIC_API_BASE ?? process.env.STORYBOOK_API_BASE
const API_BASE = process.env.INTERNAL_API_BASE ?? process.env.NEXT_PUBLIC_API_BASE ?? process.env.STORYBOOK_API_BASE
type Json = Record<string, unknown>
@@ -130,76 +128,79 @@ export async function patch(url: string, json: Json) {
}
export interface AnonymousUser {
is_you: boolean,
is_anonymous: true,
is_you: boolean
is_anonymous: true
}
export interface User {
is_you: boolean,
is_anonymous: false,
is_you: boolean
is_anonymous: false
id: number,
username: string,
name: string,
avatar_url: string | null,
github_api_url: string | null,
github_html_url: string | null,
id: number
username: string
name: string
avatar_url: string | null
github_api_url: string | null
github_html_url: string | null
}
export type Scratch = {
slug: string,
compiler: string,
arch: string,
cc_opts: string,
source_code: string,
context: string,
owner: AnonymousUser | User | null,
parent: string | null, // URL
diff_label: string | null,
name: string
description: string
slug: string
compiler: string
arch: string
cc_opts: string
source_code: string
context: string
owner: AnonymousUser | User | null // null means unclaimed
parent: string | null // URL
diff_label: string | null
score: number // -1 = doesn't compile
}
export type Compilation = {
errors: string,
diff_output: DiffOutput,
errors: string
diff_output: DiffOutput
}
export type DiffOutput = {
arch_str: string,
current_score: number,
error: string | null,
header: DiffHeader,
rows: DiffRow[],
arch_str: string
current_score: number
error: string | null
header: DiffHeader
rows: DiffRow[]
}
export type DiffHeader = {
base: DiffText[],
current: DiffText[],
previous?: DiffText[],
base: DiffText[]
current: DiffText[]
previous?: DiffText[]
}
export type DiffRow = {
key: string,
base?: DiffCell,
current?: DiffCell,
previous?: DiffCell,
key: string
base?: DiffCell
current?: DiffCell
previous?: DiffCell
}
export type DiffCell = {
text: DiffText[],
line?: number,
branch?: number,
src?: string,
src_comment?: string,
src_line?: number,
src_path?: string,
text: DiffText[]
line?: number
branch?: number
src?: string
src_comment?: string
src_line?: number
src_path?: string
}
export type DiffText = {
text: string,
format?: string,
group?: string,
index?: number,
key?: string,
text: string
format?: string
group?: string
index?: number
key?: string
}
@@ -208,29 +209,39 @@ export function isAnonUser(user: User | AnonymousUser): user is AnonymousUser {
}
export function useScratch(slugOrUrl: string): {
scratch: Readonly<Scratch>,
savedScratch: Readonly<Scratch> | null,
setScratch: (scratch: Partial<Scratch>) => void, // Update the scratch, but only locally
saveScratch: () => Promise<void>, // Persist the scratch to the server
isSaved: boolean,
scratch: Readonly<Scratch>
savedScratch: Readonly<Scratch> | null
setScratch: (scratch: Partial<Scratch>) => void // Update the scratch, but only locally
saveScratch: () => Promise<void> // Persist the scratch to the server
isSaved: boolean
} {
const url = isAbsoluteUrl(slugOrUrl) ?slugOrUrl : `/scratch/${slugOrUrl}`
const [isSaved, setIsSaved] = useState(true)
const localScratch = useRef<Scratch>()
const [localScratch, setLocalScratch] = useState<Scratch>()
const shouldGetScratchFromServer = useCallback(() => {
// This is in a useCallback so useSWR's onSuccess doesn't capture the values
if (!localScratch)
return true
// Only update localScratch if there aren't unsaved changes (otherwise, data loss could occur)
if (isSaved)
return true
return false
}, [localScratch, isSaved])
const { data, mutate } = useSWR<Scratch>(url, get, {
suspense: true,
refreshInterval: isSaved ? 5000 : 0,
refreshInterval: 5000,
onSuccess: scratch => {
if (!scratch.source_code) {
throw new Error("Scratch returned from API has no source_code (is the API misbehaving?)")
}
// Only update localScratch if there aren't unsaved changes (otherwise, data loss could occur)
// TODO: display onscreen prompt if there are scratch updates but they arent displayed
// because they could overwrite your own changes
if (!localScratch || isSaved) {
if (shouldGetScratchFromServer()) {
console.info("Got updated scratch from server", scratch)
localScratch.current = scratch
setLocalScratch(scratch)
setIsSaved(true)
}
},
onErrorRetry,
@@ -239,73 +250,72 @@ export function useScratch(slugOrUrl: string): {
// If the slug changes, forget the local scratch
useEffect(() => {
localScratch.current = undefined
setLocalScratch(undefined)
mutate()
setIsSaved(true)
}, [mutate])
}, [slugOrUrl, mutate])
const [, forceUpdate] = useState<{}>({})
const setScratch = useCallback((partial: Partial<Scratch>) => {
Object.assign(localScratch.current, partial)
setIsSaved(dequal(localScratch.current, savedScratch))
forceUpdate({}) // we changed localScratch
}, [localScratch, savedScratch])
const updateLocalScratch = useCallback((partial: Partial<Scratch>) => {
setLocalScratch(Object.assign({}, localScratch, partial))
setIsSaved(false)
}, [localScratch])
const saveScratch = useCallback(() => {
if (!localScratch.current) {
if (!localScratch) {
throw new Error("Cannot save scratch before it is loaded")
}
if (!localScratch.current.owner.is_you) {
if (!localScratch.owner.is_you) {
throw new Error("Cannot save scratch which you do not own")
}
return patch(`/scratch/${savedScratch.slug}`, {
// TODO: api should support undefinedIfUnchanged on all fields
source_code: localScratch.current.source_code,
context: localScratch.current.context, //undefinedIfUnchanged("context"),
compiler: localScratch.current.compiler,
cc_opts: localScratch.current.cc_opts,
source_code: undefinedIfUnchanged(savedScratch, localScratch, "source_code"),
context: undefinedIfUnchanged(savedScratch, localScratch, "context"),
compiler: undefinedIfUnchanged(savedScratch, localScratch, "compiler"),
cc_opts: undefinedIfUnchanged(savedScratch, localScratch, "cc_opts"),
name: undefinedIfUnchanged(savedScratch, localScratch, "name"),
description: undefinedIfUnchanged(savedScratch, localScratch, "description"),
}).then(() => {
setIsSaved(true)
mutate(localScratch.current, true)
mutate(localScratch, true)
}).catch(error => {
console.error(error)
})
}, [localScratch, savedScratch, mutate])
if (!localScratch.current) {
if (!localScratch) {
setIsSaved(true)
localScratch.current = savedScratch
setLocalScratch(savedScratch)
}
return {
scratch: localScratch.current,
scratch: localScratch ?? savedScratch,
savedScratch,
setScratch,
setScratch: updateLocalScratch,
saveScratch,
isSaved,
}
}
export async function forkScratch(parent: Scratch): Promise<Scratch> {
const scratch = await post(`/scratch/${parent.slug}/fork`, parent)
export async function forkScratch(parent: Scratch, localScratch: Partial<Scratch> = {}): Promise<Scratch> {
const scratch = await post(`/scratch/${parent.slug}/fork`, Object.assign({}, parent, localScratch))
return scratch
}
export function useForkScratchAndGo(parent: Scratch): () => Promise<void> {
export function useForkScratchAndGo(parent: Scratch, localScratch: Partial<Scratch> = {}): () => Promise<void> {
const router = useRouter()
return async () => {
const fork = await forkScratch(parent)
const fork = await forkScratch(parent, localScratch)
router.push(`/scratch/${fork.slug}`)
}
}
export function useCompilation(scratch: Scratch | null, savedScratch?: Scratch, autoRecompile = true): {
compilation: Readonly<Compilation> | null,
compile: () => Promise<void>, // no debounce
debouncedCompile: () => Promise<void>, // with debounce
isCompiling: boolean,
compilation: Readonly<Compilation> | null
compile: () => Promise<void> // no debounce
debouncedCompile: () => Promise<void> // with debounce
isCompiling: boolean
} {
const [compileRequestPromise, setCompileRequestPromise] = useState<Promise<void>>(null)
const [compilation, setCompilation] = useState<Compilation>(null)
@@ -344,20 +354,27 @@ export function useCompilation(scratch: Scratch | null, savedScratch?: Scratch,
const debouncedCompile = useDebouncedCallback(compile, 500, { leading: false, trailing: true })
useDeepCompareEffect(() => {
useEffect(() => {
if (autoRecompile) {
if (scratch && scratch.compiler !== "")
debouncedCompile()
else
setCompilation(null)
}
}, [debouncedCompile, scratch, autoRecompile])
}, [ // eslint-disable-line react-hooks/exhaustive-deps
debouncedCompile,
autoRecompile,
// fields passed to compilations
scratch.compiler, scratch.cc_opts,
scratch.source_code, scratch.context,
])
return {
compilation,
compile,
debouncedCompile,
isCompiling: !!compileRequestPromise,
isCompiling: !!compileRequestPromise || debouncedCompile.isPending(),
}
}
@@ -372,7 +389,7 @@ export function useArches(): Record<string, string> {
return data?.arches
}
export function useCompilers(): Record<string, { arch: string | null }> | null {
export function useCompilers(): Record<string, { arch: string | null }> {
const { data } = useSWR("/compilers", get, {
refreshInterval: 0,
suspense: true,

View File

@@ -5,9 +5,9 @@ import Router from "next/router"
import useResizeObserver from "@react-hook/resize-observer"
export function useSize<T extends HTMLElement>(): {
width: number | undefined,
height: number | undefined,
ref: RefObject<T>,
width: number | undefined
height: number | undefined
ref: RefObject<T>
} {
const ref = useRef<T>()
const [size, setSize] = useState({ width: undefined, height: undefined })
@@ -22,7 +22,7 @@ export function useSize<T extends HTMLElement>(): {
return { width: size.width, height: size.height, ref }
}
export function useWarnBeforeUnload(enabled: boolean, message: string = "Are you sure you want to leave this page?") {
export function useWarnBeforeUnload(enabled: boolean, message = "Are you sure you want to leave this page?") {
const enabledRef = useRef(enabled)
const messageRef = useRef(message)

View File

@@ -10,28 +10,15 @@
html {
font-size: 16px;
font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji;
line-height: 1.5;
text-rendering: optimizeQuality;
overflow: hidden;
--ratio: 1.25;
--s-5: calc(var(--s-4) / var(--ratio));
--s-4: calc(var(--s-3) / var(--ratio));
--s-3: calc(var(--s-2) / var(--ratio));
--s-2: calc(var(--s-1) / var(--ratio));
--s-1: calc(var(--s0) / var(--ratio));
--s0: 1rem;
--s1: calc(var(--s0) * var(--ratio));
--s2: calc(var(--s1) * var(--ratio));
--s3: calc(var(--s2) * var(--ratio));
--s4: calc(var(--s3) * var(--ratio));
--s5: calc(var(--s4) * var(--ratio));
--s6: calc(var(--s5) * var(--ratio));
--s7: calc(var(--s6) * var(--ratio));
}
*, *::before, *::after {
*,
*::before,
*::after {
box-sizing: border-box;
font-size: inherit;
font-family: inherit;
@@ -48,13 +35,14 @@ body {
background: var(--g300);
color: var(--a800);
line-height: var(--ratio);
line-height: 1.25;
overflow-x: hidden;
user-select: none;
position: relative;
}
html, body {
html,
body {
height: 100%;
}
@@ -76,7 +64,7 @@ main {
}
::selection {
background: #ffffff22;
background: var(--a100);
}
.routerProgressBar {

View File

@@ -0,0 +1,57 @@
.container {
padding: 32px;
max-width: 700px;
user-select: auto;
cursor: auto;
display: flex;
flex-direction: column;
gap: 21px;
line-height: 1.5;
p,
ul {
color: var(--g1700);
padding: 0;
margin: 0;
}
a {
color: var(--g1400);
&:hover {
color: var(--g1700);
text-decoration: underline;
}
}
}
.heading {
font-size: 3rem;
font-weight: 400;
color: var(--g1800);
}
.subheading {
font-size: 1rem;
font-weight: 600;
color: var(--g1800);
padding-bottom: 8px;
}
.contributors {
list-style: none;
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.rule {
border: 0;
background: var(--g400);
height: 1px;
}

View File

@@ -0,0 +1,140 @@
import { GetStaticProps } from "next"
import Link from "next/link"
import Nav from "../components/Nav"
import UserLink, { GitHubUserLink } from "../components/user/UserLink"
import * as api from "../lib/api"
import styles from "./credits.module.scss"
const MAINTAINER_USERNAMES = ["nanaian", "ethteck"]
const CONTRIBUTOR_USERNAMES = [
"zbanks",
"simonlindholm",
"TGEnigma",
"octorock",
"mkst",
"JoshDuMan",
"Henny022",
"AngheloAlf",
]
type Contributor = {
type: "decompme"
user: api.User
} | {
type: "github"
user: { login: string, avatar_url?: string }
}
async function getContributor(username: string): Promise<Contributor> {
try {
// Try and get decomp.me information if they have an account
const user: api.User = await api.get(`/users/${username}`)
return {
type: "decompme",
user,
}
} catch (error) {
// Fall back to GitHub information
const req = await fetch(`https://api.github.com/users/${username}`)
const user = await req.json()
if (user.message) {
// Rate limit :(
return {
type: "github",
user: { login: username },
}
}
return {
type: "github",
user,
}
}
}
function Contributor({ contributor }: { contributor: Contributor }) {
if (contributor.type === "decompme")
return <UserLink user={contributor.user} />
else
return <GitHubUserLink user={contributor.user} />
}
export const getStaticProps: GetStaticProps = async _context => {
return {
props: {
maintainers: await Promise.all(MAINTAINER_USERNAMES.map(getContributor)),
contributors: await Promise.all(CONTRIBUTOR_USERNAMES.map(getContributor)),
},
}
}
export default function CreditsPage({ maintainers, contributors }: {
maintainers: Contributor[]
contributors: Contributor[]
}) {
return <>
<Nav />
<main className={styles.container}>
<h1 className={styles.heading}>
Credits
</h1>
<p>
decomp.me is developed by <Contributor contributor={maintainers[0]} /> and <Contributor contributor={maintainers[1]} />.
</p>
<div>
<h3 className={styles.subheading}>
Contributors
</h3>
<ul className={styles.contributors}>
{contributors.map(contributor => {
return <li key={contributor.type === "decompme" ? contributor.user.username : contributor.user.login}>
<Contributor contributor={contributor} />
</li>
})}
</ul>
</div>
<hr className={styles.rule} />
<div>
<h3 className={styles.subheading}>
Projects
</h3>
<ul>
<li>
<Link href="https://github.com/simonlindholm/asm-differ">
<a>simonlindholm/asm-differ</a>
</Link>
</li>
<li>
<Link href="https://github.com/matt-kempster/mips_to_c">
<a>matt-kempster/mips_to_c</a>
</Link>
</li>
</ul>
</div>
<hr className={styles.rule} />
<div>
<h3 className={styles.subheading}>
Icons
</h3>
<ul>
<li>Octicons by GitHub</li>
<li>
<Link href="https://github.com/file-icons/icons">
<a>file-icons/icons</a>
</Link>
</li>
</ul>
</div>
</main>
</>
}

View File

@@ -18,5 +18,5 @@
}
.error {
color: #ff8866;
color: #f86;
}

View File

@@ -1,75 +0,0 @@
.container {
display: flex;
flex-direction: column;
gap: 1em;
max-width: 40em;
padding: 1em;
user-select: auto;
}
.rule {
border: 0;
background: var(--g400);
height: 1px;
}
.heading {
padding: 10px;
> h1 {
color: var(--g1900);
font-size: 1.25em;
font-weight: 500;
}
> p {
padding-top: 4px;
color: var(--g1000);
font-size: .9em;
max-width: 50ch;
}
}
.label {
display: block;
margin: 0;
padding: 10px;
color: var(--g1700);
font-size: .9em;
font-weight: 600;
small {
color: var(--g1000);
font-size: .8em;
}
}
.textInput {
width: 100%;
padding: 8px 10px;
color: var(--g1200);
background: var(--g200);
font: .8em monospace;
border: 1px solid var(--g500);
border-radius: 4px;
}
.editorContainer {
display: flex;
flex-direction: column;
height: 200px;
}
.editor {
width: 100%;
flex: 1;
background: var(--g200);
border: 1px solid var(--g500);
border-radius: 4px;
}

View File

@@ -1,185 +0,0 @@
import { useEffect, useState, useMemo } from "react"
import { GetStaticProps } from "next"
import Head from "next/head"
import { useRouter } from "next/router"
import AsyncButton from "../components/AsyncButton"
import Editor from "../components/Editor"
import Footer from "../components/Footer"
import Nav from "../components/Nav"
import Select from "../components/Select2"
import * as api from "../lib/api"
import styles from "./scratch.module.scss"
function getLabels(asm: string): string[] {
const lines = asm.split("\n")
const labels = []
for (const line of lines) {
const match = line.match(/^\s*glabel\s+([a-zA-Z0-9_]+)\s*$/)
if (match) {
labels.push(match[1])
}
}
return labels
}
export const getStaticProps: GetStaticProps = async _context => {
const data = await api.get("/compilers")
return {
props: {
arches: data.arches,
},
}
}
export default function NewScratch({ arches }: { arches: { [key: string]: string } }) {
const [asm, setAsm] = useState("")
const [context, setContext] = useState("")
const [arch, setArch] = useState("")
const router = useRouter()
const defaultLabel = useMemo(() => {
const labels = getLabels(asm)
return labels.length > 0 ? labels[labels.length - 1] : null
}, [asm])
const [label, setLabel] = useState<string>("")
// Load fields from localStorage
useEffect(() => {
try {
setLabel(JSON.parse(localStorage["NewScratch.label"] ?? "\"\""))
setAsm(JSON.parse(localStorage["NewScratch.asm"] ?? "\"\""))
setContext(JSON.parse(localStorage["NewScratch.context"] ?? "\"\""))
setArch(JSON.parse(localStorage["NewScratch.arch"] ?? "\"\""))
} catch (error) {
console.warn("bad localStorage", error)
}
}, [])
// Update localStorage
useEffect(() => {
localStorage["NewScratch.label"] = JSON.stringify(label)
localStorage["NewScratch.asm"] = JSON.stringify(asm)
localStorage["NewScratch.context"] = JSON.stringify(context)
localStorage["NewScratch.arch"] = JSON.stringify(arch)
}, [label, asm, context, arch])
if (!arch) {
setArch(Object.keys(arches)[0])
}
const submit = async () => {
try {
const scratch: api.Scratch = await api.post("/scratch", {
target_asm: asm,
context: context || "",
arch,
diff_label: label || defaultLabel || "",
})
localStorage["NewScratch.label"] = ""
localStorage["NewScratch.asm"] = ""
router.push(`/scratch/${scratch.slug}`)
} catch (error) {
if (error?.responseJSON?.as_errors) {
throw new Error(error.responseJSON.as_errors.join("\n"))
} else {
console.error(error)
throw error
}
}
}
return <>
<Head>
<title>New scratch | decomp.me</title>
</Head>
<Nav />
<main className={styles.container}>
<div className={styles.heading}>
<h1>Create a new scratch</h1>
<p>
A scratch is a playground where you can work on matching
a given target function using any compiler options you like.
</p>
</div>
<hr className={styles.rule} />
<div>
<p className={styles.label}>
Architecture
</p>
{/* TODO: custom horizontal <options> */}
<Select
options={arches}
value={arch}
onChange={a => setArch(a)}
/>
</div>
<hr className={styles.rule} />
<div>
<label className={styles.label} htmlFor="label">
Function name <small>(label as it appears in the target asm)</small>
</label>
<input
name="label"
type="text"
value={label}
placeholder={defaultLabel}
onChange={e => setLabel((e.target as HTMLInputElement).value)}
className={styles.textInput}
/>
</div>
<div className={styles.editorContainer}>
<p className={styles.label}>Target assembly <small>(required)</small></p>
<Editor
className={styles.editor}
language="mips"
value={asm}
onChange={setAsm}
padding={10}
showMargin
lineNumbers
/>
</div>
<div className={styles.editorContainer}>
<p className={styles.label}>
Context <small>(typically generated with m2ctx.py)</small>
</p>
<Editor
className={styles.editor}
language="c"
value={context}
onChange={setContext}
padding={10}
showMargin
lineNumbers
/>
</div>
<hr className={styles.rule} />
<div>
<AsyncButton
primary
disabled={asm.length == 0}
onClick={submit}
errorPlacement="right-center"
>
Create scratch
</AsyncButton>
</div>
</main>
<Footer />
</>
}

View File

@@ -6,7 +6,7 @@ import Head from "next/head"
import LoadingSpinner from "../../components/loading.svg"
import Nav from "../../components/Nav"
import Scratch, { nameScratch } from "../../components/scratch/Scratch"
import Scratch from "../../components/Scratch"
import * as api from "../../lib/api"
import styles from "./[slug].module.scss"
@@ -42,7 +42,7 @@ export const getStaticProps: GetStaticProps = async context => {
export default function ScratchPage({ scratch }: { scratch?: api.Scratch }) {
return <>
<Head>
<title>{scratch ? nameScratch(scratch) : "Loading scratch"} | decomp.me</title>
<title>{scratch ? scratch.name : "Scratch"} | decomp.me</title>
</Head>
<Nav />
<main className={styles.container}>

View File

@@ -0,0 +1,129 @@
.container {
display: flex;
flex-direction: column;
gap: 1em;
max-width: 40em;
padding: 1em;
cursor: default;
}
.rule {
border: 0;
background: var(--g400);
height: 1px;
}
.heading {
padding: 10px;
user-select: auto;
> h1 {
color: var(--g1900);
font-size: 1.25em;
font-weight: 500;
}
> p {
padding-top: 4px;
color: var(--g1000);
font-size: 0.9em;
max-width: 50ch;
}
}
.label {
display: block;
margin: 0;
padding: 10px;
color: var(--g1700);
font-size: 0.9em;
font-weight: 600;
small {
color: var(--g800);
font-size: 0.8em;
font-weight: 400;
padding-left: 8px;
}
}
.textInput {
width: 100%;
padding: 8px 10px;
color: var(--g1200);
background: var(--g200);
font: 0.8em monospace;
border: 1px solid var(--g500);
border-radius: 4px;
outline: none !important;
&::-webkit-input-placeholder {
color: var(--g700);
}
}
.editorContainer {
display: flex;
flex-direction: column;
height: 200px;
}
.editor {
width: 100%;
flex: 1;
background: var(--g200);
border: 1px solid var(--g500);
border-radius: 4px;
}
.compilerContainer {
display: flex;
align-items: center;
justify-content: space-between;
cursor: default;
user-select: none;
@media screen and (max-width: 400px) {
flex-direction: column;
}
& > div {
display: flex;
flex-direction: column;
flex: 1;
width: 100%;
&.compilerChoiceOr {
display: block;
flex: 0;
padding: 8px;
text-align: center;
color: var(--g500);
font-size: 0.8rem;
@media screen and (min-width: 400px) {
padding: 16px;
}
}
}
}
.compilerChoiceSelect {
width: 100%;
}
.compilerChoiceHeading {
font-size: 0.8rem;
padding: 2px 10px;
color: var(--g800);
}

View File

@@ -0,0 +1,272 @@
import { useEffect, useState, useMemo } from "react"
import { GetStaticProps } from "next"
import Head from "next/head"
import { useRouter } from "next/router"
import ArchSelect from "../../components/ArchSelect"
import AsyncButton from "../../components/AsyncButton"
import { useCompilersForArch } from "../../components/compiler/compilers"
import PresetSelect, { PRESETS } from "../../components/compiler/PresetSelect"
import Editor from "../../components/Editor"
import Footer from "../../components/Footer"
import Nav from "../../components/Nav"
import Select from "../../components/Select2"
import * as api from "../../lib/api"
import styles from "./new.module.scss"
function getLabels(asm: string): string[] {
const lines = asm.split("\n")
const labels = []
for (const line of lines) {
const match = line.match(/^\s*glabel\s+([a-zA-Z0-9_]+)\s*$/)
if (match) {
labels.push(match[1])
}
}
return labels
}
export const getStaticProps: GetStaticProps = async _context => {
const data = await api.get("/compilers")
return {
props: {
serverCompilers: data,
},
}
}
export default function NewScratch({ serverCompilers }: {
serverCompilers: {
arches: {
[key: string]: {
name: string
description: string
}
}
compilers: {
[key: string]: {
arch: string
}
}
}
}) {
const [asm, setAsm] = useState("")
const [context, setContext] = useState("")
const [arch, setArch] = useState("")
const [compiler, setCompiler] = useState<string>()
const [compilerOpts, setCompilerOpts] = useState<string>("")
const router = useRouter()
const defaultLabel = useMemo(() => {
const labels = getLabels(asm)
return labels.length > 0 ? labels[labels.length - 1] : null
}, [asm])
const [label, setLabel] = useState<string>("")
const [lineNumbers, setLineNumbers] = useState(false)
// Load fields from localStorage
useEffect(() => {
try {
setLabel(localStorage["new_scratch_label"] ?? "")
setAsm(localStorage["new_scratch_asm"] ?? "")
setContext(localStorage["new_scratch_context"] ?? "")
setArch(localStorage["new_scratch_arch"] ?? "")
setCompiler(localStorage["new_scratch_compiler"] ?? undefined)
setCompilerOpts(localStorage["new_scratch_compilerOpts"] ?? "")
} catch (error) {
console.warn("bad localStorage", error)
}
}, [])
// Update localStorage
useEffect(() => {
localStorage["new_scratch_label"] = label
localStorage["new_scratch_asm"] = asm
localStorage["new_scratch_context"] = context
localStorage["new_scratch_arch"] = arch
localStorage["new_scratch_compiler"] = compiler
localStorage["new_scratch_compilerOpts"] = compilerOpts
}, [label, asm, context, arch, compiler, compilerOpts])
const compilers = useCompilersForArch(arch, serverCompilers.compilers)
const compilerModule = compilers?.find(c => c.id === compiler)
if (!arch || Object.keys(serverCompilers.arches).indexOf(arch) === -1) {
setArch(Object.keys(serverCompilers.arches)[0])
}
if (!compilerModule) { // We just changed architectures, probably
if (compilers.length === 0) {
console.warn("No compilers supported for arch", arch)
} else {
// Fall back to the first supported compiler and no opts
setCompiler(compilers[0].id)
setCompilerOpts("")
// If there is a preset that uses a supported compiler, default to it
for (const preset of PRESETS) {
if (compilers.find(c => c.id === preset.compiler)) {
setCompiler(preset.compiler)
setCompilerOpts(preset.opts)
break
}
}
}
}
const submit = async () => {
try {
const scratch: api.Scratch = await api.post("/scratch", {
target_asm: asm,
context: context || "",
arch,
compiler,
compiler_flags: compilerOpts,
diff_label: label || defaultLabel || "",
})
localStorage["new_scratch_label"] = ""
localStorage["new_scratch_asm"] = ""
router.push(`/scratch/${scratch.slug}`)
} catch (error) {
setLineNumbers(true) // line numbers are likely relevant to the error
if (error?.responseJSON?.as_errors) {
throw new Error(error.responseJSON.as_errors.join("\n"))
} else {
console.error(error)
throw error
}
}
}
return <>
<Head>
<title>New scratch | decomp.me</title>
</Head>
<Nav />
<main className={styles.container}>
<div className={styles.heading}>
<h1>Create a new scratch</h1>
<p>
A scratch is a playground where you can work on matching
a given target function using any compiler options you like.
</p>
</div>
<hr className={styles.rule} />
<div>
<p className={styles.label}>
Architecture
</p>
<ArchSelect
arches={serverCompilers.arches}
value={arch}
onChange={a => setArch(a)}
/>
</div>
<div>
<p className={styles.label}>
Compiler
</p>
<div className={styles.compilerContainer}>
<div>
<span className={styles.compilerChoiceHeading}>Select a compiler</span>
<Select
className={styles.compilerChoiceSelect}
options={compilers.reduce((options, compiler) => {
return {
...options,
[compiler.id]: compiler.name,
}
}, {})}
value={compiler}
onChange={c => {
setCompiler(c)
setCompilerOpts("")
}}
/>
</div>
<div className={styles.compilerChoiceOr}>or</div>
<div>
<span className={styles.compilerChoiceHeading}>Select a preset</span>
<PresetSelect
className={styles.compilerChoiceSelect}
arch={arch}
compiler={compiler}
opts={compilerOpts}
setCompiler={setCompiler}
setOpts={setCompilerOpts}
serverCompilers={serverCompilers.compilers}
/>
</div>
</div>
</div>
<hr className={styles.rule} />
<div>
<label className={styles.label} htmlFor="label">
Function name <small>(label as it appears in the target asm)</small>
</label>
<input
name="label"
type="text"
value={label}
placeholder={defaultLabel}
onChange={e => setLabel((e.target as HTMLInputElement).value)}
className={styles.textInput}
/>
</div>
<div className={styles.editorContainer}>
<p className={styles.label}>Target assembly <small>(required)</small></p>
<Editor
className={styles.editor}
language="mips"
value={asm}
onChange={setAsm}
padding={10}
showMargin={lineNumbers}
lineNumbers={lineNumbers}
/>
</div>
<div className={styles.editorContainer}>
<p className={styles.label}>
Context <small>(typically generated with m2ctx.py)</small>
</p>
<Editor
className={styles.editor}
language="c"
value={context}
onChange={setContext}
padding={10}
showMargin={lineNumbers}
lineNumbers={lineNumbers}
/>
</div>
<hr className={styles.rule} />
<div>
<AsyncButton
primary
disabled={asm.length == 0}
onClick={submit}
errorPlacement="right-center"
>
Create scratch
</AsyncButton>
</div>
</main>
<Footer />
</>
}

View File

@@ -6,23 +6,24 @@
$accent-dark: color.scale($accent, $lightness: -40%, $saturation: -20%);
@if color.saturation($accent) == 0% {
$accent-light: color.change($accent-light, $saturation: 0%)
$accent-light: color.change($accent-light, $saturation: 0%);
}
--frog-pupil: #292f33;
--frog-primary: #{$accent};
--frog-secondary: #{$accent-light};
--frog-nose: #{$accent-dark};
--accent: #{$accent};
--complement: #{color.complement($accent)};
@for $i from 1 through 40 {
$multiplier: 2.5%;
--g#{$i * 50}: #{hsl(color.hue($bg), color.saturation($bg), ($i - 1) * $multiplier)};
}
$peak: 0;
@if color.blackness($bg) > 50% {
$peak: 255;
}

View File

@@ -23,7 +23,7 @@
.avatar {
width: 64px;
height: 64px;
background: #ffffff44;
background: #fff4;
border-radius: 999px;
}

View File

@@ -87,7 +87,7 @@ export default function UserPage({ user: initialUser }: { user: api.User }) {
height={64}
/>}
<h1 className={styles.name}>
<div>{user.name} {user.is_you && <i>(you)</i>}</div>
<div>{user.name}</div>
<div className={styles.username}>
@{user.username}

File diff suppressed because it is too large Load Diff