Skip to content

Commit

Permalink
feat: console input
Browse files Browse the repository at this point in the history
  • Loading branch information
goldbuick committed May 11, 2024
1 parent 34ad5b4 commit 3b2ed2b
Show file tree
Hide file tree
Showing 6 changed files with 191 additions and 21 deletions.
4 changes: 3 additions & 1 deletion zss/device/tape.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { proxy, useSnapshot } from 'valtio'
import { createdevice } from 'zss/device'
import { createguid } from 'zss/mapping/guid'
import { clamp } from 'zss/mapping/number'
import { isnumber } from 'zss/mapping/types'

// system wide message logger
Expand All @@ -15,13 +16,14 @@ export enum TAPE_DISPLAY {
}

type TAPE_ROW = [string, string, string, ...any[]]

type TAPE_STATE = {
open: boolean
mode: TAPE_DISPLAY
logs: TAPE_ROW[]
}

const tape = proxy<TAPE_STATE>({
const tape: TAPE_STATE = proxy({
open: false,
mode: TAPE_DISPLAY.BOTTOM,
logs: [],
Expand Down
185 changes: 170 additions & 15 deletions zss/gadget/components/tape.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useThree } from '@react-three/fiber'
import { useState } from 'react'
import {
TAPE_DISPLAY,
tapesetmode,
Expand All @@ -10,10 +11,22 @@ import {
createwritetextcontext,
tokenizeAndWriteTextFormat as tokenizeandwritetextformat,
tokenizeandmeasuretextformat,
applystrtoindex,
applycolortoindexes,
} from 'zss/gadget/data/textformat'
import { COLOR, DRAW_CHAR_HEIGHT, DRAW_CHAR_WIDTH } from 'zss/gadget/data/types'
import { clamp } from 'zss/mapping/number'
import { stringsplice } from 'zss/mapping/string'
import { ispresent } from 'zss/mapping/types'

import { UserFocus, UserHotkey, UserInput } from './userinput'
import { useBlink } from './panel/common'
import {
UserFocus,
UserHotkey,
UserInput,
UserInputMods,
isMac,
} from './userinput'
import { TileSnapshot, resetTiles, useTiles } from './usetiles'

const SCALE = 1
Expand All @@ -38,7 +51,6 @@ export function TapeConsole() {
let width = cols
let height = rows

console.info(tape)
switch (tape.mode) {
case TAPE_DISPLAY.TOP:
height = Math.round(rows * 0.5)
Expand All @@ -56,7 +68,7 @@ export function TapeConsole() {
break
default:
case TAPE_DISPLAY.FULL:
// no-op
// defaults
break
}

Expand All @@ -72,49 +84,192 @@ export function TapeConsole() {
rightEdge: width,
}

const blink = useBlink()
const [cursor, setcursor] = useState(0)
const [inputstate, setinputstate] = useState('')
const [selection, setselection] = useState<number | undefined>(undefined)

// bail on odd states
if (width < 1 || height < 1) {
return null
}

// logs
for (let i = 0; i < tape.logs.length && context.y >= 0; ++i) {
const [id, level, source, ...message] = tape.logs[i]
const messagetext = message.map((v) => JSON.stringify(v)).join(' ')
const rowtext = `${id.slice(id.length - 3)}>${source}>${level}: ${messagetext}`
const measure = tokenizeandmeasuretextformat(rowtext, width, height)
//
context.y -= measure?.y ?? 1
const reset = context.y
tokenizeandwritetextformat(rowtext, context)
context.y = reset
}

// input & selection
const visiblerange = width - 2
const inputindex = (height - 1) * width
const hasselection = ispresent(selection)
const ii1 = hasselection ? Math.min(selection, cursor) : cursor
const ii2 = hasselection ? Math.max(selection, cursor) : cursor
const iic = ii2 - ii1

const inputstateselected = hasselection
? stringsplice(inputstate, ii1, iic)
: inputstate

// draw input line
const inputline = inputstate.padEnd(width, '_')
applystrtoindex(inputindex, inputline, context)
// draw selection
if (hasselection) {
applycolortoindexes(inputindex + ii1, inputindex + ii2, 15, 8, context)
}
// draw cursor
if (blink) {
applystrtoindex(inputindex + cursor, String.fromCharCode(221), context)
}

// update state
function inputstatesetsplice(index: number, count: number, insert?: string) {
setinputstate(stringsplice(inputstate, index, count, insert))
}

function trackselection(shift: boolean) {
if (shift) {
if (!ispresent(selection)) {
setselection(clamp(cursor - 1, 0, inputstate.length))
}
} else {
setselection(undefined)
}
}

function deleteselection() {
setcursor(ii1)
setselection(undefined)
inputstatesetsplice(ii1, iic)
}

return (
<group
position={[marginX * 0.5 + left, marginY + top, 0]}
scale={[SCALE, SCALE, 1.0]}
>
<UserHotkey hotkey="Escape">
{() => {
tapesetopen(false)
}}
</UserHotkey>
<UserHotkey hotkey="Shift+?">
{() => {
tapesetopen(!tape.open)
}}
</UserHotkey>
{tape.open && (
{tape.open ? (
<UserFocus>
<UserHotkey hotkey="Escape">{() => tapesetopen(false)}</UserHotkey>
<TileSnapshot width={width} height={height} tiles={tiles} />
<UserInput
MENU_BUTTON={(mods) => {
tapesetmode(mods.shift ? -1 : 1)
}}
MOVE_LEFT={(mods) => {
trackselection(mods.shift)
setcursor(clamp(cursor - 1, 0, inputstate.length))
}}
MOVE_RIGHT={(mods) => {
trackselection(mods.shift)
setcursor(clamp(cursor + 1, 0, inputstate.length))
}}
OK_BUTTON={() => {
// invoke command
console.info('invoke', { inputstate, inputstateselected })
setcursor(0)
setselection(undefined)
setinputstate('')
}}
keydown={(event) => {
//
const { key } = event
const lkey = key.toLowerCase()
const mods: UserInputMods = {
alt: event.altKey,
ctrl: isMac ? event.metaKey : event.ctrlKey,
shift: event.shiftKey,
}

console.info('keydown', lkey)
switch (lkey) {
case 'delete':
if (hasselection) {
deleteselection()
} else if (inputstate.length > 0) {
inputstatesetsplice(cursor, 1)
}
break
case 'backspace':
if (hasselection) {
deleteselection()
} else if (cursor > 0) {
inputstatesetsplice(cursor - 1, 1)
setcursor(cursor - 1)
}
break
default:
if (mods.ctrl) {
switch (lkey) {
case 'a':
setselection(0)
setcursor(inputstate.length)
break
case 'c':
if (ispresent(navigator.clipboard)) {
navigator.clipboard
.writeText(inputstateselected)
.catch((err) => console.error(err))
}
break
case 'v':
if (ispresent(navigator.clipboard)) {
navigator.clipboard
.readText()
.then((text) => {
if (hasselection) {
inputstatesetsplice(ii1, iic, text)
setselection(undefined)
setcursor(ii2)
} else {
inputstatesetsplice(cursor, 0, text)
setcursor(cursor + text.length)
}
})
.catch((err) => console.error(err))
}
break
case 'x':
if (ispresent(navigator.clipboard)) {
navigator.clipboard
.writeText(inputstateselected)
.then(() => deleteselection())
.catch((err) => console.error(err))
}
break
}
} else if (mods.alt) {
// no-op ?? - could this shove text around when you have selection ??
// or jump by 10 ?
} else if (
key.length === 1 &&
inputstate.length < visiblerange
) {
if (hasselection) {
setcursor(ii2)
inputstatesetsplice(ii1, ii2, key)
} else {
setcursor(cursor + 1)
inputstatesetsplice(cursor, cursor, key)
}
}
break
}
}}
/>
</UserFocus>
) : (
<UserHotkey hotkey="Shift+?">
{() => tapesetopen(!tape.open)}
</UserHotkey>
)}
</group>
)
Expand Down
4 changes: 2 additions & 2 deletions zss/gadget/components/usetiles.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useMemo } from 'react'
import { useMemo } from 'react'
import { objectKeys } from 'ts-extras'
import { proxy, useSnapshot } from 'valtio'
import { ispresent } from 'zss/mapping/types'
Expand All @@ -23,7 +23,7 @@ export function useTiles(
color: new Array(size).fill(color),
bg: new Array(size).fill(bg),
})
}, [width, height])
}, [size, char, color, bg])

return tiles
}
Expand Down
10 changes: 10 additions & 0 deletions zss/mapping/string.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export function stringsplice(
str: string,
index: number,
count: number,
insert?: string,
) {
const a = str.slice(0, index)
const b = str.slice(index + count)
return `${a}${insert ?? ''}${b}`
}
Binary file modified zss/terminal/scratches.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 6 additions & 3 deletions zss/terminal/terminal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import {
EffectComposer,
BrightnessContrast,
ChromaticAberration,
Bloom,
} from '@react-three/postprocessing'
import { BlendFunction, KernelSize, Resolution } from 'postprocessing'
import { BlendFunction } from 'postprocessing'
import { Suspense, useEffect, useState } from 'react'
import Stats from 'stats.js'
import { NearestFilter } from 'three'
import { STATS_DEV } from 'zss/config'
import { createplatform } from 'zss/platform'

Expand All @@ -22,6 +22,9 @@ const TUG = 0.0006

export function Terminal() {
const splat = useTexture(decoimageurl)
splat.minFilter = NearestFilter
splat.magFilter = NearestFilter

const [stats] = useState(() => new Stats())
const [active, setActive] = useState(false)

Expand Down Expand Up @@ -75,7 +78,7 @@ export function Terminal() {
texture={splat}
blendFunction={BlendFunction.OVERLAY}
/>
<BrightnessContrast brightness={0.02} contrast={0.1} />
<BrightnessContrast brightness={0.04} contrast={0.1} />
</EffectComposer>
</Suspense>
</>
Expand Down

0 comments on commit 3b2ed2b

Please sign in to comment.