bitp 1.0.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bitbake_project/__init__.py +88 -0
- bitbake_project/__main__.py +14 -0
- bitbake_project/cli.py +1580 -0
- bitbake_project/commands/__init__.py +60 -0
- bitbake_project/commands/branch.py +889 -0
- bitbake_project/commands/common.py +2372 -0
- bitbake_project/commands/config.py +1515 -0
- bitbake_project/commands/deps.py +903 -0
- bitbake_project/commands/explore.py +2269 -0
- bitbake_project/commands/export.py +1030 -0
- bitbake_project/commands/fragment.py +884 -0
- bitbake_project/commands/init.py +515 -0
- bitbake_project/commands/projects.py +1505 -0
- bitbake_project/commands/recipe.py +1374 -0
- bitbake_project/commands/repos.py +154 -0
- bitbake_project/commands/search.py +313 -0
- bitbake_project/commands/update.py +181 -0
- bitbake_project/core.py +1811 -0
- bitp-1.0.7.dist-info/METADATA +401 -0
- bitp-1.0.7.dist-info/RECORD +24 -0
- bitp-1.0.7.dist-info/WHEEL +5 -0
- bitp-1.0.7.dist-info/entry_points.txt +3 -0
- bitp-1.0.7.dist-info/licenses/COPYING +338 -0
- bitp-1.0.7.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1515 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright (C) 2025 Bruce Ashfield <bruce.ashfield@gmail.com>
|
|
3
|
+
#
|
|
4
|
+
# SPDX-License-Identifier: GPL-2.0-only
|
|
5
|
+
#
|
|
6
|
+
"""Config command - view and configure repo/layer settings."""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
import shutil
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
import tempfile
|
|
15
|
+
from typing import Dict, List, Optional, Set, Tuple
|
|
16
|
+
|
|
17
|
+
from ..core import (
|
|
18
|
+
Colors,
|
|
19
|
+
current_branch,
|
|
20
|
+
current_head,
|
|
21
|
+
fzf_available,
|
|
22
|
+
get_fzf_preview_resize_bindings,
|
|
23
|
+
git_toplevel,
|
|
24
|
+
load_defaults,
|
|
25
|
+
repo_is_clean,
|
|
26
|
+
save_defaults,
|
|
27
|
+
FZF_THEMES,
|
|
28
|
+
FZF_TEXT_COLORS,
|
|
29
|
+
get_current_theme_name,
|
|
30
|
+
get_current_text_color_name,
|
|
31
|
+
get_fzf_color_args,
|
|
32
|
+
get_custom_colors,
|
|
33
|
+
fzf_theme_picker,
|
|
34
|
+
fzf_text_color_picker,
|
|
35
|
+
fzf_custom_color_menu,
|
|
36
|
+
TERMINAL_COLOR_ELEMENTS,
|
|
37
|
+
ANSI_COLORS,
|
|
38
|
+
get_terminal_color,
|
|
39
|
+
set_terminal_color,
|
|
40
|
+
)
|
|
41
|
+
from .projects import (
|
|
42
|
+
get_directory_browser,
|
|
43
|
+
_pick_directory_browser,
|
|
44
|
+
get_git_viewer,
|
|
45
|
+
_pick_git_viewer,
|
|
46
|
+
get_preview_layout,
|
|
47
|
+
_pick_preview_layout,
|
|
48
|
+
get_recipe_use_bitbake_layers,
|
|
49
|
+
_pick_recipe_use_bitbake_layers,
|
|
50
|
+
)
|
|
51
|
+
from .common import (
|
|
52
|
+
resolve_bblayers_path,
|
|
53
|
+
resolve_base_and_layers,
|
|
54
|
+
extract_layer_paths,
|
|
55
|
+
collect_repos,
|
|
56
|
+
repo_display_name,
|
|
57
|
+
layer_display_name,
|
|
58
|
+
get_push_target,
|
|
59
|
+
set_push_target,
|
|
60
|
+
remove_push_target,
|
|
61
|
+
add_extra_repo,
|
|
62
|
+
add_hidden_repo,
|
|
63
|
+
remove_hidden_repo,
|
|
64
|
+
get_hidden_repos,
|
|
65
|
+
discover_layers,
|
|
66
|
+
discover_git_repos,
|
|
67
|
+
add_layer_to_bblayers,
|
|
68
|
+
remove_layer_from_bblayers,
|
|
69
|
+
build_layer_collection_map,
|
|
70
|
+
get_upstream_count_ls_remote,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def run_config_edit(args) -> int:
|
|
74
|
+
"""Edit a layer's layer.conf file in $EDITOR."""
|
|
75
|
+
pairs, _repo_sets = resolve_base_and_layers(args.bblayers)
|
|
76
|
+
layers = [layer for layer, repo in pairs]
|
|
77
|
+
|
|
78
|
+
# Find the target layer by index, name, or path
|
|
79
|
+
target_layer = None
|
|
80
|
+
try:
|
|
81
|
+
idx = int(args.layer)
|
|
82
|
+
if 1 <= idx <= len(layers):
|
|
83
|
+
target_layer = layers[idx - 1]
|
|
84
|
+
else:
|
|
85
|
+
print(f"Invalid index {idx}. Valid range: 1-{len(layers)}")
|
|
86
|
+
return 1
|
|
87
|
+
except ValueError:
|
|
88
|
+
# Try matching by layer name (directory name)
|
|
89
|
+
for layer in layers:
|
|
90
|
+
if layer_display_name(layer).lower() == args.layer.lower():
|
|
91
|
+
target_layer = layer
|
|
92
|
+
break
|
|
93
|
+
# Then try as path
|
|
94
|
+
if not target_layer and os.path.isdir(args.layer):
|
|
95
|
+
target_layer = os.path.abspath(args.layer)
|
|
96
|
+
# Finally try partial path match
|
|
97
|
+
if not target_layer:
|
|
98
|
+
for layer in layers:
|
|
99
|
+
if args.layer in layer or layer.endswith(args.layer):
|
|
100
|
+
target_layer = layer
|
|
101
|
+
break
|
|
102
|
+
|
|
103
|
+
if not target_layer:
|
|
104
|
+
print(f"Layer not found: {args.layer}")
|
|
105
|
+
print("\nAvailable layers:")
|
|
106
|
+
for idx, layer in enumerate(layers, start=1):
|
|
107
|
+
print(f" {idx}. {layer_display_name(layer)}: {layer}")
|
|
108
|
+
return 1
|
|
109
|
+
|
|
110
|
+
# Find layer.conf
|
|
111
|
+
layer_conf = os.path.join(target_layer, "conf", "layer.conf")
|
|
112
|
+
if not os.path.isfile(layer_conf):
|
|
113
|
+
print(f"layer.conf not found: {layer_conf}")
|
|
114
|
+
return 1
|
|
115
|
+
|
|
116
|
+
# Get editor
|
|
117
|
+
editor = os.environ.get("EDITOR", os.environ.get("VISUAL", "vi"))
|
|
118
|
+
|
|
119
|
+
print(f"Editing: {layer_conf}")
|
|
120
|
+
try:
|
|
121
|
+
subprocess.run([editor, layer_conf], check=True)
|
|
122
|
+
return 0
|
|
123
|
+
except subprocess.CalledProcessError as e:
|
|
124
|
+
print(f"Editor exited with code {e.returncode}")
|
|
125
|
+
return e.returncode
|
|
126
|
+
except FileNotFoundError:
|
|
127
|
+
print(f"Editor not found: {editor}")
|
|
128
|
+
print("Set $EDITOR environment variable to your preferred editor")
|
|
129
|
+
return 1
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def run_config(args) -> int:
|
|
134
|
+
# Handle 'edit' command: config edit <layer>
|
|
135
|
+
if args.repo == "edit":
|
|
136
|
+
if not args.extra_arg:
|
|
137
|
+
print("Usage: bit config edit <layer>")
|
|
138
|
+
print(" layer: index, name, or path")
|
|
139
|
+
return 1
|
|
140
|
+
# Create a mock args object with layer attribute
|
|
141
|
+
class EditArgs:
|
|
142
|
+
pass
|
|
143
|
+
edit_args = EditArgs()
|
|
144
|
+
edit_args.bblayers = args.bblayers
|
|
145
|
+
edit_args.layer = args.extra_arg
|
|
146
|
+
return run_config_edit(edit_args)
|
|
147
|
+
|
|
148
|
+
defaults = load_defaults(args.defaults_file)
|
|
149
|
+
repos, _repo_sets = collect_repos(args.bblayers, defaults)
|
|
150
|
+
|
|
151
|
+
# If no repo specified, use fzf interactive interface
|
|
152
|
+
if args.repo is None:
|
|
153
|
+
return fzf_config_repos(repos, defaults, args.bblayers, args.defaults_file)
|
|
154
|
+
|
|
155
|
+
# Find the target repo by index, display name, or path
|
|
156
|
+
target_repo = None
|
|
157
|
+
try:
|
|
158
|
+
idx = int(args.repo)
|
|
159
|
+
if 1 <= idx <= len(repos):
|
|
160
|
+
target_repo = repos[idx - 1]
|
|
161
|
+
else:
|
|
162
|
+
print(f"Invalid index {idx}. Valid range: 1-{len(repos)}")
|
|
163
|
+
return 1
|
|
164
|
+
except ValueError:
|
|
165
|
+
# Try matching by display name first
|
|
166
|
+
for repo in repos:
|
|
167
|
+
if repo_display_name(repo).lower() == args.repo.lower():
|
|
168
|
+
target_repo = repo
|
|
169
|
+
break
|
|
170
|
+
# Then try as path
|
|
171
|
+
if not target_repo and os.path.isdir(args.repo):
|
|
172
|
+
target_repo = os.path.abspath(args.repo)
|
|
173
|
+
# Finally try partial path match
|
|
174
|
+
if not target_repo:
|
|
175
|
+
for repo in repos:
|
|
176
|
+
if args.repo in repo or repo.endswith(args.repo):
|
|
177
|
+
target_repo = repo
|
|
178
|
+
break
|
|
179
|
+
|
|
180
|
+
if not target_repo:
|
|
181
|
+
print(f"Repo not found: {args.repo}")
|
|
182
|
+
return 1
|
|
183
|
+
|
|
184
|
+
# If -e/--edit specified, open interactive submenu for this repo
|
|
185
|
+
if getattr(args, 'edit', False):
|
|
186
|
+
fzf_repo_config(target_repo, defaults, args.defaults_file)
|
|
187
|
+
return 0
|
|
188
|
+
|
|
189
|
+
# If no options specified, show current config for this repo
|
|
190
|
+
if args.display_name is None and args.update_default is None:
|
|
191
|
+
display = repo_display_name(target_repo)
|
|
192
|
+
update_default = defaults.get(target_repo, "rebase")
|
|
193
|
+
push_target = get_push_target(defaults, target_repo)
|
|
194
|
+
print(f"Repo: {target_repo}")
|
|
195
|
+
print(f"Display name: {display}")
|
|
196
|
+
try:
|
|
197
|
+
custom = subprocess.check_output(
|
|
198
|
+
["git", "-C", target_repo, "config", "--get", "bit.display-name"],
|
|
199
|
+
stderr=subprocess.DEVNULL,
|
|
200
|
+
text=True,
|
|
201
|
+
).strip()
|
|
202
|
+
if custom:
|
|
203
|
+
print(" (custom override)")
|
|
204
|
+
else:
|
|
205
|
+
print(" (auto-detected)")
|
|
206
|
+
except subprocess.CalledProcessError:
|
|
207
|
+
print(" (auto-detected)")
|
|
208
|
+
print(f"Update default: {update_default}")
|
|
209
|
+
if push_target:
|
|
210
|
+
print(f"Push target: {push_target.get('push_url', '')}")
|
|
211
|
+
if push_target.get('branch_prefix'):
|
|
212
|
+
print(f" Branch prefix: {push_target['branch_prefix']}")
|
|
213
|
+
return 0
|
|
214
|
+
|
|
215
|
+
# Handle display name changes
|
|
216
|
+
if args.display_name is not None:
|
|
217
|
+
if args.display_name == "":
|
|
218
|
+
# Clear custom name
|
|
219
|
+
try:
|
|
220
|
+
subprocess.run(
|
|
221
|
+
["git", "-C", target_repo, "config", "--unset", "bit.display-name"],
|
|
222
|
+
check=True,
|
|
223
|
+
)
|
|
224
|
+
print(f"Cleared custom display name for {target_repo}")
|
|
225
|
+
print(f"Now using: {repo_display_name(target_repo)}")
|
|
226
|
+
except subprocess.CalledProcessError:
|
|
227
|
+
print(f"No custom display name was set for {target_repo}")
|
|
228
|
+
else:
|
|
229
|
+
subprocess.run(
|
|
230
|
+
["git", "-C", target_repo, "config", "bit.display-name", args.display_name],
|
|
231
|
+
check=True,
|
|
232
|
+
)
|
|
233
|
+
print(f"Set display name for {target_repo}")
|
|
234
|
+
print(f" {args.display_name}")
|
|
235
|
+
|
|
236
|
+
# Handle update default changes
|
|
237
|
+
if args.update_default is not None:
|
|
238
|
+
old_default = defaults.get(target_repo, "rebase")
|
|
239
|
+
defaults[target_repo] = args.update_default
|
|
240
|
+
save_defaults(args.defaults_file, defaults)
|
|
241
|
+
print(f"Set update default for {target_repo}")
|
|
242
|
+
print(f" {old_default} -> {args.update_default}")
|
|
243
|
+
|
|
244
|
+
return 0
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def show_repo_status_detail(repo: str, branch: str) -> None:
|
|
249
|
+
"""Show detailed status for a repo (local and upstream commits)."""
|
|
250
|
+
if not branch:
|
|
251
|
+
print(" (detached HEAD)")
|
|
252
|
+
return
|
|
253
|
+
|
|
254
|
+
# Local commits
|
|
255
|
+
try:
|
|
256
|
+
local_log = subprocess.check_output(
|
|
257
|
+
["git", "-C", repo, "log", "--oneline", f"origin/{branch}..HEAD"],
|
|
258
|
+
text=True,
|
|
259
|
+
stderr=subprocess.DEVNULL,
|
|
260
|
+
).strip()
|
|
261
|
+
if local_log:
|
|
262
|
+
lines = local_log.splitlines()
|
|
263
|
+
print(f" {len(lines)} local commit(s):")
|
|
264
|
+
for line in lines[:10]:
|
|
265
|
+
print(f" {line}")
|
|
266
|
+
if len(lines) > 10:
|
|
267
|
+
print(f" ... and {len(lines) - 10} more")
|
|
268
|
+
else:
|
|
269
|
+
print(" No local commits")
|
|
270
|
+
except subprocess.CalledProcessError:
|
|
271
|
+
print(" (could not get local commits)")
|
|
272
|
+
|
|
273
|
+
# Upstream commits
|
|
274
|
+
try:
|
|
275
|
+
upstream_log = subprocess.check_output(
|
|
276
|
+
["git", "-C", repo, "log", "--oneline", f"HEAD..origin/{branch}"],
|
|
277
|
+
text=True,
|
|
278
|
+
stderr=subprocess.DEVNULL,
|
|
279
|
+
).strip()
|
|
280
|
+
if upstream_log:
|
|
281
|
+
lines = upstream_log.splitlines()
|
|
282
|
+
print(f" {len(lines)} upstream commit(s) to pull:")
|
|
283
|
+
for line in lines[:5]:
|
|
284
|
+
print(f" {Colors.YELLOW}{line}{Colors.RESET}")
|
|
285
|
+
if len(lines) > 5:
|
|
286
|
+
print(f" ... and {len(lines) - 5} more")
|
|
287
|
+
else:
|
|
288
|
+
print(" Up-to-date with upstream")
|
|
289
|
+
except subprocess.CalledProcessError:
|
|
290
|
+
print(" (could not get upstream commits)")
|
|
291
|
+
|
|
292
|
+
# Working tree status
|
|
293
|
+
is_clean = repo_is_clean(repo)
|
|
294
|
+
if is_clean:
|
|
295
|
+
print(f" Working tree: {Colors.green('clean')}")
|
|
296
|
+
else:
|
|
297
|
+
print(f" Working tree: {Colors.red('DIRTY')}")
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def parse_config_variables(conf_path: str) -> List[Tuple[str, int, str]]:
|
|
302
|
+
"""
|
|
303
|
+
Parse BitBake config file for variable assignments.
|
|
304
|
+
Handles multi-line continuations (lines ending with backslash).
|
|
305
|
+
Returns: [(var_name, line_number, raw_value), ...]
|
|
306
|
+
"""
|
|
307
|
+
if not os.path.isfile(conf_path):
|
|
308
|
+
return []
|
|
309
|
+
|
|
310
|
+
results = []
|
|
311
|
+
seen_vars = set() # Track seen variable names to deduplicate
|
|
312
|
+
# Match variable assignments: VAR = "...", VAR += "...", VAR:append = "...", etc.
|
|
313
|
+
var_pattern = re.compile(r'^([A-Z_][A-Z0-9_]*(?::[a-z_]+)?)\s*[\?\+\.]*=')
|
|
314
|
+
|
|
315
|
+
try:
|
|
316
|
+
with open(conf_path, "r") as f:
|
|
317
|
+
lines = f.readlines()
|
|
318
|
+
|
|
319
|
+
i = 0
|
|
320
|
+
while i < len(lines):
|
|
321
|
+
line = lines[i]
|
|
322
|
+
stripped = line.strip()
|
|
323
|
+
if not stripped or stripped.startswith("#"):
|
|
324
|
+
i += 1
|
|
325
|
+
continue
|
|
326
|
+
|
|
327
|
+
match = var_pattern.match(stripped)
|
|
328
|
+
if match:
|
|
329
|
+
var_name = match.group(1)
|
|
330
|
+
start_line = i + 1 # 1-indexed
|
|
331
|
+
|
|
332
|
+
# Extract value (everything after the = sign)
|
|
333
|
+
eq_pos = stripped.find("=")
|
|
334
|
+
value_parts = [stripped[eq_pos + 1:].strip()]
|
|
335
|
+
|
|
336
|
+
# Handle line continuations (ending with \)
|
|
337
|
+
while value_parts[-1].endswith("\\") and i + 1 < len(lines):
|
|
338
|
+
i += 1
|
|
339
|
+
cont_line = lines[i].strip()
|
|
340
|
+
# Remove trailing \ from previous part
|
|
341
|
+
value_parts[-1] = value_parts[-1][:-1].strip()
|
|
342
|
+
value_parts.append(cont_line)
|
|
343
|
+
|
|
344
|
+
# Combine all parts
|
|
345
|
+
full_value = " ".join(value_parts)
|
|
346
|
+
# Clean up quotes and whitespace
|
|
347
|
+
full_value = full_value.strip().strip('"').strip("'").strip()
|
|
348
|
+
|
|
349
|
+
# Only add if we haven't seen this variable (deduplicate)
|
|
350
|
+
if var_name not in seen_vars:
|
|
351
|
+
seen_vars.add(var_name)
|
|
352
|
+
# Truncate for display
|
|
353
|
+
display_value = full_value
|
|
354
|
+
if len(display_value) > 50:
|
|
355
|
+
display_value = display_value[:47] + "..."
|
|
356
|
+
results.append((var_name, start_line, display_value))
|
|
357
|
+
|
|
358
|
+
i += 1
|
|
359
|
+
except (IOError, OSError):
|
|
360
|
+
pass
|
|
361
|
+
|
|
362
|
+
return results
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def fzf_build_config(bblayers_path: Optional[str] = None) -> None:
|
|
367
|
+
"""Show config menu for project conf files (local.conf, bblayers.conf)."""
|
|
368
|
+
# Find conf directory
|
|
369
|
+
bblayers_conf = resolve_bblayers_path(bblayers_path)
|
|
370
|
+
if not bblayers_conf:
|
|
371
|
+
print("\nCould not find bblayers.conf")
|
|
372
|
+
input("Press Enter to continue...")
|
|
373
|
+
return
|
|
374
|
+
|
|
375
|
+
conf_dir = os.path.dirname(bblayers_conf)
|
|
376
|
+
local_conf = os.path.join(conf_dir, "local.conf")
|
|
377
|
+
|
|
378
|
+
# Track which files are expanded
|
|
379
|
+
expanded_files: Set[str] = set()
|
|
380
|
+
# Cache parsed variables
|
|
381
|
+
var_cache: Dict[str, List[Tuple[str, int, str]]] = {}
|
|
382
|
+
# Cache for bitbake-getvar results
|
|
383
|
+
getvar_cache: Dict[str, str] = {}
|
|
384
|
+
|
|
385
|
+
def get_variables(conf_path: str) -> List[Tuple[str, int, str]]:
|
|
386
|
+
"""Get cached variables for a config file."""
|
|
387
|
+
if conf_path not in var_cache:
|
|
388
|
+
var_cache[conf_path] = parse_config_variables(conf_path)
|
|
389
|
+
return var_cache[conf_path]
|
|
390
|
+
|
|
391
|
+
# Calculate max variable name length for alignment
|
|
392
|
+
def get_max_var_len() -> int:
|
|
393
|
+
max_len = 0
|
|
394
|
+
for conf_path in [bblayers_conf, local_conf]:
|
|
395
|
+
if conf_path in expanded_files:
|
|
396
|
+
for var_name, _, _ in get_variables(conf_path):
|
|
397
|
+
max_len = max(max_len, len(var_name))
|
|
398
|
+
return max_len
|
|
399
|
+
|
|
400
|
+
# Create temp files for preview script and getvar cache
|
|
401
|
+
getvar_cache_file = os.path.join(tempfile.gettempdir(), f"bit-getvar-{os.getpid()}.txt")
|
|
402
|
+
preview_script_file = os.path.join(tempfile.gettempdir(), f"bit-preview-{os.getpid()}.sh")
|
|
403
|
+
|
|
404
|
+
# Write preview script once (content doesn't change)
|
|
405
|
+
with open(preview_script_file, "w") as f:
|
|
406
|
+
f.write(f'''#!/bin/bash
|
|
407
|
+
item="$1"
|
|
408
|
+
cache_file="{getvar_cache_file}"
|
|
409
|
+
if [[ "$item" == VAR:* ]]; then
|
|
410
|
+
var_name=$(echo "$item" | cut -d: -f2)
|
|
411
|
+
file_path=$(echo "$item" | cut -d: -f3-)
|
|
412
|
+
line_num="${{file_path##*:}}"
|
|
413
|
+
file_path="${{file_path%:*}}"
|
|
414
|
+
# Check if we have cached getvar output for this variable
|
|
415
|
+
if [ -f "$cache_file" ]; then
|
|
416
|
+
cached_var=$(head -1 "$cache_file" 2>/dev/null)
|
|
417
|
+
if [ "$cached_var" = "$var_name" ]; then
|
|
418
|
+
echo -e "\\033[1;36mbitbake-getvar $var_name\\033[0m"
|
|
419
|
+
echo
|
|
420
|
+
tail -n +2 "$cache_file"
|
|
421
|
+
exit 0
|
|
422
|
+
fi
|
|
423
|
+
fi
|
|
424
|
+
# Show value from file (15 lines to capture multi-line values)
|
|
425
|
+
echo -e "\\033[1m$var_name\\033[0m (line $line_num)"
|
|
426
|
+
echo
|
|
427
|
+
[ -f "$file_path" ] && sed -n "${{line_num}},$((line_num + 14))p" "$file_path"
|
|
428
|
+
else
|
|
429
|
+
file_path="${{item#FILE:}}"
|
|
430
|
+
[ -f "$file_path" ] && head -40 "$file_path"
|
|
431
|
+
fi
|
|
432
|
+
''')
|
|
433
|
+
os.chmod(preview_script_file, 0o755)
|
|
434
|
+
preview_cmd = f"{preview_script_file} {{1}}"
|
|
435
|
+
|
|
436
|
+
next_selection = None # Track item to select after operations
|
|
437
|
+
try:
|
|
438
|
+
while True:
|
|
439
|
+
menu_lines = []
|
|
440
|
+
max_var_len = get_max_var_len()
|
|
441
|
+
|
|
442
|
+
# bblayers.conf entry
|
|
443
|
+
bblayers_vars = get_variables(bblayers_conf)
|
|
444
|
+
expand_marker = "+ " if bblayers_vars and bblayers_conf not in expanded_files else " "
|
|
445
|
+
if bblayers_conf in expanded_files:
|
|
446
|
+
expand_marker = "- "
|
|
447
|
+
menu_lines.append(f"FILE:{bblayers_conf}\t{expand_marker}Edit bblayers.conf conf/bblayers.conf")
|
|
448
|
+
|
|
449
|
+
# Expanded variables for bblayers.conf
|
|
450
|
+
if bblayers_conf in expanded_files:
|
|
451
|
+
for i, (var_name, line_num, raw_value) in enumerate(bblayers_vars):
|
|
452
|
+
is_last = (i == len(bblayers_vars) - 1)
|
|
453
|
+
prefix = " └─ " if is_last else " ├─ "
|
|
454
|
+
# Show VAR = value aligned with colors
|
|
455
|
+
padded_name = f"{var_name:<{max_var_len}}"
|
|
456
|
+
display_value = raw_value if raw_value else '""'
|
|
457
|
+
if len(display_value) > 40:
|
|
458
|
+
display_value = display_value[:37] + "..."
|
|
459
|
+
colored_name = Colors.cyan(padded_name)
|
|
460
|
+
colored_value = Colors.green(display_value)
|
|
461
|
+
menu_lines.append(f"VAR:{var_name}:{bblayers_conf}:{line_num}\t{prefix}{colored_name} = {colored_value}")
|
|
462
|
+
|
|
463
|
+
# local.conf entry
|
|
464
|
+
if os.path.isfile(local_conf):
|
|
465
|
+
local_vars = get_variables(local_conf)
|
|
466
|
+
expand_marker = "+ " if local_vars and local_conf not in expanded_files else " "
|
|
467
|
+
if local_conf in expanded_files:
|
|
468
|
+
expand_marker = "- "
|
|
469
|
+
menu_lines.append(f"FILE:{local_conf}\t{expand_marker}Edit local.conf conf/local.conf")
|
|
470
|
+
|
|
471
|
+
# Expanded variables for local.conf
|
|
472
|
+
if local_conf in expanded_files:
|
|
473
|
+
for i, (var_name, line_num, raw_value) in enumerate(local_vars):
|
|
474
|
+
is_last = (i == len(local_vars) - 1)
|
|
475
|
+
prefix = " └─ " if is_last else " ├─ "
|
|
476
|
+
padded_name = f"{var_name:<{max_var_len}}"
|
|
477
|
+
display_value = raw_value if raw_value else '""'
|
|
478
|
+
if len(display_value) > 40:
|
|
479
|
+
display_value = display_value[:37] + "..."
|
|
480
|
+
colored_name = Colors.cyan(padded_name)
|
|
481
|
+
colored_value = Colors.green(display_value)
|
|
482
|
+
menu_lines.append(f"VAR:{var_name}:{local_conf}:{line_num}\t{prefix}{colored_name} = {colored_value}")
|
|
483
|
+
else:
|
|
484
|
+
menu_lines.append(f"FILE:{local_conf}\t Edit local.conf (not found)")
|
|
485
|
+
|
|
486
|
+
header = "Project config | Enter=edit | \\=expand | r=bitbake-getvar | q/←=back"
|
|
487
|
+
|
|
488
|
+
try:
|
|
489
|
+
fzf_cmd = [
|
|
490
|
+
"fzf",
|
|
491
|
+
"--ansi",
|
|
492
|
+
"--no-multi",
|
|
493
|
+
"--no-sort",
|
|
494
|
+
"--layout=reverse-list",
|
|
495
|
+
"--height", "50%",
|
|
496
|
+
"--header", header,
|
|
497
|
+
"--prompt", "Select: ",
|
|
498
|
+
"--with-nth", "2..",
|
|
499
|
+
"--delimiter", "\t",
|
|
500
|
+
"--preview", preview_cmd,
|
|
501
|
+
"--preview-window", "right:60%:wrap",
|
|
502
|
+
"--bind", "pgdn:preview-page-down",
|
|
503
|
+
"--bind", "pgup:preview-page-up",
|
|
504
|
+
"--bind", "esc:become(echo BACK)",
|
|
505
|
+
"--bind", "left:become(echo BACK)",
|
|
506
|
+
"--bind", "q:become(echo BACK)",
|
|
507
|
+
"--bind", "b:become(echo FILE:" + bblayers_conf + ")",
|
|
508
|
+
"--bind", "l:become(echo FILE:" + local_conf + ")",
|
|
509
|
+
"--bind", "right:accept",
|
|
510
|
+
"--bind", "\\:become(echo EXPAND {1})",
|
|
511
|
+
"--bind", "r:become(echo GETVAR {1})",
|
|
512
|
+
] + get_fzf_preview_resize_bindings() + get_fzf_color_args()
|
|
513
|
+
|
|
514
|
+
# Jump to selected item's position if we have one
|
|
515
|
+
if next_selection:
|
|
516
|
+
for i, line in enumerate(menu_lines):
|
|
517
|
+
if line.startswith(next_selection + "\t"):
|
|
518
|
+
fzf_cmd.extend(["--sync", "--bind", f"load:pos({i + 1})"])
|
|
519
|
+
break
|
|
520
|
+
next_selection = None
|
|
521
|
+
|
|
522
|
+
result = subprocess.run(
|
|
523
|
+
fzf_cmd,
|
|
524
|
+
input="\n".join(menu_lines),
|
|
525
|
+
stdout=subprocess.PIPE,
|
|
526
|
+
text=True,
|
|
527
|
+
)
|
|
528
|
+
except FileNotFoundError:
|
|
529
|
+
print("fzf not found")
|
|
530
|
+
return
|
|
531
|
+
|
|
532
|
+
if result.returncode != 0 or not result.stdout.strip():
|
|
533
|
+
break
|
|
534
|
+
|
|
535
|
+
output = result.stdout.strip()
|
|
536
|
+
|
|
537
|
+
if output == "BACK":
|
|
538
|
+
break
|
|
539
|
+
elif output.startswith("EXPAND "):
|
|
540
|
+
# Toggle expansion of a file
|
|
541
|
+
item = output[7:].strip()
|
|
542
|
+
if item.startswith("FILE:"):
|
|
543
|
+
file_path = item[5:]
|
|
544
|
+
if file_path in expanded_files:
|
|
545
|
+
expanded_files.discard(file_path)
|
|
546
|
+
else:
|
|
547
|
+
expanded_files.add(file_path)
|
|
548
|
+
elif item.startswith("VAR:"):
|
|
549
|
+
# Expanding on a variable - expand its parent file
|
|
550
|
+
parts = item.split(":")
|
|
551
|
+
if len(parts) >= 3:
|
|
552
|
+
file_path = ":".join(parts[2:-1]) # Handle paths with colons
|
|
553
|
+
if file_path in expanded_files:
|
|
554
|
+
expanded_files.discard(file_path)
|
|
555
|
+
else:
|
|
556
|
+
expanded_files.add(file_path)
|
|
557
|
+
continue
|
|
558
|
+
elif output.startswith("GETVAR "):
|
|
559
|
+
# Run bitbake-getvar for a variable and cache result for preview
|
|
560
|
+
item = output[7:].strip()
|
|
561
|
+
if item.startswith("VAR:"):
|
|
562
|
+
parts = item.split(":")
|
|
563
|
+
if len(parts) >= 2:
|
|
564
|
+
var_name = parts[1]
|
|
565
|
+
# Check if bitbake-getvar is available
|
|
566
|
+
if not shutil.which("bitbake-getvar"):
|
|
567
|
+
print(f"\nbitbake-getvar not found in PATH.")
|
|
568
|
+
print("Source oe-init-build-env first to enable this feature.")
|
|
569
|
+
input("Press Enter to continue...")
|
|
570
|
+
continue
|
|
571
|
+
print(f"\nRunning: bitbake-getvar {var_name}...")
|
|
572
|
+
# Write result to cache file for preview
|
|
573
|
+
with open(getvar_cache_file, "w") as f:
|
|
574
|
+
f.write(var_name + "\n")
|
|
575
|
+
try:
|
|
576
|
+
getvar_result = subprocess.run(
|
|
577
|
+
["bitbake-getvar", var_name],
|
|
578
|
+
stdout=subprocess.PIPE,
|
|
579
|
+
stderr=subprocess.STDOUT,
|
|
580
|
+
text=True,
|
|
581
|
+
)
|
|
582
|
+
with open(getvar_cache_file, "a") as f:
|
|
583
|
+
f.write(getvar_result.stdout)
|
|
584
|
+
# Jump back to this item so preview shows the result
|
|
585
|
+
next_selection = item
|
|
586
|
+
if getvar_result.returncode != 0:
|
|
587
|
+
print(f"Error: {getvar_result.stdout}")
|
|
588
|
+
input("Press Enter to continue...")
|
|
589
|
+
except FileNotFoundError:
|
|
590
|
+
print(f"\nbitbake-getvar not found.")
|
|
591
|
+
input("Press Enter to continue...")
|
|
592
|
+
continue
|
|
593
|
+
elif output.startswith("FILE:"):
|
|
594
|
+
# Extract first field (before tab) then strip FILE: prefix
|
|
595
|
+
file_path = output.split("\t")[0][5:]
|
|
596
|
+
if os.path.isfile(file_path):
|
|
597
|
+
editor = os.environ.get("EDITOR", os.environ.get("VISUAL", "vi"))
|
|
598
|
+
subprocess.run([editor, file_path])
|
|
599
|
+
# Clear cache after editing
|
|
600
|
+
var_cache.pop(file_path, None)
|
|
601
|
+
else:
|
|
602
|
+
print(f"\nFile not found: {file_path}")
|
|
603
|
+
input("Press Enter to continue...")
|
|
604
|
+
elif output.startswith("VAR:"):
|
|
605
|
+
# Open editor at specific line - extract first field before tab
|
|
606
|
+
first_field = output.split("\t")[0]
|
|
607
|
+
parts = first_field.split(":")
|
|
608
|
+
if len(parts) >= 4:
|
|
609
|
+
var_name = parts[1]
|
|
610
|
+
line_num = parts[-1]
|
|
611
|
+
file_path = ":".join(parts[2:-1])
|
|
612
|
+
if os.path.isfile(file_path):
|
|
613
|
+
editor = os.environ.get("EDITOR", os.environ.get("VISUAL", "vi"))
|
|
614
|
+
# Most editors support +line syntax
|
|
615
|
+
subprocess.run([editor, f"+{line_num}", file_path])
|
|
616
|
+
# Clear cache after editing
|
|
617
|
+
var_cache.pop(file_path, None)
|
|
618
|
+
finally:
|
|
619
|
+
# Cleanup temp files
|
|
620
|
+
for tmp_file in [getvar_cache_file, preview_script_file]:
|
|
621
|
+
if os.path.exists(tmp_file):
|
|
622
|
+
try:
|
|
623
|
+
os.unlink(tmp_file)
|
|
624
|
+
except OSError:
|
|
625
|
+
pass
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def fzf_repo_config(
|
|
630
|
+
repo: str,
|
|
631
|
+
defaults: Dict[str, str],
|
|
632
|
+
defaults_file: str,
|
|
633
|
+
) -> None:
|
|
634
|
+
"""Show config submenu for a single repo (standalone version for explore)."""
|
|
635
|
+
|
|
636
|
+
def get_display_name() -> Tuple[str, bool]:
|
|
637
|
+
"""Get display name and whether it's custom."""
|
|
638
|
+
display = repo_display_name(repo)
|
|
639
|
+
is_custom = False
|
|
640
|
+
try:
|
|
641
|
+
with open(defaults_file, "r") as f:
|
|
642
|
+
for line in f:
|
|
643
|
+
if line.startswith(f"display:{repo}="):
|
|
644
|
+
is_custom = True
|
|
645
|
+
break
|
|
646
|
+
except FileNotFoundError:
|
|
647
|
+
pass
|
|
648
|
+
return display, is_custom
|
|
649
|
+
|
|
650
|
+
def set_display_name() -> None:
|
|
651
|
+
"""Prompt to set custom display name."""
|
|
652
|
+
display, _ = get_display_name()
|
|
653
|
+
print(f"\nCurrent display name: {display}")
|
|
654
|
+
print(f"Repo path: {repo}")
|
|
655
|
+
new_name = input("Enter new display name (empty to reset to auto): ").strip()
|
|
656
|
+
|
|
657
|
+
key = f"display:{repo}"
|
|
658
|
+
if new_name:
|
|
659
|
+
save_default(defaults_file, key, new_name)
|
|
660
|
+
print(f"Set display name to: {new_name}")
|
|
661
|
+
else:
|
|
662
|
+
# Remove custom name
|
|
663
|
+
remove_default(defaults_file, key)
|
|
664
|
+
print(f"Reset to auto name: {repo_display_name(repo)}")
|
|
665
|
+
input("Press Enter to continue...")
|
|
666
|
+
|
|
667
|
+
def pick_update_default() -> None:
|
|
668
|
+
"""Show submenu to pick update default."""
|
|
669
|
+
display, _ = get_display_name()
|
|
670
|
+
current = defaults.get(repo, "rebase")
|
|
671
|
+
|
|
672
|
+
options = [
|
|
673
|
+
("rebase", "Rebase local commits on top of upstream"),
|
|
674
|
+
("merge", "Merge upstream into local branch"),
|
|
675
|
+
("skip", "Skip this repo during updates"),
|
|
676
|
+
]
|
|
677
|
+
|
|
678
|
+
menu_lines = []
|
|
679
|
+
for opt, desc in options:
|
|
680
|
+
marker = "●" if opt == current else "○"
|
|
681
|
+
menu_lines.append(f"{opt}\t{marker} {opt:<8} {desc}")
|
|
682
|
+
|
|
683
|
+
header = f"Update default for {display}"
|
|
684
|
+
|
|
685
|
+
try:
|
|
686
|
+
result = subprocess.run(
|
|
687
|
+
[
|
|
688
|
+
"fzf",
|
|
689
|
+
"--no-multi",
|
|
690
|
+
"--no-sort",
|
|
691
|
+
"--height", "~8",
|
|
692
|
+
"--header", header,
|
|
693
|
+
"--prompt", "Select: ",
|
|
694
|
+
"--with-nth", "2..",
|
|
695
|
+
"--delimiter", "\t",
|
|
696
|
+
"--bind", "esc:become(echo BACK)",
|
|
697
|
+
"--bind", "q:become(echo BACK)",
|
|
698
|
+
] + get_fzf_color_args(),
|
|
699
|
+
input="\n".join(menu_lines),
|
|
700
|
+
stdout=subprocess.PIPE,
|
|
701
|
+
text=True,
|
|
702
|
+
)
|
|
703
|
+
except FileNotFoundError:
|
|
704
|
+
return
|
|
705
|
+
|
|
706
|
+
if result.returncode != 0 or not result.stdout.strip():
|
|
707
|
+
return
|
|
708
|
+
|
|
709
|
+
output = result.stdout.strip()
|
|
710
|
+
if output == "BACK":
|
|
711
|
+
return
|
|
712
|
+
|
|
713
|
+
# Extract option from first field
|
|
714
|
+
selected = output.split("\t")[0]
|
|
715
|
+
if selected in ("rebase", "merge", "skip"):
|
|
716
|
+
save_default(defaults_file, repo, selected)
|
|
717
|
+
defaults[repo] = selected
|
|
718
|
+
|
|
719
|
+
def configure_push_target() -> None:
|
|
720
|
+
"""Configure push target for this repo."""
|
|
721
|
+
push_target = get_push_target(defaults, repo)
|
|
722
|
+
current_url = push_target.get("push_url", "") if push_target else ""
|
|
723
|
+
current_prefix = push_target.get("branch_prefix", "") if push_target else ""
|
|
724
|
+
|
|
725
|
+
print(f"\nPush target for {repo_display_name(repo)}")
|
|
726
|
+
print(f"Current push URL: {current_url or '(not configured)'}")
|
|
727
|
+
print(f"Current branch prefix: {current_prefix or '(none)'}")
|
|
728
|
+
print()
|
|
729
|
+
|
|
730
|
+
new_url = input("Push URL (empty to clear, - to keep): ").strip()
|
|
731
|
+
if new_url == "-":
|
|
732
|
+
new_url = current_url
|
|
733
|
+
elif not new_url:
|
|
734
|
+
# Clear the push target
|
|
735
|
+
remove_push_target(defaults_file, defaults, repo)
|
|
736
|
+
print("Push target cleared.")
|
|
737
|
+
input("Press Enter to continue...")
|
|
738
|
+
return
|
|
739
|
+
|
|
740
|
+
new_prefix = input("Branch prefix (e.g. 'yourname/', empty for none, - to keep): ").strip()
|
|
741
|
+
if new_prefix == "-":
|
|
742
|
+
new_prefix = current_prefix
|
|
743
|
+
|
|
744
|
+
set_push_target(defaults_file, defaults, repo, new_url, new_prefix)
|
|
745
|
+
print(f"Push target set: {new_url}")
|
|
746
|
+
if new_prefix:
|
|
747
|
+
print(f"Branch prefix: {new_prefix}")
|
|
748
|
+
input("Press Enter to continue...")
|
|
749
|
+
|
|
750
|
+
def edit_layer_conf() -> None:
|
|
751
|
+
"""Edit layer.conf for this repo."""
|
|
752
|
+
# Find layer.conf files in this repo
|
|
753
|
+
layer_confs = []
|
|
754
|
+
for root, dirs, files in os.walk(repo):
|
|
755
|
+
# Don't descend into .git
|
|
756
|
+
if ".git" in dirs:
|
|
757
|
+
dirs.remove(".git")
|
|
758
|
+
if "layer.conf" in files:
|
|
759
|
+
conf_path = os.path.join(root, "conf", "layer.conf")
|
|
760
|
+
if os.path.isfile(conf_path):
|
|
761
|
+
layer_confs.append(conf_path)
|
|
762
|
+
else:
|
|
763
|
+
# Check if it's directly in conf/
|
|
764
|
+
parent = os.path.dirname(root)
|
|
765
|
+
if os.path.basename(root) == "conf":
|
|
766
|
+
layer_confs.append(os.path.join(root, "layer.conf"))
|
|
767
|
+
|
|
768
|
+
if not layer_confs:
|
|
769
|
+
# Try standard location
|
|
770
|
+
for item in os.listdir(repo):
|
|
771
|
+
conf_path = os.path.join(repo, item, "conf", "layer.conf")
|
|
772
|
+
if os.path.isfile(conf_path):
|
|
773
|
+
layer_confs.append(conf_path)
|
|
774
|
+
|
|
775
|
+
if not layer_confs:
|
|
776
|
+
print(f"\nNo layer.conf found in {repo}")
|
|
777
|
+
input("Press Enter to continue...")
|
|
778
|
+
return
|
|
779
|
+
|
|
780
|
+
if len(layer_confs) == 1:
|
|
781
|
+
editor = os.environ.get("EDITOR", os.environ.get("VISUAL", "vi"))
|
|
782
|
+
subprocess.run([editor, layer_confs[0]])
|
|
783
|
+
else:
|
|
784
|
+
# Multiple - let user pick
|
|
785
|
+
menu = "\n".join(layer_confs)
|
|
786
|
+
try:
|
|
787
|
+
result = subprocess.run(
|
|
788
|
+
["fzf", "--height", "~10", "--header", "Select layer.conf to edit"] + get_fzf_color_args(),
|
|
789
|
+
input=menu,
|
|
790
|
+
stdout=subprocess.PIPE,
|
|
791
|
+
text=True,
|
|
792
|
+
)
|
|
793
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
794
|
+
editor = os.environ.get("EDITOR", os.environ.get("VISUAL", "vi"))
|
|
795
|
+
subprocess.run([editor, result.stdout.strip()])
|
|
796
|
+
except FileNotFoundError:
|
|
797
|
+
pass
|
|
798
|
+
|
|
799
|
+
while True:
|
|
800
|
+
display, is_custom = get_display_name()
|
|
801
|
+
update_default = defaults.get(repo, "rebase")
|
|
802
|
+
|
|
803
|
+
display_suffix = " (custom)" if is_custom else " (auto)"
|
|
804
|
+
push_target = get_push_target(defaults, repo)
|
|
805
|
+
push_status = push_target.get("push_url", "")[:40] if push_target else "(not configured)"
|
|
806
|
+
menu_lines = [
|
|
807
|
+
f"DISPLAY\tDisplay name {display}{display_suffix}",
|
|
808
|
+
f"DEFAULT\tUpdate default {update_default}",
|
|
809
|
+
f"PUSH\tPush target {push_status}",
|
|
810
|
+
f"EDIT\tEdit layer.conf →",
|
|
811
|
+
]
|
|
812
|
+
|
|
813
|
+
header = f"Configure {display} ({repo})"
|
|
814
|
+
|
|
815
|
+
try:
|
|
816
|
+
result = subprocess.run(
|
|
817
|
+
[
|
|
818
|
+
"fzf",
|
|
819
|
+
"--no-multi",
|
|
820
|
+
"--no-sort",
|
|
821
|
+
"--height", "~8",
|
|
822
|
+
"--header", header,
|
|
823
|
+
"--prompt", "Select: ",
|
|
824
|
+
"--with-nth", "2..",
|
|
825
|
+
"--delimiter", "\t",
|
|
826
|
+
"--bind", "esc:become(echo BACK)",
|
|
827
|
+
"--bind", "left:become(echo BACK)",
|
|
828
|
+
"--bind", "q:become(echo BACK)",
|
|
829
|
+
"--bind", "right:accept",
|
|
830
|
+
] + get_fzf_color_args(),
|
|
831
|
+
input="\n".join(menu_lines),
|
|
832
|
+
stdout=subprocess.PIPE,
|
|
833
|
+
text=True,
|
|
834
|
+
)
|
|
835
|
+
except FileNotFoundError:
|
|
836
|
+
return
|
|
837
|
+
|
|
838
|
+
if result.returncode != 0 or not result.stdout.strip():
|
|
839
|
+
return
|
|
840
|
+
|
|
841
|
+
output = result.stdout.strip()
|
|
842
|
+
|
|
843
|
+
if output == "BACK":
|
|
844
|
+
return
|
|
845
|
+
elif output.startswith("DISPLAY\t"):
|
|
846
|
+
set_display_name()
|
|
847
|
+
elif output.startswith("DEFAULT\t"):
|
|
848
|
+
pick_update_default()
|
|
849
|
+
elif output.startswith("PUSH\t"):
|
|
850
|
+
configure_push_target()
|
|
851
|
+
elif output.startswith("EDIT\t"):
|
|
852
|
+
edit_layer_conf()
|
|
853
|
+
|
|
854
|
+
|
|
855
|
+
|
|
856
|
+
def fzf_config_repos(
|
|
857
|
+
repos: List[str],
|
|
858
|
+
defaults: Dict[str, str],
|
|
859
|
+
bblayers_path: str,
|
|
860
|
+
defaults_file: str,
|
|
861
|
+
) -> int:
|
|
862
|
+
"""
|
|
863
|
+
Interactive fzf-based config interface.
|
|
864
|
+
Returns exit code.
|
|
865
|
+
"""
|
|
866
|
+
if not repos:
|
|
867
|
+
print("No repos found.")
|
|
868
|
+
return 1
|
|
869
|
+
|
|
870
|
+
def get_repo_info(repo: str) -> Tuple[str, bool]:
|
|
871
|
+
"""Get display name and whether it's custom."""
|
|
872
|
+
display = repo_display_name(repo)
|
|
873
|
+
is_custom = False
|
|
874
|
+
try:
|
|
875
|
+
custom = subprocess.check_output(
|
|
876
|
+
["git", "-C", repo, "config", "--get", "bit.display-name"],
|
|
877
|
+
stderr=subprocess.DEVNULL,
|
|
878
|
+
text=True,
|
|
879
|
+
).strip()
|
|
880
|
+
if custom:
|
|
881
|
+
is_custom = True
|
|
882
|
+
except subprocess.CalledProcessError:
|
|
883
|
+
pass
|
|
884
|
+
return display, is_custom
|
|
885
|
+
|
|
886
|
+
def build_menu_lines() -> str:
|
|
887
|
+
"""Build fzf menu input with header as last line (appears at top in fzf)."""
|
|
888
|
+
menu_lines = []
|
|
889
|
+
max_name_len = 20
|
|
890
|
+
|
|
891
|
+
# First pass to get max name length
|
|
892
|
+
for repo in repos:
|
|
893
|
+
display, _ = get_repo_info(repo)
|
|
894
|
+
if len(display) + 1 > max_name_len: # +1 for potential *
|
|
895
|
+
max_name_len = len(display) + 1
|
|
896
|
+
|
|
897
|
+
# Data lines in reverse order (so they display 1,2,3... from top in fzf)
|
|
898
|
+
data_lines = []
|
|
899
|
+
for idx, repo in enumerate(repos, start=1):
|
|
900
|
+
display, is_custom = get_repo_info(repo)
|
|
901
|
+
if is_custom:
|
|
902
|
+
display = f"{display}*"
|
|
903
|
+
update_default = defaults.get(repo, "rebase")
|
|
904
|
+
line = f"{repo}\t{idx:<4} {display:<{max_name_len}} {update_default:<10} {repo}"
|
|
905
|
+
data_lines.append(line)
|
|
906
|
+
|
|
907
|
+
# Add special "project" entry for project config (with separator)
|
|
908
|
+
bblayers_conf = resolve_bblayers_path(bblayers_path)
|
|
909
|
+
if bblayers_conf:
|
|
910
|
+
conf_dir = os.path.dirname(bblayers_conf)
|
|
911
|
+
# Add separator before project
|
|
912
|
+
data_lines.append(f"SEPARATOR\t") # Empty separator line
|
|
913
|
+
# Pad plain text first, then apply color (ANSI codes don't take visual space)
|
|
914
|
+
project_name = f"{'project':<{max_name_len}}"
|
|
915
|
+
build_line = f"PROJECT\t{'P':<4} {Colors.cyan(project_name)} {'—':<10} {conf_dir}"
|
|
916
|
+
data_lines.append(build_line)
|
|
917
|
+
|
|
918
|
+
# Add "bit" entry for tool settings
|
|
919
|
+
settings_name = f"{'bit':<{max_name_len}}"
|
|
920
|
+
settings_line = f"SETTINGS\t{'S':<4} {Colors.magenta(settings_name)} {'—':<10} configure options"
|
|
921
|
+
data_lines.append(settings_line)
|
|
922
|
+
|
|
923
|
+
# Reverse so item N is first in input (appears at bottom), item 1 is near end
|
|
924
|
+
menu_lines = list(reversed(data_lines))
|
|
925
|
+
|
|
926
|
+
# Column header as LAST line (appears at TOP in default fzf layout)
|
|
927
|
+
col_header = f"HEADER\t{'#':<4} {'Name':<{max_name_len}} {'Default':<10} Path"
|
|
928
|
+
# Separator line under header (second-to-last, appears just below header)
|
|
929
|
+
sep_len = 4 + 1 + max_name_len + 1 + 10 + 1 + 20 # rough width matching columns
|
|
930
|
+
separator = f"SEPARATOR\t{'─' * sep_len}"
|
|
931
|
+
menu_lines.append(separator)
|
|
932
|
+
menu_lines.append(col_header)
|
|
933
|
+
|
|
934
|
+
return "\n".join(menu_lines)
|
|
935
|
+
|
|
936
|
+
def set_display_name(repo: str) -> None:
|
|
937
|
+
"""Prompt and set display name for a repo."""
|
|
938
|
+
current, is_custom = get_repo_info(repo)
|
|
939
|
+
try:
|
|
940
|
+
if is_custom:
|
|
941
|
+
new_name = input(f"\nDisplay name [{current}] (empty to clear): ").strip()
|
|
942
|
+
else:
|
|
943
|
+
new_name = input(f"\nDisplay name [{current}]: ").strip()
|
|
944
|
+
except (EOFError, KeyboardInterrupt):
|
|
945
|
+
print("\n Cancelled.")
|
|
946
|
+
return
|
|
947
|
+
|
|
948
|
+
if new_name == "" and is_custom:
|
|
949
|
+
# Clear custom name
|
|
950
|
+
try:
|
|
951
|
+
subprocess.run(
|
|
952
|
+
["git", "-C", repo, "config", "--unset", "bit.display-name"],
|
|
953
|
+
check=True,
|
|
954
|
+
)
|
|
955
|
+
print(f" Cleared. Now using: {repo_display_name(repo)}")
|
|
956
|
+
except subprocess.CalledProcessError:
|
|
957
|
+
pass
|
|
958
|
+
elif new_name:
|
|
959
|
+
subprocess.run(
|
|
960
|
+
["git", "-C", repo, "config", "bit.display-name", new_name],
|
|
961
|
+
check=True,
|
|
962
|
+
)
|
|
963
|
+
print(f" Set to: {new_name}")
|
|
964
|
+
else:
|
|
965
|
+
print(" Unchanged.")
|
|
966
|
+
|
|
967
|
+
def set_update_default(repo: str, new_default: str) -> None:
|
|
968
|
+
"""Set update default for a repo."""
|
|
969
|
+
old_default = defaults.get(repo, "rebase")
|
|
970
|
+
if old_default == new_default:
|
|
971
|
+
display, _ = get_repo_info(repo)
|
|
972
|
+
print(f"\n {display}: already set to {new_default}")
|
|
973
|
+
return
|
|
974
|
+
defaults[repo] = new_default
|
|
975
|
+
save_defaults(defaults_file, defaults)
|
|
976
|
+
display, _ = get_repo_info(repo)
|
|
977
|
+
print(f"\n {display}: {old_default} → {new_default}")
|
|
978
|
+
|
|
979
|
+
def edit_layer_conf(repo: str) -> None:
|
|
980
|
+
"""Edit layer.conf for a repo."""
|
|
981
|
+
# Find layers in this repo
|
|
982
|
+
pairs, _repo_sets = resolve_base_and_layers(bblayers_path)
|
|
983
|
+
repo_layers = [layer for layer, r in pairs if r == repo]
|
|
984
|
+
|
|
985
|
+
if not repo_layers:
|
|
986
|
+
print(f"\n No layers found in {repo}")
|
|
987
|
+
return
|
|
988
|
+
|
|
989
|
+
if len(repo_layers) == 1:
|
|
990
|
+
layer = repo_layers[0]
|
|
991
|
+
else:
|
|
992
|
+
# Multiple layers - use fzf to pick
|
|
993
|
+
display_name, _ = get_repo_info(repo)
|
|
994
|
+
menu_lines = []
|
|
995
|
+
bindings = []
|
|
996
|
+
for i, lyr in enumerate(repo_layers, start=1):
|
|
997
|
+
# Format: "layer_path\t# layer_name"
|
|
998
|
+
menu_lines.append(f"{lyr}\t{i} {layer_display_name(lyr)}")
|
|
999
|
+
# Add number key binding (1-9)
|
|
1000
|
+
if i <= 9:
|
|
1001
|
+
bindings.extend(["--bind", f"{i}:become(echo {lyr})"])
|
|
1002
|
+
|
|
1003
|
+
try:
|
|
1004
|
+
result = subprocess.run(
|
|
1005
|
+
[
|
|
1006
|
+
"fzf",
|
|
1007
|
+
"--no-multi",
|
|
1008
|
+
"--no-sort",
|
|
1009
|
+
"--height", "~10",
|
|
1010
|
+
"--header", f"Select layer in {display_name} (←=back, 1-{min(len(repo_layers), 9)} or Enter)",
|
|
1011
|
+
"--prompt", "Layer: ",
|
|
1012
|
+
"--with-nth", "2..",
|
|
1013
|
+
"--delimiter", "\t",
|
|
1014
|
+
"--bind", "esc:become(echo BACK)",
|
|
1015
|
+
"--bind", "left:become(echo BACK)",
|
|
1016
|
+
"--bind", "q:become(echo BACK)",
|
|
1017
|
+
] + bindings + get_fzf_color_args(),
|
|
1018
|
+
input="\n".join(menu_lines),
|
|
1019
|
+
stdout=subprocess.PIPE,
|
|
1020
|
+
text=True,
|
|
1021
|
+
)
|
|
1022
|
+
except FileNotFoundError:
|
|
1023
|
+
print(" fzf not found")
|
|
1024
|
+
return
|
|
1025
|
+
|
|
1026
|
+
if result.returncode != 0 or not result.stdout.strip():
|
|
1027
|
+
return
|
|
1028
|
+
|
|
1029
|
+
output = result.stdout.strip()
|
|
1030
|
+
if output == "BACK":
|
|
1031
|
+
return
|
|
1032
|
+
# Extract layer path (either raw path from number key, or first field from selection)
|
|
1033
|
+
if "\t" in output:
|
|
1034
|
+
layer = output.split("\t")[0]
|
|
1035
|
+
else:
|
|
1036
|
+
layer = output
|
|
1037
|
+
|
|
1038
|
+
layer_conf = os.path.join(layer, "conf", "layer.conf")
|
|
1039
|
+
if not os.path.isfile(layer_conf):
|
|
1040
|
+
print(f"\n layer.conf not found: {layer_conf}")
|
|
1041
|
+
return
|
|
1042
|
+
|
|
1043
|
+
editor = os.environ.get("EDITOR", os.environ.get("VISUAL", "vi"))
|
|
1044
|
+
print(f"\n Editing: {layer_conf}")
|
|
1045
|
+
try:
|
|
1046
|
+
subprocess.run([editor, layer_conf], check=True)
|
|
1047
|
+
except (subprocess.CalledProcessError, FileNotFoundError) as e:
|
|
1048
|
+
print(f" Error: {e}")
|
|
1049
|
+
|
|
1050
|
+
def show_repo_submenu(repo: str) -> None:
|
|
1051
|
+
"""Show config submenu for a single repo."""
|
|
1052
|
+
while True:
|
|
1053
|
+
display, is_custom = get_repo_info(repo)
|
|
1054
|
+
update_default = defaults.get(repo, "rebase")
|
|
1055
|
+
|
|
1056
|
+
# Build menu showing current values
|
|
1057
|
+
display_suffix = " (custom)" if is_custom else " (auto)"
|
|
1058
|
+
push_target = get_push_target(defaults, repo)
|
|
1059
|
+
push_status = push_target.get("push_url", "")[:40] if push_target else "(not configured)"
|
|
1060
|
+
menu_lines = [
|
|
1061
|
+
f"DISPLAY\tDisplay name {display}{display_suffix}",
|
|
1062
|
+
f"DEFAULT\tUpdate default {update_default}",
|
|
1063
|
+
f"PUSH\tPush target {push_status}",
|
|
1064
|
+
f"EDIT\tEdit layer.conf →",
|
|
1065
|
+
]
|
|
1066
|
+
|
|
1067
|
+
header = f"Configure {display} ({repo})"
|
|
1068
|
+
|
|
1069
|
+
try:
|
|
1070
|
+
result = subprocess.run(
|
|
1071
|
+
[
|
|
1072
|
+
"fzf",
|
|
1073
|
+
"--no-multi",
|
|
1074
|
+
"--no-sort",
|
|
1075
|
+
"--height", "~8",
|
|
1076
|
+
"--header", header,
|
|
1077
|
+
"--prompt", "Select: ",
|
|
1078
|
+
"--with-nth", "2..",
|
|
1079
|
+
"--delimiter", "\t",
|
|
1080
|
+
"--bind", "esc:become(echo BACK)",
|
|
1081
|
+
"--bind", "left:become(echo BACK)",
|
|
1082
|
+
"--bind", "q:become(echo BACK)",
|
|
1083
|
+
"--bind", "right:accept",
|
|
1084
|
+
] + get_fzf_color_args(),
|
|
1085
|
+
input="\n".join(menu_lines),
|
|
1086
|
+
stdout=subprocess.PIPE,
|
|
1087
|
+
text=True,
|
|
1088
|
+
)
|
|
1089
|
+
except FileNotFoundError:
|
|
1090
|
+
return
|
|
1091
|
+
|
|
1092
|
+
if result.returncode != 0 or not result.stdout.strip():
|
|
1093
|
+
return
|
|
1094
|
+
|
|
1095
|
+
output = result.stdout.strip()
|
|
1096
|
+
|
|
1097
|
+
if output == "BACK":
|
|
1098
|
+
return
|
|
1099
|
+
elif output.startswith("DISPLAY\t"):
|
|
1100
|
+
set_display_name(repo)
|
|
1101
|
+
elif output.startswith("DEFAULT\t"):
|
|
1102
|
+
# Show default picker submenu
|
|
1103
|
+
pick_update_default(repo)
|
|
1104
|
+
elif output.startswith("EDIT\t"):
|
|
1105
|
+
edit_layer_conf(repo)
|
|
1106
|
+
elif output.startswith("PUSH\t"):
|
|
1107
|
+
configure_push_target(repo)
|
|
1108
|
+
|
|
1109
|
+
def configure_push_target(repo: str) -> None:
|
|
1110
|
+
"""Configure push target for a repo."""
|
|
1111
|
+
display, _ = get_repo_info(repo)
|
|
1112
|
+
push_target = get_push_target(defaults, repo)
|
|
1113
|
+
current_url = push_target.get("push_url", "") if push_target else ""
|
|
1114
|
+
current_prefix = push_target.get("branch_prefix", "") if push_target else ""
|
|
1115
|
+
|
|
1116
|
+
print(f"\nPush target for {display}")
|
|
1117
|
+
print(f"Current push URL: {current_url or '(not configured)'}")
|
|
1118
|
+
print(f"Current branch prefix: {current_prefix or '(none)'}")
|
|
1119
|
+
print()
|
|
1120
|
+
|
|
1121
|
+
new_url = input("Push URL (empty to clear, - to keep): ").strip()
|
|
1122
|
+
if new_url == "-":
|
|
1123
|
+
new_url = current_url
|
|
1124
|
+
elif not new_url:
|
|
1125
|
+
# Clear the push target
|
|
1126
|
+
remove_push_target(defaults_file, defaults, repo)
|
|
1127
|
+
print("Push target cleared.")
|
|
1128
|
+
input("Press Enter to continue...")
|
|
1129
|
+
return
|
|
1130
|
+
|
|
1131
|
+
new_prefix = input("Branch prefix (e.g. 'yourname/', empty for none, - to keep): ").strip()
|
|
1132
|
+
if new_prefix == "-":
|
|
1133
|
+
new_prefix = current_prefix
|
|
1134
|
+
|
|
1135
|
+
set_push_target(defaults_file, defaults, repo, new_url, new_prefix)
|
|
1136
|
+
print(f"Push target set: {new_url}")
|
|
1137
|
+
if new_prefix:
|
|
1138
|
+
print(f"Branch prefix: {new_prefix}")
|
|
1139
|
+
input("Press Enter to continue...")
|
|
1140
|
+
|
|
1141
|
+
def show_build_config() -> None:
|
|
1142
|
+
"""Show config submenu for project conf files."""
|
|
1143
|
+
fzf_build_config(bblayers_path)
|
|
1144
|
+
|
|
1145
|
+
def show_settings_submenu() -> None:
|
|
1146
|
+
"""Show settings submenu for bit tool configuration."""
|
|
1147
|
+
while True:
|
|
1148
|
+
# Theme summary
|
|
1149
|
+
current_theme = get_current_theme_name(defaults_file)
|
|
1150
|
+
custom_colors = get_custom_colors(defaults_file)
|
|
1151
|
+
custom_count = len(custom_colors)
|
|
1152
|
+
theme_summary = current_theme
|
|
1153
|
+
if custom_count:
|
|
1154
|
+
theme_summary += f" +{custom_count} custom"
|
|
1155
|
+
|
|
1156
|
+
# Directory browser info
|
|
1157
|
+
browser = get_directory_browser()
|
|
1158
|
+
browser_desc = {
|
|
1159
|
+
"auto": "auto-detect",
|
|
1160
|
+
"broot": "broot",
|
|
1161
|
+
"ranger": "ranger",
|
|
1162
|
+
"nnn": "nnn",
|
|
1163
|
+
"fzf": "fzf built-in"
|
|
1164
|
+
}.get(browser, browser)
|
|
1165
|
+
|
|
1166
|
+
# Git viewer info
|
|
1167
|
+
git_viewer = get_git_viewer()
|
|
1168
|
+
git_viewer_desc = {
|
|
1169
|
+
"auto": "auto-detect",
|
|
1170
|
+
"tig": "tig",
|
|
1171
|
+
"lazygit": "lazygit",
|
|
1172
|
+
"gitk": "gitk",
|
|
1173
|
+
}.get(git_viewer, git_viewer)
|
|
1174
|
+
|
|
1175
|
+
# Preview layout info
|
|
1176
|
+
preview_layout = get_preview_layout()
|
|
1177
|
+
preview_layout_desc = {
|
|
1178
|
+
"down": "bottom",
|
|
1179
|
+
"right": "side-by-side",
|
|
1180
|
+
"up": "top",
|
|
1181
|
+
}.get(preview_layout, preview_layout)
|
|
1182
|
+
|
|
1183
|
+
# Recipe scan method info
|
|
1184
|
+
use_bitbake_layers = get_recipe_use_bitbake_layers()
|
|
1185
|
+
recipe_scan_desc = "bitbake-layers" if use_bitbake_layers else "file scan"
|
|
1186
|
+
|
|
1187
|
+
menu_lines = [
|
|
1188
|
+
f"COLORS\tColors {theme_summary}",
|
|
1189
|
+
f"BROWSER\tDir Browser {browser_desc}",
|
|
1190
|
+
f"GITVIEWER\tGit Viewer {git_viewer_desc}",
|
|
1191
|
+
f"PREVIEW\tPreview Layout {preview_layout_desc}",
|
|
1192
|
+
f"RECIPE\tRecipe Scan {recipe_scan_desc}",
|
|
1193
|
+
]
|
|
1194
|
+
|
|
1195
|
+
try:
|
|
1196
|
+
result = subprocess.run(
|
|
1197
|
+
[
|
|
1198
|
+
"fzf",
|
|
1199
|
+
"--no-multi",
|
|
1200
|
+
"--no-sort",
|
|
1201
|
+
"--height", "~10",
|
|
1202
|
+
"--header", "bit Settings (←/q=back)",
|
|
1203
|
+
"--prompt", "Setting: ",
|
|
1204
|
+
"--with-nth", "2..",
|
|
1205
|
+
"--delimiter", "\t",
|
|
1206
|
+
"--bind", "esc:become(echo BACK)",
|
|
1207
|
+
"--bind", "left:become(echo BACK)",
|
|
1208
|
+
"--bind", "q:become(echo BACK)",
|
|
1209
|
+
"--bind", "right:accept",
|
|
1210
|
+
] + get_fzf_color_args(),
|
|
1211
|
+
input="\n".join(menu_lines),
|
|
1212
|
+
stdout=subprocess.PIPE,
|
|
1213
|
+
text=True,
|
|
1214
|
+
)
|
|
1215
|
+
except FileNotFoundError:
|
|
1216
|
+
return
|
|
1217
|
+
|
|
1218
|
+
if result.returncode != 0 or not result.stdout.strip():
|
|
1219
|
+
return
|
|
1220
|
+
|
|
1221
|
+
output = result.stdout.strip()
|
|
1222
|
+
|
|
1223
|
+
if output == "BACK":
|
|
1224
|
+
return
|
|
1225
|
+
elif output.startswith("COLORS\t"):
|
|
1226
|
+
show_colors_submenu()
|
|
1227
|
+
elif output.startswith("BROWSER\t"):
|
|
1228
|
+
_pick_directory_browser()
|
|
1229
|
+
elif output.startswith("GITVIEWER\t"):
|
|
1230
|
+
_pick_git_viewer()
|
|
1231
|
+
elif output.startswith("PREVIEW\t"):
|
|
1232
|
+
_pick_preview_layout()
|
|
1233
|
+
elif output.startswith("RECIPE\t"):
|
|
1234
|
+
_pick_recipe_use_bitbake_layers()
|
|
1235
|
+
|
|
1236
|
+
def show_colors_submenu() -> None:
|
|
1237
|
+
"""Show colors/theme submenu."""
|
|
1238
|
+
while True:
|
|
1239
|
+
current_theme = get_current_theme_name(defaults_file)
|
|
1240
|
+
theme_desc = FZF_THEMES.get(current_theme, ("", ""))[1]
|
|
1241
|
+
custom_colors = get_custom_colors(defaults_file)
|
|
1242
|
+
custom_count = len(custom_colors)
|
|
1243
|
+
custom_desc = f"{custom_count} override{'s' if custom_count != 1 else ''}" if custom_count else "none"
|
|
1244
|
+
|
|
1245
|
+
# Terminal colors summary
|
|
1246
|
+
terminal_overrides = sum(
|
|
1247
|
+
1 for elem in TERMINAL_COLOR_ELEMENTS
|
|
1248
|
+
if get_terminal_color(elem, defaults_file) != TERMINAL_COLOR_ELEMENTS[elem][0]
|
|
1249
|
+
)
|
|
1250
|
+
terminal_desc = f"{terminal_overrides} override{'s' if terminal_overrides != 1 else ''}" if terminal_overrides else "defaults"
|
|
1251
|
+
|
|
1252
|
+
menu_lines = [
|
|
1253
|
+
f"THEME\tTheme {current_theme} - {theme_desc}",
|
|
1254
|
+
f"CUSTOM\tIndividual {custom_desc}",
|
|
1255
|
+
f"TERMINAL\tTerminal {terminal_desc}",
|
|
1256
|
+
]
|
|
1257
|
+
|
|
1258
|
+
try:
|
|
1259
|
+
result = subprocess.run(
|
|
1260
|
+
[
|
|
1261
|
+
"fzf",
|
|
1262
|
+
"--no-multi",
|
|
1263
|
+
"--no-sort",
|
|
1264
|
+
"--height", "~10",
|
|
1265
|
+
"--header", "Colors (←/q=back)",
|
|
1266
|
+
"--prompt", "Option: ",
|
|
1267
|
+
"--with-nth", "2..",
|
|
1268
|
+
"--delimiter", "\t",
|
|
1269
|
+
"--bind", "esc:become(echo BACK)",
|
|
1270
|
+
"--bind", "left:become(echo BACK)",
|
|
1271
|
+
"--bind", "q:become(echo BACK)",
|
|
1272
|
+
"--bind", "right:accept",
|
|
1273
|
+
] + get_fzf_color_args(),
|
|
1274
|
+
input="\n".join(menu_lines),
|
|
1275
|
+
stdout=subprocess.PIPE,
|
|
1276
|
+
text=True,
|
|
1277
|
+
)
|
|
1278
|
+
except FileNotFoundError:
|
|
1279
|
+
return
|
|
1280
|
+
|
|
1281
|
+
if result.returncode != 0 or not result.stdout.strip():
|
|
1282
|
+
return
|
|
1283
|
+
|
|
1284
|
+
output = result.stdout.strip()
|
|
1285
|
+
|
|
1286
|
+
if output == "BACK":
|
|
1287
|
+
return
|
|
1288
|
+
elif output.startswith("THEME\t"):
|
|
1289
|
+
fzf_theme_picker(defaults_file)
|
|
1290
|
+
elif output.startswith("CUSTOM\t"):
|
|
1291
|
+
fzf_custom_color_menu(defaults_file)
|
|
1292
|
+
elif output.startswith("TERMINAL\t"):
|
|
1293
|
+
show_terminal_colors_submenu()
|
|
1294
|
+
|
|
1295
|
+
def show_terminal_colors_submenu() -> None:
|
|
1296
|
+
"""Show terminal output colors submenu."""
|
|
1297
|
+
while True:
|
|
1298
|
+
menu_lines = []
|
|
1299
|
+
for elem, (default_color, desc) in TERMINAL_COLOR_ELEMENTS.items():
|
|
1300
|
+
current = get_terminal_color(elem, defaults_file)
|
|
1301
|
+
is_default = (current == default_color)
|
|
1302
|
+
marker = " " if is_default else "● "
|
|
1303
|
+
# Show color sample
|
|
1304
|
+
ansi_code = ANSI_COLORS.get(current, "\033[33m")
|
|
1305
|
+
sample = f"{ansi_code}■■■\033[0m"
|
|
1306
|
+
status = f"{current}" if not is_default else f"{current} (default)"
|
|
1307
|
+
menu_lines.append(f"{elem}\t{marker}{sample} {desc:<35} {status}")
|
|
1308
|
+
|
|
1309
|
+
try:
|
|
1310
|
+
result = subprocess.run(
|
|
1311
|
+
[
|
|
1312
|
+
"fzf",
|
|
1313
|
+
"--no-multi",
|
|
1314
|
+
"--no-sort",
|
|
1315
|
+
"--ansi",
|
|
1316
|
+
"--height", "~15",
|
|
1317
|
+
"--header", "Terminal Colors (←/q=back)\nThese colors are used in status output, not fzf menus",
|
|
1318
|
+
"--prompt", "Element: ",
|
|
1319
|
+
"--with-nth", "2..",
|
|
1320
|
+
"--delimiter", "\t",
|
|
1321
|
+
"--bind", "esc:become(echo BACK)",
|
|
1322
|
+
"--bind", "left:become(echo BACK)",
|
|
1323
|
+
"--bind", "q:become(echo BACK)",
|
|
1324
|
+
"--bind", "right:accept",
|
|
1325
|
+
] + get_fzf_color_args(),
|
|
1326
|
+
input="\n".join(menu_lines),
|
|
1327
|
+
stdout=subprocess.PIPE,
|
|
1328
|
+
text=True,
|
|
1329
|
+
)
|
|
1330
|
+
except FileNotFoundError:
|
|
1331
|
+
return
|
|
1332
|
+
|
|
1333
|
+
if result.returncode != 0 or not result.stdout.strip():
|
|
1334
|
+
return
|
|
1335
|
+
|
|
1336
|
+
output = result.stdout.strip()
|
|
1337
|
+
|
|
1338
|
+
if output == "BACK":
|
|
1339
|
+
return
|
|
1340
|
+
|
|
1341
|
+
# Extract element name
|
|
1342
|
+
elem = output.split("\t")[0] if "\t" in output else output
|
|
1343
|
+
if elem in TERMINAL_COLOR_ELEMENTS:
|
|
1344
|
+
pick_terminal_color(elem)
|
|
1345
|
+
|
|
1346
|
+
def pick_terminal_color(element: str) -> None:
|
|
1347
|
+
"""Pick a color for a terminal output element."""
|
|
1348
|
+
default_color = TERMINAL_COLOR_ELEMENTS[element][0]
|
|
1349
|
+
current = get_terminal_color(element, defaults_file)
|
|
1350
|
+
desc = TERMINAL_COLOR_ELEMENTS[element][1]
|
|
1351
|
+
|
|
1352
|
+
# Build color options
|
|
1353
|
+
menu_lines = []
|
|
1354
|
+
# Add default option first
|
|
1355
|
+
default_marker = "● " if current == default_color else " "
|
|
1356
|
+
default_ansi = ANSI_COLORS.get(default_color, "\033[33m")
|
|
1357
|
+
menu_lines.append(f"(default)\t{default_marker}{default_ansi}■■■\033[0m (default: {default_color})")
|
|
1358
|
+
|
|
1359
|
+
for name, ansi in sorted(ANSI_COLORS.items()):
|
|
1360
|
+
if name == default_color:
|
|
1361
|
+
continue # Already shown as default
|
|
1362
|
+
marker = "● " if name == current else " "
|
|
1363
|
+
menu_lines.append(f"{name}\t{marker}{ansi}■■■\033[0m {name}")
|
|
1364
|
+
|
|
1365
|
+
try:
|
|
1366
|
+
result = subprocess.run(
|
|
1367
|
+
[
|
|
1368
|
+
"fzf",
|
|
1369
|
+
"--no-multi",
|
|
1370
|
+
"--no-sort",
|
|
1371
|
+
"--ansi",
|
|
1372
|
+
"--height", "~20",
|
|
1373
|
+
"--header", f"Pick color for: {desc} (←/q=back)",
|
|
1374
|
+
"--prompt", "Color: ",
|
|
1375
|
+
"--with-nth", "2..",
|
|
1376
|
+
"--delimiter", "\t",
|
|
1377
|
+
"--bind", "esc:become(echo BACK)",
|
|
1378
|
+
"--bind", "left:become(echo BACK)",
|
|
1379
|
+
"--bind", "q:become(echo BACK)",
|
|
1380
|
+
] + get_fzf_color_args(),
|
|
1381
|
+
input="\n".join(menu_lines),
|
|
1382
|
+
stdout=subprocess.PIPE,
|
|
1383
|
+
text=True,
|
|
1384
|
+
)
|
|
1385
|
+
except FileNotFoundError:
|
|
1386
|
+
return
|
|
1387
|
+
|
|
1388
|
+
if result.returncode != 0 or not result.stdout.strip():
|
|
1389
|
+
return
|
|
1390
|
+
|
|
1391
|
+
output = result.stdout.strip()
|
|
1392
|
+
if output == "BACK":
|
|
1393
|
+
return
|
|
1394
|
+
|
|
1395
|
+
selected = output.split("\t")[0] if "\t" in output else output
|
|
1396
|
+
if selected == "(default)":
|
|
1397
|
+
set_terminal_color(element, "(default)", defaults_file)
|
|
1398
|
+
elif selected in ANSI_COLORS:
|
|
1399
|
+
set_terminal_color(element, selected, defaults_file)
|
|
1400
|
+
|
|
1401
|
+
def pick_update_default(repo: str) -> None:
|
|
1402
|
+
"""Show submenu to pick update default."""
|
|
1403
|
+
display, _ = get_repo_info(repo)
|
|
1404
|
+
current = defaults.get(repo, "rebase")
|
|
1405
|
+
|
|
1406
|
+
menu_lines = []
|
|
1407
|
+
for opt in ["rebase", "merge", "skip"]:
|
|
1408
|
+
marker = "●" if opt == current else "○"
|
|
1409
|
+
menu_lines.append(f"{opt}\t{marker} {opt}")
|
|
1410
|
+
|
|
1411
|
+
try:
|
|
1412
|
+
result = subprocess.run(
|
|
1413
|
+
[
|
|
1414
|
+
"fzf",
|
|
1415
|
+
"--no-multi",
|
|
1416
|
+
"--no-sort",
|
|
1417
|
+
"--height", "~6",
|
|
1418
|
+
"--header", f"Update default for {display}",
|
|
1419
|
+
"--prompt", "Default: ",
|
|
1420
|
+
"--with-nth", "2..",
|
|
1421
|
+
"--delimiter", "\t",
|
|
1422
|
+
"--bind", "r:become(echo rebase)",
|
|
1423
|
+
"--bind", "m:become(echo merge)",
|
|
1424
|
+
"--bind", "s:become(echo skip)",
|
|
1425
|
+
] + get_fzf_color_args(),
|
|
1426
|
+
input="\n".join(menu_lines),
|
|
1427
|
+
stdout=subprocess.PIPE,
|
|
1428
|
+
text=True,
|
|
1429
|
+
)
|
|
1430
|
+
except FileNotFoundError:
|
|
1431
|
+
return
|
|
1432
|
+
|
|
1433
|
+
if result.returncode != 0 or not result.stdout.strip():
|
|
1434
|
+
return
|
|
1435
|
+
|
|
1436
|
+
output = result.stdout.strip()
|
|
1437
|
+
# Handle both direct key (rebase) and selection (rebase\t● rebase)
|
|
1438
|
+
new_default = output.split("\t")[0]
|
|
1439
|
+
if new_default in ("rebase", "merge", "skip"):
|
|
1440
|
+
set_update_default(repo, new_default)
|
|
1441
|
+
|
|
1442
|
+
header = "Enter/→=configure | d=display | r=rebase | m=merge | s=skip | e=edit | q=quit"
|
|
1443
|
+
|
|
1444
|
+
while True:
|
|
1445
|
+
menu_input = build_menu_lines()
|
|
1446
|
+
|
|
1447
|
+
try:
|
|
1448
|
+
result = subprocess.run(
|
|
1449
|
+
[
|
|
1450
|
+
"fzf",
|
|
1451
|
+
"--no-multi",
|
|
1452
|
+
"--no-sort",
|
|
1453
|
+
"--no-info",
|
|
1454
|
+
"--ansi",
|
|
1455
|
+
"--height", "~50%",
|
|
1456
|
+
"--header", header,
|
|
1457
|
+
"--prompt", "Config: ",
|
|
1458
|
+
"--with-nth", "2..", # Hide repo path / HEADER marker (field 1)
|
|
1459
|
+
"--delimiter", "\t",
|
|
1460
|
+
"--bind", "q:become(echo QUIT)",
|
|
1461
|
+
"--bind", "d:become(echo DISPLAY {1})",
|
|
1462
|
+
"--bind", "r:become(echo REBASE {1})",
|
|
1463
|
+
"--bind", "m:become(echo MERGE {1})",
|
|
1464
|
+
"--bind", "s:become(echo SKIP {1})",
|
|
1465
|
+
"--bind", "e:become(echo EDIT {1})",
|
|
1466
|
+
"--bind", "right:accept",
|
|
1467
|
+
] + get_fzf_color_args(),
|
|
1468
|
+
input=menu_input,
|
|
1469
|
+
stdout=subprocess.PIPE,
|
|
1470
|
+
text=True,
|
|
1471
|
+
)
|
|
1472
|
+
except FileNotFoundError:
|
|
1473
|
+
print("fzf not found. Use CLI: bit config <repo> --display-name/--update-default")
|
|
1474
|
+
return 1
|
|
1475
|
+
|
|
1476
|
+
if result.returncode != 0 or not result.stdout.strip():
|
|
1477
|
+
break
|
|
1478
|
+
|
|
1479
|
+
output = result.stdout.strip()
|
|
1480
|
+
|
|
1481
|
+
if output == "QUIT":
|
|
1482
|
+
break
|
|
1483
|
+
elif output.startswith("DISPLAY "):
|
|
1484
|
+
repo_path = output[8:].strip()
|
|
1485
|
+
if repo_path not in ("HEADER", "SEPARATOR"):
|
|
1486
|
+
set_display_name(repo_path)
|
|
1487
|
+
elif output.startswith("REBASE "):
|
|
1488
|
+
repo_path = output[7:].strip()
|
|
1489
|
+
if repo_path not in ("HEADER", "SEPARATOR"):
|
|
1490
|
+
set_update_default(repo_path, "rebase")
|
|
1491
|
+
elif output.startswith("MERGE "):
|
|
1492
|
+
repo_path = output[6:].strip()
|
|
1493
|
+
if repo_path not in ("HEADER", "SEPARATOR"):
|
|
1494
|
+
set_update_default(repo_path, "merge")
|
|
1495
|
+
elif output.startswith("SKIP "):
|
|
1496
|
+
repo_path = output[5:].strip()
|
|
1497
|
+
if repo_path not in ("HEADER", "SEPARATOR"):
|
|
1498
|
+
set_update_default(repo_path, "skip")
|
|
1499
|
+
elif output.startswith("EDIT "):
|
|
1500
|
+
repo_path = output[5:].strip()
|
|
1501
|
+
if repo_path not in ("HEADER", "SEPARATOR"):
|
|
1502
|
+
edit_layer_conf(repo_path)
|
|
1503
|
+
elif "\t" in output:
|
|
1504
|
+
# Enter was pressed - drill into submenu (ignore header/separator lines)
|
|
1505
|
+
repo_path = output.split("\t")[0]
|
|
1506
|
+
if repo_path == "PROJECT":
|
|
1507
|
+
show_build_config()
|
|
1508
|
+
elif repo_path == "SETTINGS":
|
|
1509
|
+
show_settings_submenu()
|
|
1510
|
+
elif repo_path not in ("HEADER", "SEPARATOR"):
|
|
1511
|
+
show_repo_submenu(repo_path)
|
|
1512
|
+
|
|
1513
|
+
return 0
|
|
1514
|
+
|
|
1515
|
+
|