File size: 6,252 Bytes
e6b25de
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
"""xros_shell.py — the spatial shell: a command interpreter over the shared world.

This is the "terminal" half of the spatial OS. The terminal (in the Quest app)
sends a command string; the shell parses it, mutates the shared WorldState, and
returns text output. World mutations are broadcast to every connected client by
the server, so a `spawn panel` typed in the terminal makes a panel appear in the
spatial scene for everyone.

Commands:
  help                          list commands
  ls | list                     list every object in the world
  spawn <type> [x y z] [opts]   create an object (panel, cube, ...)
  move <id> x y z               move an object
  rm <id> | remove <id>         remove an object
  clear                         remove every object
  gen <description>             generative UI (stub: rule-based panel generator)

The `gen` command is the stub for the generative-UI layer: it maps a natural-
language description to a panel type via keyword rules, so the concept works
end to end before a real LLM is wired in.
"""

from __future__ import annotations

import shlex
from typing import List, Optional, Tuple

from xros_world import Pose, WorldState

HELP_TEXT = """spatial shell commands:
  help
  ls | list
  spawn <type> [x y z] [--title T] [--color C]
  move <id> x y z
  rm <id> | remove <id>
  clear
  gen <description>   (generative UI — stub)
"""

# Keyword -> panel type rules for the generative-UI stub.
GEN_RULES: List[Tuple[List[str], str]] = [
    (["clock", "time", "watch"], "clock"),
    (["note", "memo", "remind", "remember"], "note"),
    (["todo", "task", "checklist", "list"], "todo"),
    (["chart", "graph", "plot", "bar"], "chart"),
    (["image", "photo", "picture", "pic"], "image"),
    (["video", "player", "movie"], "video"),
    (["map", "location", "where"], "map"),
    (["browser", "web", "search"], "browser"),
]


class SpatialShell:
    def __init__(self, world: WorldState):
        self.world = world

    async def execute(self, command: str) -> str:
        """Run one command line and return its text output."""
        if not command.strip():
            return ""
        try:
            parts = shlex.split(command)
        except ValueError as e:
            return f"error: {e}"
        if not parts:
            return ""
        cmd, args = parts[0].lower(), parts[1:]
        handler = getattr(self, f"cmd_{cmd}", None)
        if handler is None:
            return f"error: unknown command '{cmd}' (try 'help')"
        try:
            return await handler(args)
        except Exception as e:  # surface any error as terminal text, never crash
            return f"error: {e}"

    # ---- commands ------------------------------------------------------

    async def cmd_help(self, args: List[str]) -> str:
        return HELP_TEXT.strip()

    async def cmd_ls(self, args: List[str]) -> str:
        objs = await self.world.snapshot()
        if not objs:
            return "(empty world)"
        lines = []
        for o in objs:
            x, y, z = o["pose"]["position"]
            title = o["properties"].get("title", "")
            lines.append(f"{o['id'][:8]}  {o['type']:<8}  ({x:.1f}, {y:.1f}, {z:.1f})  {title}")
        return "\n".join(lines)

    async def cmd_spawn(self, args: List[str]) -> str:
        type_, pos, opts = self._parse_spawn(args)
        obj = await self.world.spawn(
            type=type_,
            pose=Pose(position=pos),
            properties=opts,
        )
        title = opts.get("title", "")
        return f"spawned {type_} {obj.id[:8]}" + (f" \"{title}\"" if title else "")

    async def cmd_move(self, args: List[str]) -> str:
        if len(args) < 4:
            return "usage: move <id> x y z"
        obj_id, xyz = args[0], [float(a) for a in args[1:4]]
        obj = await self.world.move(obj_id, Pose(position=xyz))
        return f"moved {obj_id[:8]} to ({xyz[0]:.1f}, {xyz[1]:.1f}, {xyz[2]:.1f})"

    async def cmd_rm(self, args: List[str]) -> str:
        if not args:
            return "usage: rm <id>"
        obj = await self.world.remove(args[0])
        return f"removed {args[0][:8]} ({obj.type})"

    async def cmd_clear(self, args: List[str]) -> str:
        objs = await self.world.snapshot()
        for o in objs:
            await self.world.remove(o["id"])
        return f"cleared {len(objs)} object(s)"

    async def cmd_gen(self, args: List[str]) -> str:
        """Generative-UI stub: map a description to a panel via keyword rules."""
        if not args:
            return "usage: gen <description>"
        description = " ".join(args)
        panel_type = self._classify(description)
        obj = await self.world.spawn(
            type="panel",
            pose=Pose(position=[0.0, 0.0, -1.5]),
            properties={"title": description, "panel_type": panel_type},
        )
        return f"generated {panel_type} panel {obj.id[:8]} for \"{description}\""

    # ---- helpers -------------------------------------------------------

    def _parse_spawn(self, args: List[str]) -> Tuple[str, List[float], dict]:
        """Parse `spawn <type> [x y z] [--title T] [--color C]`."""
        if not args:
            raise ValueError("usage: spawn <type> [x y z] [--title T] [--color C]")
        type_ = args[0]
        rest = args[1:]
        pos: List[float] = [0.0, 0.0, -1.5]  # default: 1.5m in front of the user
        opts: dict = {}
        i = 0
        # leading three numbers = position
        nums: List[float] = []
        while i < len(rest) and len(nums) < 3:
            try:
                nums.append(float(rest[i]))
                i += 1
            except ValueError:
                break
        if nums:
            pos = nums
        # remaining --key value pairs
        while i < len(rest):
            if rest[i].startswith("--") and i + 1 < len(rest):
                opts[rest[i][2:]] = rest[i + 1]
                i += 2
            else:
                i += 1
        return type_, pos, opts

    def _classify(self, description: str) -> str:
        low = description.lower()
        for keywords, panel_type in GEN_RULES:
            if any(k in low for k in keywords):
                return panel_type
        return "generic"