-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathLucy.py
More file actions
240 lines (206 loc) · 8.56 KB
/
Copy pathLucy.py
File metadata and controls
240 lines (206 loc) · 8.56 KB
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
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
#!/usr/bin/env python3
import curses
import os
import subprocess
import sys
MIN_TERM_HEIGHT = 15
MIN_TERM_WIDTH = 65
def is_installed():
"""True when the workspace has been built (mirrors launch_lucy.sh's check)."""
return os.path.isfile("install/setup.bash")
def get_dev_mode():
if not os.path.exists(".env"):
return False
with open(".env", "r") as f:
for line in f:
if line.strip().startswith("DEV="):
return line.strip().split("=")[1].lower() == "true"
return False
def set_dev_mode(is_enabled):
lines = []
dev_found = False
if os.path.exists(".env"):
with open(".env", "r") as f:
lines = f.readlines()
with open(".env", "w") as f:
for line in lines:
if line.strip().startswith("DEV="):
f.write(f"DEV={str(is_enabled).lower()}\n")
dev_found = True
else:
f.write(line)
if not dev_found:
f.write(f"DEV={str(is_enabled).lower()}\n")
def run_command(command, interactive=False):
"""Runs a command.
If interactive is True, runs natively in the terminal.
"""
print(f"--- Running: {' '.join(command)} ---")
try:
if interactive:
# Inherit standard IO to maintain terminal size and TTY functionality
return subprocess.run(command).returncode
else:
# Popen is fine for non-interactive scripts like install/build
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
while True:
output = process.stdout.readline()
if output == '' and process.poll() is not None:
break
if output:
print(output.strip())
return process.poll()
except FileNotFoundError:
print(f"Error: Command '{command[0]}' not found. Make sure it's in your PATH and executable.")
return -1
except Exception as e:
print(f"An error occurred: {e}")
return -1
def not_installed_screen(stdscr):
"""First-run screen shown when the workspace isn't built yet.
Returns True to install, False to close. (Terminal size is guaranteed by the
pre-check in __main__.)"""
curses.curs_set(0)
stdscr.nodelay(0)
stdscr.timeout(-1)
while True:
stdscr.clear()
h, w = stdscr.getmaxyx()
title = "Lucy Workspace Manager"
stdscr.addstr(0, max(0, (w - len(title)) // 2), title, curses.A_BOLD)
lines = [
("Lucy is not installed on this machine.", curses.A_BOLD),
("", curses.A_NORMAL),
("Press ENTER to install it.", curses.A_NORMAL),
("Press Q or ESC to close.", curses.A_DIM),
]
start = max(2, h // 2 - len(lines) // 2)
for i, (text, attr) in enumerate(lines):
stdscr.addstr(start + i, max(0, (w - len(text)) // 2), text, attr)
stdscr.refresh()
key = stdscr.getch()
if key in (ord('\n'), ord('\r')):
return True
if key in (ord('q'), ord('Q'), 27): # q / ESC
return False
def main_tui(stdscr):
"""The main curses TUI function. Returns the command to run."""
h, w = stdscr.getmaxyx()
if h < MIN_TERM_HEIGHT or w < MIN_TERM_WIDTH:
return "TerminalTooSmall"
curses.curs_set(0)
stdscr.nodelay(0)
stdscr.timeout(-1)
curses.start_color()
curses.use_default_colors()
curses.init_pair(1, curses.COLOR_CYAN, -1)
is_dev_mode = get_dev_mode()
current_idx = 0
options = ["Launch", "---", "Update", "Rebuild", "Exit", "---", "Developer Mode"]
while True:
stdscr.clear()
h, w = stdscr.getmaxyx()
title = "Lucy Workspace Manager"
stdscr.addstr(0, max(0, (w - len(title)) // 2), title, curses.A_BOLD)
for i, option in enumerate(options):
if option == "---":
stdscr.addstr(2 + i, 4, "----------------------")
continue
prefix = "> " if current_idx == i else " "
if option == "Developer Mode":
checkbox = "[x]" if is_dev_mode else "[ ]"
stdscr.addstr(2 + i, 4, f"{prefix}{checkbox} {option}")
else:
stdscr.addstr(2 + i, 4, f"{prefix}{option}")
stdscr.addstr(h - 2, 2, "Enter/Space: Select/Toggle | Up/Down: Navigate", curses.A_DIM)
stdscr.refresh()
key = stdscr.getch()
if key == curses.KEY_UP:
current_idx = (current_idx - 1 + len(options)) % len(options)
if options[current_idx] == "---":
current_idx = (current_idx - 1 + len(options)) % len(options)
elif key == curses.KEY_DOWN:
current_idx = (current_idx + 1) % len(options)
if options[current_idx] == "---":
current_idx = (current_idx + 1) % len(options)
elif key in [ord(' '), ord('\n')]:
selected_option = options[current_idx]
if selected_option == "Developer Mode":
is_dev_mode = not is_dev_mode
set_dev_mode(is_dev_mode)
elif selected_option == "Update":
return {"cmd": ["./install.sh"], "interactive": False, "name": "Install"}
elif selected_option == "Rebuild":
return {"cmd": ["./install.sh", "--build-only"], "interactive": False, "name": "Rebuild"}
elif selected_option == "Launch":
return {"cmd": ["./launch_lucy.sh"], "interactive": True, "name": "Launch"}
elif selected_option == "Exit":
return None
if __name__ == "__main__":
# This initial check is done before curses.wrapper to provide a clean error message
# without the screen flicker of initializing and de-initializing curses.
def check_initial_size():
stdscr = curses.initscr()
h, w = stdscr.getmaxyx()
curses.endwin()
return h >= MIN_TERM_HEIGHT and w >= MIN_TERM_WIDTH
if not check_initial_size():
print("Error: Terminal window is too small.", file=sys.stderr)
print(f"Please increase the terminal size to at least {MIN_TERM_WIDTH}x{MIN_TERM_HEIGHT} characters.", file=sys.stderr)
sys.exit(1)
# First run: nothing built yet — offer to install before showing the menu.
if not is_installed():
try:
wants_install = curses.wrapper(not_installed_screen)
except KeyboardInterrupt:
print("\nExiting.")
sys.exit(0)
except curses.error as e:
print(f"A terminal error occurred: {e}", file=sys.stderr)
print("This might be due to resizing the window. Please restart.", file=sys.stderr)
sys.exit(1)
if not wants_install:
sys.exit(0)
rc = run_command(["./install.sh"], interactive=False)
if rc != 0:
print(f"\n--- Install finished with exit code {rc} ---")
print("Press Enter to exit.")
input()
sys.exit(rc)
print("\n--- Install finished successfully. ---")
print("Press Enter to continue to the menu.")
input()
while True:
task = None
try:
task = curses.wrapper(main_tui)
except KeyboardInterrupt:
print("\nExiting.")
sys.exit(0)
except curses.error as e:
print(f"A terminal error occurred: {e}", file=sys.stderr)
print("This might be due to resizing the window. Please restart.", file=sys.stderr)
sys.exit(1)
if isinstance(task, str) and task == "TerminalTooSmall":
# This case is handled by the pre-check, but as a fallback.
print("Error: Terminal window is too small.", file=sys.stderr)
print(f"Please increase the terminal size to at least {MIN_TERM_WIDTH}x{MIN_TERM_HEIGHT} characters.", file=sys.stderr)
sys.exit(1)
if not task:
# User selected Exit
break
rc = run_command(task["cmd"], interactive=task.get("interactive", False))
if task.get("interactive", False):
print(f"--- Session finished with exit code {rc} ---")
break
task_name = task.get("name")
if task_name in ["Install", "Rebuild"] and rc == 0:
print(f"\n--- Task '{task_name}' finished successfully. ---")
print("Press Enter to return to the menu.")
input()
else:
print(f"\n--- Task '{task_name}' finished with exit code {rc} ---")
print("Press Enter to exit.")
input()
break
sys.exit(0)