Загрузочное меню ISO было оформлено в стиле Virtuality
- графическая тема GRUB (фон, подсветка выбранного пункта, полоса автозапуска) в image/grub, подключается в build-iso.sh; при отсутствии модулей GRUB меню остаётся текстовым; - пункты меню переведены: «Установить Virtuality X.Y.Z», «Загрузить с другого диска», «Настройки UEFI», «Проверить память»; - tests/render_html.mjs рендерит HTML в PNG (для фона темы). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019LEUXMcdZo7b87mjkUsPgg
@@ -216,20 +216,41 @@ import sys
|
|||||||
path, unattended, version = sys.argv[1], sys.argv[2] == "1", sys.argv[3]
|
path, unattended, version = sys.argv[1], sys.argv[2] == "1", sys.argv[3]
|
||||||
text = open(path).read()
|
text = open(path).read()
|
||||||
# /autoinstall.yaml applies to every entry, so all Ubuntu entries become Virtuality entries.
|
# /autoinstall.yaml applies to every entry, so all Ubuntu entries become Virtuality entries.
|
||||||
suffix = " (UNATTENDED: erases disk)" if unattended else ""
|
suffix = " — автоматически, диск будет стёрт" if unattended else ""
|
||||||
text = text.replace('menuentry "Try or Install Ubuntu Server"', 'menuentry "Install Virtuality %s%s"' % (version, suffix), 1)
|
text = text.replace('menuentry "Try or Install Ubuntu Server"', 'menuentry "Установить Virtuality %s%s"' % (version, suffix), 1)
|
||||||
text = text.replace('menuentry "Ubuntu Server with the HWE kernel"', 'menuentry "Install Virtuality %s with the HWE kernel%s"' % (version, suffix), 1)
|
text = text.replace('menuentry "Ubuntu Server with the HWE kernel"', 'menuentry "Установить Virtuality %s (новое ядро HWE)%s"' % (version, suffix), 1)
|
||||||
if "Install Virtuality" not in text:
|
text = text.replace("menuentry 'Boot from next volume'", "menuentry 'Загрузить с другого диска'", 1)
|
||||||
|
text = text.replace("menuentry 'UEFI Firmware Settings'", "menuentry 'Настройки UEFI'", 1)
|
||||||
|
text = text.replace("menuentry 'Test memory'", "menuentry 'Проверить память'", 1)
|
||||||
|
if "Установить Virtuality" not in text:
|
||||||
raise SystemExit("unexpected grub.cfg menu titles")
|
raise SystemExit("unexpected grub.cfg menu titles")
|
||||||
if unattended:
|
if unattended:
|
||||||
text = re.sub(r"^(\s*linux\s+\S+\s+)---", r"\1autoinstall ---", text, flags=re.M)
|
text = re.sub(r"^(\s*linux\s+\S+\s+)---", r"\1autoinstall ---", text, flags=re.M)
|
||||||
text = re.sub(r"^set timeout=\d+", "set default=0\nset timeout=10", text, count=1, flags=re.M)
|
text = re.sub(r"^set timeout=\d+\n", "", text, count=1, flags=re.M)
|
||||||
open(path, "w").write(text)
|
text = text.replace("loadfont unicode\n", "", 1)
|
||||||
|
# Branded graphical menu; if a module or the font is missing GRUB falls back to the plain text menu.
|
||||||
|
theme = """insmod all_video
|
||||||
|
insmod gfxterm
|
||||||
|
insmod png
|
||||||
|
insmod gfxmenu
|
||||||
|
set gfxmode=1024x768,auto
|
||||||
|
if loadfont /boot/grub/fonts/unicode.pf2; then
|
||||||
|
terminal_output gfxterm
|
||||||
|
set theme=/boot/grub/themes/virtuality/theme.txt
|
||||||
|
export theme
|
||||||
|
fi
|
||||||
|
set default=0
|
||||||
|
set timeout=10
|
||||||
|
"""
|
||||||
|
open(path, "w").write(theme + text)
|
||||||
PYEOF
|
PYEOF
|
||||||
new_md5="$(md5sum "${WORK_DIR}/grub.cfg" | cut -d' ' -f1)"
|
new_md5="$(md5sum "${WORK_DIR}/grub.cfg" | cut -d' ' -f1)"
|
||||||
sed -i "s|^[0-9a-f]\{32\} \./boot/grub/grub.cfg$|${new_md5} ./boot/grub/grub.cfg|" "${WORK_DIR}/md5sum.txt"
|
sed -i "s|^[0-9a-f]\{32\} \./boot/grub/grub.cfg$|${new_md5} ./boot/grub/grub.cfg|" "${WORK_DIR}/md5sum.txt"
|
||||||
|
|
||||||
# ---------------------------------------------------------- repack
|
# ---------------------------------------------------------- repack
|
||||||
|
THEME_DIR="${WORK_DIR}/theme"
|
||||||
|
mkdir -p "$THEME_DIR"
|
||||||
|
cp "${IMAGE_DIR}"/grub/theme.txt "${IMAGE_DIR}"/grub/*.png "$THEME_DIR"/
|
||||||
say "Writing ${OUTPUT_ISO}"
|
say "Writing ${OUTPUT_ISO}"
|
||||||
rm -f "$OUTPUT_ISO"
|
rm -f "$OUTPUT_ISO"
|
||||||
chmod -R a+rX,go-w "$PAYLOAD"
|
chmod -R a+rX,go-w "$PAYLOAD"
|
||||||
@@ -238,6 +259,7 @@ if ! xorriso -indev "$UBUNTU_ISO" -outdev "$OUTPUT_ISO" \
|
|||||||
-map "$PAYLOAD" /virtuality \
|
-map "$PAYLOAD" /virtuality \
|
||||||
-map "${WORK_DIR}/autoinstall.yaml" /autoinstall.yaml \
|
-map "${WORK_DIR}/autoinstall.yaml" /autoinstall.yaml \
|
||||||
-map "${WORK_DIR}/grub.cfg" /boot/grub/grub.cfg \
|
-map "${WORK_DIR}/grub.cfg" /boot/grub/grub.cfg \
|
||||||
|
-map "${THEME_DIR}" /boot/grub/themes/virtuality \
|
||||||
-map "${WORK_DIR}/md5sum.txt" /md5sum.txt \
|
-map "${WORK_DIR}/md5sum.txt" /md5sum.txt \
|
||||||
-boot_image any replay > "${WORK_DIR}/xorriso.log" 2>&1; then
|
-boot_image any replay > "${WORK_DIR}/xorriso.log" 2>&1; then
|
||||||
tail -n 30 "${WORK_DIR}/xorriso.log" >&2
|
tail -n 30 "${WORK_DIR}/xorriso.log" >&2
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# Тема загрузочного меню
|
||||||
|
|
||||||
|
`theme.txt` — тема GRUB (gfxmenu) для установочного ISO, `background.png` — фон 1024×768,
|
||||||
|
`select_*.png` — подсветка выбранного пункта.
|
||||||
|
|
||||||
|
Фон рендерится из `background.html` (тот же стиль, что у страницы входа панели):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
NODE_PATH=$(npm root -g) node tests/render_html.mjs image/grub/background.html image/grub/background.png 1024 768
|
||||||
|
```
|
||||||
|
|
||||||
|
Пиксмапы подсветки — однотонные RGBA 6×6 (110, 96, 255, 150).
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<!doctype html><html><head><meta charset="utf-8"><style>
|
||||||
|
html,body{margin:0;width:1024px;height:768px;overflow:hidden;font-family:Inter,"Segoe UI",Roboto,Helvetica,Arial,sans-serif}
|
||||||
|
body{background:radial-gradient(900px 520px at 8% 0%, rgba(123,107,255,.55), transparent 60%), radial-gradient(760px 520px at 100% 100%, rgba(47,115,240,.45), transparent 55%), #0b0a1f;color:#fff;position:relative}
|
||||||
|
.grid{position:absolute;inset:0;background-image:linear-gradient(rgba(255,255,255,.045) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.045) 1px,transparent 1px);background-size:48px 48px;-webkit-mask-image:radial-gradient(ellipse at 30% 30%,#000 15%,transparent 72%)}
|
||||||
|
.brand{position:absolute;left:72px;top:64px;display:flex;align-items:center;gap:18px}
|
||||||
|
.mark{width:64px;height:64px;border-radius:18px;background:linear-gradient(135deg,#7b6bff 0%,#5a48f5 45%,#2f73f0 100%);box-shadow:0 14px 40px -10px rgba(90,72,245,.9), inset 0 1px 0 rgba(255,255,255,.3);display:grid;place-items:center}
|
||||||
|
.mark svg{width:38px;height:38px}
|
||||||
|
.name{font-size:38px;font-weight:700;letter-spacing:-.02em;line-height:1}
|
||||||
|
.name small{display:block;margin-top:8px;font-size:16px;font-weight:500;color:rgba(255,255,255,.62);letter-spacing:0}
|
||||||
|
.tag{position:absolute;left:72px;top:176px;font-size:30px;font-weight:600;letter-spacing:-.02em;line-height:1.25;max-width:620px}
|
||||||
|
.tag span{background:linear-gradient(90deg,#c4bcff,#8fb8ff);-webkit-background-clip:text;background-clip:text;color:transparent}
|
||||||
|
.foot{position:absolute;left:72px;bottom:56px;color:rgba(255,255,255,.5);font-size:15px}
|
||||||
|
.hint{position:absolute;right:72px;bottom:56px;color:rgba(255,255,255,.5);font-size:15px;text-align:right}
|
||||||
|
</style></head><body><div class="grid"></div>
|
||||||
|
<div class="brand"><div class="mark"><svg viewBox="0 0 24 24" fill="none"><path d="M4.5 5.5 12 19l7.5-13.5" stroke="#fff" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/><path d="M9 5.5h6" stroke="#fff" stroke-opacity=".55" stroke-width="2.6" stroke-linecap="round"/></svg></div><div class="name">Virtuality<small>Платформа виртуализации</small></div></div>
|
||||||
|
<div class="tag">Ваши серверы.<br><span>Под полным контролем.</span></div>
|
||||||
|
<div class="foot">Установщик · KVM · QEMU · libvirt</div>
|
||||||
|
<div class="hint">↑ ↓ — выбор пункта, Enter — запуск</div>
|
||||||
|
</body></html>
|
||||||
|
После Ширина: | Высота: | Размер: 151 KiB |
|
После Ширина: | Высота: | Размер: 75 B |
|
После Ширина: | Высота: | Размер: 75 B |
|
После Ширина: | Высота: | Размер: 75 B |
|
После Ширина: | Высота: | Размер: 75 B |
|
После Ширина: | Высота: | Размер: 75 B |
|
После Ширина: | Высота: | Размер: 75 B |
|
После Ширина: | Высота: | Размер: 75 B |
|
После Ширина: | Высота: | Размер: 75 B |
|
После Ширина: | Высота: | Размер: 75 B |
@@ -0,0 +1,41 @@
|
|||||||
|
desktop-image: "background.png"
|
||||||
|
desktop-image-scale-method: "stretch"
|
||||||
|
desktop-color: "#0b0a1f"
|
||||||
|
title-text: ""
|
||||||
|
terminal-font: "Unicode Regular"
|
||||||
|
terminal-left: "7%"
|
||||||
|
terminal-top: "36%"
|
||||||
|
terminal-width: "86%"
|
||||||
|
terminal-height: "50%"
|
||||||
|
terminal-border: "0"
|
||||||
|
|
||||||
|
+ boot_menu {
|
||||||
|
left = 7%
|
||||||
|
top = 36%
|
||||||
|
width = 60%
|
||||||
|
height = 42%
|
||||||
|
item_font = "Unicode Regular"
|
||||||
|
item_color = "#b9b8cf"
|
||||||
|
selected_item_color = "#ffffff"
|
||||||
|
item_height = 36
|
||||||
|
item_padding = 10
|
||||||
|
item_spacing = 6
|
||||||
|
icon_width = 0
|
||||||
|
icon_height = 0
|
||||||
|
scrollbar = false
|
||||||
|
selected_item_pixmap_style = "select_*.png"
|
||||||
|
}
|
||||||
|
|
||||||
|
+ progress_bar {
|
||||||
|
id = "__timeout__"
|
||||||
|
left = 7%
|
||||||
|
top = 86%
|
||||||
|
width = 60%
|
||||||
|
height = 24
|
||||||
|
font = "Unicode Regular"
|
||||||
|
text_color = "#ffffff"
|
||||||
|
fg_color = "#7b6bff"
|
||||||
|
bg_color = "#1c1a36"
|
||||||
|
border_color = "#2a2748"
|
||||||
|
text = "Автоматический запуск через %d с"
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { createRequire } from 'node:module';
|
||||||
|
const { chromium } = createRequire(import.meta.url)('playwright');
|
||||||
|
// Render an HTML file to PNG. Usage: node tests/render_html.mjs in.html out.png [width] [height]
|
||||||
|
const [,, html, out, width = '1024', height = '768'] = process.argv;
|
||||||
|
const browser = await chromium.launch();
|
||||||
|
const page = await browser.newPage({ viewport: { width: +width, height: +height }, deviceScaleFactor: 1 });
|
||||||
|
await page.goto('file://' + (html.startsWith('/') ? html : process.cwd() + '/' + html));
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
await page.screenshot({ path: out, type: 'png' });
|
||||||
|
await browser.close();
|
||||||