LoveLogicAI commited on
Commit
e6b25de
·
verified ·
1 Parent(s): 12f4745

spatial shell

Browse files
Files changed (1) hide show
  1. xros_shell.py +169 -0
xros_shell.py ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """xros_shell.py — the spatial shell: a command interpreter over the shared world.
2
+
3
+ This is the "terminal" half of the spatial OS. The terminal (in the Quest app)
4
+ sends a command string; the shell parses it, mutates the shared WorldState, and
5
+ returns text output. World mutations are broadcast to every connected client by
6
+ the server, so a `spawn panel` typed in the terminal makes a panel appear in the
7
+ spatial scene for everyone.
8
+
9
+ Commands:
10
+ help list commands
11
+ ls | list list every object in the world
12
+ spawn <type> [x y z] [opts] create an object (panel, cube, ...)
13
+ move <id> x y z move an object
14
+ rm <id> | remove <id> remove an object
15
+ clear remove every object
16
+ gen <description> generative UI (stub: rule-based panel generator)
17
+
18
+ The `gen` command is the stub for the generative-UI layer: it maps a natural-
19
+ language description to a panel type via keyword rules, so the concept works
20
+ end to end before a real LLM is wired in.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import shlex
26
+ from typing import List, Optional, Tuple
27
+
28
+ from xros_world import Pose, WorldState
29
+
30
+ HELP_TEXT = """spatial shell commands:
31
+ help
32
+ ls | list
33
+ spawn <type> [x y z] [--title T] [--color C]
34
+ move <id> x y z
35
+ rm <id> | remove <id>
36
+ clear
37
+ gen <description> (generative UI — stub)
38
+ """
39
+
40
+ # Keyword -> panel type rules for the generative-UI stub.
41
+ GEN_RULES: List[Tuple[List[str], str]] = [
42
+ (["clock", "time", "watch"], "clock"),
43
+ (["note", "memo", "remind", "remember"], "note"),
44
+ (["todo", "task", "checklist", "list"], "todo"),
45
+ (["chart", "graph", "plot", "bar"], "chart"),
46
+ (["image", "photo", "picture", "pic"], "image"),
47
+ (["video", "player", "movie"], "video"),
48
+ (["map", "location", "where"], "map"),
49
+ (["browser", "web", "search"], "browser"),
50
+ ]
51
+
52
+
53
+ class SpatialShell:
54
+ def __init__(self, world: WorldState):
55
+ self.world = world
56
+
57
+ async def execute(self, command: str) -> str:
58
+ """Run one command line and return its text output."""
59
+ if not command.strip():
60
+ return ""
61
+ try:
62
+ parts = shlex.split(command)
63
+ except ValueError as e:
64
+ return f"error: {e}"
65
+ if not parts:
66
+ return ""
67
+ cmd, args = parts[0].lower(), parts[1:]
68
+ handler = getattr(self, f"cmd_{cmd}", None)
69
+ if handler is None:
70
+ return f"error: unknown command '{cmd}' (try 'help')"
71
+ try:
72
+ return await handler(args)
73
+ except Exception as e: # surface any error as terminal text, never crash
74
+ return f"error: {e}"
75
+
76
+ # ---- commands ------------------------------------------------------
77
+
78
+ async def cmd_help(self, args: List[str]) -> str:
79
+ return HELP_TEXT.strip()
80
+
81
+ async def cmd_ls(self, args: List[str]) -> str:
82
+ objs = await self.world.snapshot()
83
+ if not objs:
84
+ return "(empty world)"
85
+ lines = []
86
+ for o in objs:
87
+ x, y, z = o["pose"]["position"]
88
+ title = o["properties"].get("title", "")
89
+ lines.append(f"{o['id'][:8]} {o['type']:<8} ({x:.1f}, {y:.1f}, {z:.1f}) {title}")
90
+ return "\n".join(lines)
91
+
92
+ async def cmd_spawn(self, args: List[str]) -> str:
93
+ type_, pos, opts = self._parse_spawn(args)
94
+ obj = await self.world.spawn(
95
+ type=type_,
96
+ pose=Pose(position=pos),
97
+ properties=opts,
98
+ )
99
+ title = opts.get("title", "")
100
+ return f"spawned {type_} {obj.id[:8]}" + (f" \"{title}\"" if title else "")
101
+
102
+ async def cmd_move(self, args: List[str]) -> str:
103
+ if len(args) < 4:
104
+ return "usage: move <id> x y z"
105
+ obj_id, xyz = args[0], [float(a) for a in args[1:4]]
106
+ obj = await self.world.move(obj_id, Pose(position=xyz))
107
+ return f"moved {obj_id[:8]} to ({xyz[0]:.1f}, {xyz[1]:.1f}, {xyz[2]:.1f})"
108
+
109
+ async def cmd_rm(self, args: List[str]) -> str:
110
+ if not args:
111
+ return "usage: rm <id>"
112
+ obj = await self.world.remove(args[0])
113
+ return f"removed {args[0][:8]} ({obj.type})"
114
+
115
+ async def cmd_clear(self, args: List[str]) -> str:
116
+ objs = await self.world.snapshot()
117
+ for o in objs:
118
+ await self.world.remove(o["id"])
119
+ return f"cleared {len(objs)} object(s)"
120
+
121
+ async def cmd_gen(self, args: List[str]) -> str:
122
+ """Generative-UI stub: map a description to a panel via keyword rules."""
123
+ if not args:
124
+ return "usage: gen <description>"
125
+ description = " ".join(args)
126
+ panel_type = self._classify(description)
127
+ obj = await self.world.spawn(
128
+ type="panel",
129
+ pose=Pose(position=[0.0, 0.0, -1.5]),
130
+ properties={"title": description, "panel_type": panel_type},
131
+ )
132
+ return f"generated {panel_type} panel {obj.id[:8]} for \"{description}\""
133
+
134
+ # ---- helpers -------------------------------------------------------
135
+
136
+ def _parse_spawn(self, args: List[str]) -> Tuple[str, List[float], dict]:
137
+ """Parse `spawn <type> [x y z] [--title T] [--color C]`."""
138
+ if not args:
139
+ raise ValueError("usage: spawn <type> [x y z] [--title T] [--color C]")
140
+ type_ = args[0]
141
+ rest = args[1:]
142
+ pos: List[float] = [0.0, 0.0, -1.5] # default: 1.5m in front of the user
143
+ opts: dict = {}
144
+ i = 0
145
+ # leading three numbers = position
146
+ nums: List[float] = []
147
+ while i < len(rest) and len(nums) < 3:
148
+ try:
149
+ nums.append(float(rest[i]))
150
+ i += 1
151
+ except ValueError:
152
+ break
153
+ if nums:
154
+ pos = nums
155
+ # remaining --key value pairs
156
+ while i < len(rest):
157
+ if rest[i].startswith("--") and i + 1 < len(rest):
158
+ opts[rest[i][2:]] = rest[i + 1]
159
+ i += 2
160
+ else:
161
+ i += 1
162
+ return type_, pos, opts
163
+
164
+ def _classify(self, description: str) -> str:
165
+ low = description.lower()
166
+ for keywords, panel_type in GEN_RULES:
167
+ if any(k in low for k in keywords):
168
+ return panel_type
169
+ return "generic"