cub: modernize TUI (rounded borders, semantic colors, help overlay, Home dashboard) + add CLI commands (UpdatesAvailable, OwnsFile, pacman aliases) (v6.0 2026)
This commit is contained in:
@@ -0,0 +1,391 @@
|
||||
# GRUB Integration Plan — Red Bear OS
|
||||
|
||||
**Date:** 2026-04-17
|
||||
**Status:** Fully implemented (build-tested, not yet runtime boot-tested). ESP formatted as FAT32
|
||||
per UEFI spec. Both Phase 1 (post-build script) and Phase 2 (installer-native) are wired.
|
||||
**Remaining:** Runtime UEFI boot validation in QEMU (`make all CONFIG_NAME=redbear-grub && make qemu`).
|
||||
**Prerequisite:** The `grub` package is included in `redbear-grub.toml` for clean-tree builds.
|
||||
**Approach:** Option A — GRUB as boot manager, chainloading Redox bootloader
|
||||
|
||||
## Overview
|
||||
|
||||
Add GNU GRUB as an optional boot manager for Red Bear OS. GRUB presents a menu
|
||||
at boot and chainloads the existing Redox bootloader, which then boots the
|
||||
kernel normally. This gives users:
|
||||
|
||||
- Multi-boot capability alongside Linux, Windows, or other OSes
|
||||
- Boot menu with timeout and manual selection
|
||||
- Familiar GRUB rescue shell for debugging
|
||||
- No changes to the Redox kernel, RedoxFS, or existing boot flow
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
UEFI firmware
|
||||
→ EFI/BOOT/BOOTX64.EFI (GRUB standalone image)
|
||||
→ grub.cfg: default entry chainloads Redox bootloader
|
||||
→ EFI/REDBEAR/redbear.efi (Redox bootloader)
|
||||
→ Reads RedoxFS partition
|
||||
→ Loads kernel
|
||||
→ Boots Red Bear OS
|
||||
```
|
||||
|
||||
### ESP Layout (GRUB mode)
|
||||
|
||||
```
|
||||
EFI/
|
||||
├── BOOT/
|
||||
│ ├── BOOTX64.EFI ← GRUB (primary, loaded by UEFI firmware)
|
||||
│ └── grub.cfg ← GRUB configuration
|
||||
└── REDBEAR/
|
||||
└── redbear.efi ← Redox bootloader (chainload target)
|
||||
```
|
||||
|
||||
### ESP Layout (default, no GRUB)
|
||||
|
||||
```
|
||||
EFI/
|
||||
└── BOOT/
|
||||
└── BOOTX64.EFI ← Redox bootloader (unchanged)
|
||||
```
|
||||
|
||||
## Why GRUB?
|
||||
|
||||
1. **GRUB does not support RedoxFS.** Writing a GRUB filesystem module for
|
||||
RedoxFS is high-risk, GPL-licensing-sensitive work. Chainloading avoids it.
|
||||
2. **The Redox bootloader works.** It reads RedoxFS directly and boots the
|
||||
kernel. No need to replicate that logic in GRUB.
|
||||
3. **GRUB is universally understood.** System administrators know GRUB. A
|
||||
`grub.cfg` is easier to customize than a custom bootloader.
|
||||
4. **Multi-boot.** GRUB can boot Linux, Windows, and other OSes alongside
|
||||
Red Bear OS without any changes to those systems.
|
||||
|
||||
## GRUB Module Set
|
||||
|
||||
The standalone EFI image includes these modules:
|
||||
|
||||
| Module | Purpose |
|
||||
|--------|---------|
|
||||
| `part_gpt` | GPT partition table support |
|
||||
| `part_msdos` | MBR partition table support |
|
||||
| `fat` | FAT32 filesystem (ESP) |
|
||||
| `ext2` | ext2/3/4 filesystem |
|
||||
| `normal` | Normal mode (menu, scripting) |
|
||||
| `configfile` | Load configuration files |
|
||||
| `search` | Search for files/volumes |
|
||||
| `search_fs_uuid` | Search by filesystem UUID |
|
||||
| `search_label` | Search by volume label |
|
||||
| `echo` | Print messages |
|
||||
| `test` | Conditional expressions |
|
||||
| `ls` | List files and devices |
|
||||
| `cat` | Display file contents |
|
||||
| `halt` | Shut down |
|
||||
| `reboot` | Reboot |
|
||||
|
||||
Note: `chainloader` is a built-in command in GRUB 2.12 (no separate module needed).
|
||||
|
||||
Red Bear policy now requires a local `redoxfs.mod` artifact for GRUB builds.
|
||||
The GRUB recipe resolves it in this order:
|
||||
1. `local/recipes/core/grub/modules/redoxfs.mod`
|
||||
2. `${COOKBOOK_SYSROOT}/usr/lib/grub/x86_64-efi/redoxfs.mod`
|
||||
|
||||
If neither exists, the GRUB recipe fails fast.
|
||||
|
||||
## GRUB Configuration
|
||||
|
||||
The default `grub.cfg`:
|
||||
|
||||
```cfg
|
||||
# Red Bear OS GRUB Configuration
|
||||
set default=0
|
||||
set timeout=5
|
||||
|
||||
menuentry "Red Bear OS" {
|
||||
chainloader /EFI/REDBEAR/redbear.efi
|
||||
boot
|
||||
}
|
||||
|
||||
menuentry "Reboot" {
|
||||
reboot
|
||||
}
|
||||
|
||||
menuentry "Shutdown" {
|
||||
halt
|
||||
}
|
||||
```
|
||||
|
||||
Users can customize `grub.cfg` to add entries for other operating systems,
|
||||
change the timeout, or add additional Red Bear OS entries (e.g., recovery
|
||||
mode with different kernel parameters, once supported).
|
||||
|
||||
## ESP Size Requirements
|
||||
|
||||
| Component | Typical Size |
|
||||
|-----------|--------------|
|
||||
| GRUB EFI binary (with modules) | ~500 KiB (varies with module list) |
|
||||
| Redox bootloader | 100–200 KiB |
|
||||
| grub.cfg | < 1 KiB |
|
||||
| **Total** | **~1 MiB** |
|
||||
|
||||
The default ESP is 1 MiB (too small for GRUB). Configs using GRUB must set:
|
||||
|
||||
```toml
|
||||
[general]
|
||||
efi_partition_size = 16 # 16 MiB, enough for GRUB + Redox bootloader + margin
|
||||
```
|
||||
|
||||
## Linux-Compatible CLI
|
||||
|
||||
Red Bear OS provides `grub-install` and `grub-mkconfig` wrappers that match GNU GRUB
|
||||
command-line conventions. Users migrating from Linux can use familiar switches.
|
||||
|
||||
| Linux Command | Red Bear OS Location |
|
||||
|---------------|---------------------|
|
||||
| `grub-install` | `local/scripts/grub-install` |
|
||||
| `grub-mkconfig` | `local/scripts/grub-mkconfig` |
|
||||
|
||||
Add to PATH for convenience:
|
||||
```bash
|
||||
export PATH="$PWD/local/scripts:$PATH"
|
||||
```
|
||||
|
||||
### grub-install
|
||||
|
||||
```bash
|
||||
# Install GRUB into a disk image
|
||||
grub-install --target=x86_64-efi --disk-image=build/x86_64/harddrive.img
|
||||
|
||||
# Verbose mode
|
||||
grub-install --target=x86_64-efi --disk-image=build/x86_64/harddrive.img --verbose
|
||||
|
||||
# Show help
|
||||
grub-install --help
|
||||
```
|
||||
|
||||
Supported options: `--target=`, `--efi-directory=`, `--bootloader-id=`, `--removable`,
|
||||
`--disk-image=`, `--modules=`, `--no-nvram`, `--verbose`, `--help`, `--version`.
|
||||
|
||||
Unsupported Linux options are accepted and ignored silently for script compatibility.
|
||||
|
||||
### grub-mkconfig
|
||||
|
||||
```bash
|
||||
# Preview generated config
|
||||
grub-mkconfig
|
||||
|
||||
# Write to file
|
||||
grub-mkconfig -o local/recipes/core/grub/grub.cfg
|
||||
|
||||
# Custom timeout
|
||||
grub-mkconfig --timeout=10 -o /boot/grub/grub.cfg
|
||||
```
|
||||
|
||||
Supported options: `-o`/`--output=`, `--timeout=`, `--set-default=`, `--help`, `--version`.
|
||||
|
||||
## Implementation — Phase 1: Post-Build Script
|
||||
|
||||
Phase 1 uses a post-build script to modify the ESP in an existing disk image.
|
||||
This approach requires **no changes to the installer** and works immediately.
|
||||
|
||||
### Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `local/recipes/core/grub/recipe.toml` | Build GRUB from source, produce `grub.efi` |
|
||||
| `local/recipes/core/grub/grub.cfg` | Default GRUB configuration |
|
||||
| `local/recipes/core/grub/modules/redoxfs.mod` | Mandatory local GRUB RedoxFS module artifact |
|
||||
| `local/scripts/install-grub.sh` | Post-build ESP modification script |
|
||||
| `local/scripts/fat_tool.py` | Python FAT32 tool (no mtools dependency) |
|
||||
| `recipes/core/grub → local/recipes/core/grub` | Symlink for recipe discovery |
|
||||
|
||||
### Workflow
|
||||
|
||||
```bash
|
||||
# 1. Build GRUB recipe
|
||||
make r.grub
|
||||
|
||||
# 2. Build Red Bear OS (with larger ESP)
|
||||
make all CONFIG_NAME=redbear-full # Must have efi_partition_size = 16
|
||||
|
||||
# 3. Install GRUB into disk image
|
||||
./local/scripts/install-grub.sh build/x86_64/harddrive.img
|
||||
|
||||
# 4. Test
|
||||
make qemu
|
||||
```
|
||||
|
||||
### Requirements
|
||||
|
||||
- Python 3 (for `fat_tool.py` — no mtools dependency)
|
||||
- GRUB build dependencies: `gcc`, `make`, `bison`, `flex`, `autoconf`, `automake`
|
||||
- ESP must be ≥ 8 MiB (set `efi_partition_size = 16` in config)
|
||||
|
||||
## Implementation — Phase 2: Installer-Native Support
|
||||
|
||||
Phase 2 adds GRUB awareness directly to the Redox installer, eliminating the
|
||||
post-build script step. The installer reads `bootloader = "grub"` from config,
|
||||
fetches the GRUB package alongside the bootloader, and writes the chainload
|
||||
ESP layout automatically.
|
||||
|
||||
### Changes Made
|
||||
|
||||
1. **`GeneralConfig`** (`config/general.rs`): Added `bootloader: Option<String>`
|
||||
field (`"redox"` default, `"grub"` for GRUB), with merge support.
|
||||
|
||||
2. **`DiskOption`** (`installer.rs`): Added `grub_efi: Option<&[u8]>` and
|
||||
`grub_config: Option<&[u8]>` fields for optional GRUB data.
|
||||
|
||||
3. **`fetch_bootloaders`**: When `bootloader = "grub"`, installs the `grub`
|
||||
package alongside `bootloader` and returns `grub.efi` + `grub.cfg` data.
|
||||
Return type extended to `(bios, efi, grub_efi, grub_cfg)`.
|
||||
|
||||
4. **`with_whole_disk` / `with_whole_disk_ext4`**: When `grub_efi` and
|
||||
`grub_config` are both present, writes the GRUB chainload layout:
|
||||
- `EFI/BOOT/BOOTX64.EFI` ← GRUB
|
||||
- `EFI/BOOT/grub.cfg` ← GRUB configuration
|
||||
- `EFI/REDBEAR/redbear.efi` ← Redox bootloader (chainload target)
|
||||
|
||||
5. **`install_inner`**: Passes GRUB data from `fetch_bootloaders` through
|
||||
`DiskOption`.
|
||||
|
||||
6. **CLI** (`bin/installer.rs`): Added `--bootloader grub` flag that sets
|
||||
`config.general.bootloader`.
|
||||
|
||||
7. **TUI** (`bin/installer_tui.rs`): Updated `DiskOption` construction with
|
||||
`grub_efi: None, grub_config: None`.
|
||||
|
||||
### Config Usage
|
||||
|
||||
```toml
|
||||
# config/redbear-grub.toml
|
||||
include = ["redbear-full.toml"]
|
||||
|
||||
[general]
|
||||
bootloader = "grub"
|
||||
efi_partition_size = 16
|
||||
```
|
||||
|
||||
Or via CLI (note: INSTALLER_OPTS replaces defaults, so --cookbook=. must be included):
|
||||
```bash
|
||||
./target/release/repo cook installer
|
||||
make all CONFIG_NAME=redbear-full INSTALLER_OPTS="--cookbook=. --bootloader grub"
|
||||
```
|
||||
|
||||
**Note:** The config file approach (`redbear-grub.toml`) is preferred over the CLI flag
|
||||
because INSTALLER_OPTS completely replaces the default value (`--cookbook=.`) rather than
|
||||
appending to it. Omitting `--cookbook=.` breaks local package resolution for GRUB.
|
||||
|
||||
## GRUB Recipe Design
|
||||
|
||||
The GRUB recipe uses `template = "custom"` because GRUB must be built for the
|
||||
**host machine** (it's a build tool that produces EFI binaries), not for the
|
||||
Redox target. The cookbook's `configure` template cross-compiles for Redox,
|
||||
which is wrong for GRUB.
|
||||
|
||||
Key build steps:
|
||||
1. Configure with `--target=x86_64 --with-platform=efi` (produces x86_64 EFI)
|
||||
2. Disable unnecessary components (themes, mkfont, mount, device-mapper)
|
||||
3. Run `grub-mkimage` to create standalone EFI binary with curated modules
|
||||
4. Stage `grub.efi` and `grub.cfg` to `/usr/lib/boot/`
|
||||
|
||||
### Build Notes
|
||||
|
||||
The recipe uses `template = "custom"` because the cookbook's default `configure`
|
||||
template sets `--host="${GNU_TARGET}"` for Redox cross-compilation, which is wrong
|
||||
for GRUB (a host build tool producing EFI binaries).
|
||||
|
||||
Two issues required workarounds:
|
||||
|
||||
1. **Cross-compiler override.** The cookbook sets `CC`, `CXX`, `CFLAGS`, etc. to
|
||||
the Redox cross-toolchain. GRUB must be built with the host compiler. Fix:
|
||||
`unset CC CXX CPP LD AR NM RANLIB OBJCOPY STRIP PKG_CONFIG` and
|
||||
`unset CFLAGS CXXFLAGS CPPFLAGS LDFLAGS` at the top of the script.
|
||||
|
||||
2. **Missing `extra_deps.lst`.** GRUB 2.12 release tarballs omit
|
||||
`grub-core/extra_deps.lst` (normally generated by `autogen.sh` from git).
|
||||
Fix: `touch "${COOKBOOK_SOURCE}/grub-core/extra_deps.lst"` before configure.
|
||||
|
||||
3. **grub.cfg location.** The config file lives in the recipe directory
|
||||
(`${COOKBOOK_RECIPE}/grub.cfg`), not in the extracted source tarball
|
||||
(`${COOKBOOK_SOURCE}/`). The copy step uses `COOKBOOK_RECIPE`.
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- GRUB configuration is on the ESP (FAT32), which is readable/writable by any OS
|
||||
- Secure Boot: GRUB standalone images are not signed. Users needing Secure Boot
|
||||
must sign `BOOTX64.EFI` with their own key or use `shim`
|
||||
- The chainload target (`EFI/REDBEAR/redbear.efi`) is also on the ESP
|
||||
- No credentials or secrets are stored in the GRUB configuration
|
||||
|
||||
## Limitations
|
||||
|
||||
- GRUB cannot read RedoxFS (no module exists)
|
||||
- Cannot pass kernel parameters directly (chainloading bypasses this)
|
||||
- BIOS boot is not supported (only UEFI)
|
||||
- ESP must be sized to ≥ 8 MiB in config (16 MiB recommended)
|
||||
- GRUB bootloader is incompatible with `skip_partitions = true` (requires GPT layout with ESP)
|
||||
- TUI installer does not support GRUB mode (intentional — TUI is for live disk reinstall)
|
||||
- Runtime UEFI boot test has not been performed yet (requires full `make all` build, ~hours)
|
||||
|
||||
## Testing
|
||||
|
||||
### Phase 1: Post-build script (standalone)
|
||||
|
||||
```bash
|
||||
# Build GRUB recipe
|
||||
make r.grub
|
||||
|
||||
# Build image (any config with efi_partition_size >= 16)
|
||||
make all CONFIG_NAME=redbear-full
|
||||
|
||||
# Install GRUB into disk image (uses fat_tool.py, no mtools needed)
|
||||
./local/scripts/install-grub.sh build/x86_64/harddrive.img
|
||||
|
||||
# Verify ESP contents
|
||||
python3 local/scripts/fat_tool.py ls build/x86_64/harddrive.img 1048576 /
|
||||
|
||||
# Boot in QEMU
|
||||
make qemu
|
||||
# Expected: GRUB menu appears, "Red Bear OS" entry boots successfully
|
||||
```
|
||||
|
||||
### Phase 2: Installer-native (automatic)
|
||||
|
||||
```bash
|
||||
# Build GRUB recipe (must be built before installer runs)
|
||||
make r.grub
|
||||
|
||||
# Build image with GRUB config (installer fetches GRUB automatically)
|
||||
make all CONFIG_NAME=redbear-grub
|
||||
|
||||
# Or via CLI flag
|
||||
make all CONFIG_NAME=redbear-full INSTALLER_OPTS="--bootloader grub --cookbook=."
|
||||
|
||||
# Verify ESP contents
|
||||
python3 local/scripts/fat_tool.py ls build/x86_64/harddrive.img 1048576 /
|
||||
|
||||
# Boot in QEMU
|
||||
make qemu
|
||||
# Expected: GRUB menu appears, "Red Bear OS" entry boots successfully
|
||||
```
|
||||
|
||||
### Unit tests (no full build required)
|
||||
|
||||
```bash
|
||||
# Verify GRUB recipe builds
|
||||
CI=1 ./target/release/repo cook grub
|
||||
|
||||
# Verify host-side installer accepts --bootloader flag
|
||||
build/fstools/bin/redox_installer --bootloader=grub --config=config/redbear-grub.toml --list-packages
|
||||
|
||||
# Verify fat_tool.py operations
|
||||
python3 local/scripts/fat_tool.py --help
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- GNU GRUB Manual: https://www.gnu.org/software/grub/manual/grub/grub.html
|
||||
- GRUB EFI standalone image: `grub-mkimage -O x86_64-efi ...`
|
||||
- UEFI boot specification: `EFI/BOOT/BOOTX64.EFI` is the fallback boot path
|
||||
- Redox bootloader source: `recipes/core/bootloader/source/`
|
||||
- Installer GPT layout: `recipes/core/installer/source/src/installer.rs`
|
||||
@@ -0,0 +1,748 @@
|
||||
# Red Bear OS — Kernel, IPC, and Credential Syscalls Plan
|
||||
|
||||
**Date:** 2026-04-30
|
||||
**Scope:** Kernel architecture, IPC infrastructure, credential syscalls, process isolation
|
||||
**Implementation status:** Phases K1-K2, K4 ✅ complete. Phases K3, K5 deferred.
|
||||
**Status:** This document is the canonical kernel + IPC plan, extending `local/docs/CONSOLE-TO-KDE-DESKTOP-PLAN.md`
|
||||
|
||||
## 1. Purpose
|
||||
|
||||
This plan defines the implementation roadmap for kernel hardening, IPC improvements, and credential
|
||||
syscall implementation in Red Bear OS. It is the **canonical kernel authority** superseding scattered
|
||||
kernel guidance in other docs.
|
||||
|
||||
**Relationship to existing plans:**
|
||||
|
||||
| Document | Relationship |
|
||||
|----------|-------------|
|
||||
| `CONSOLE-TO-KDE-DESKTOP-PLAN.md` | Parent: CONSOLE-TO-KDE v4.0 (Kernel & Core Infrastructure) |
|
||||
| `IRQ-AND-LOWLEVEL-CONTROLLERS-ENHANCEMENT-PLAN.md` | Sibling: IRQ/PCI/MSI-X — not duplicated here |
|
||||
| `RELIBC-IPC-ASSESSMENT-AND-IMPROVEMENT-PLAN.md` | Companion: relibc IPC surface — this plan covers kernel side |
|
||||
| `ACPI-IMPROVEMENT-PLAN.md` | Sibling: ACPI power/shutdown — relevant for §4 (shutdown robustness) |
|
||||
| `CONSOLE-TO-KDE-DESKTOP-PLAN.md` | Consumer: desktop stack depends on kernel work here |
|
||||
|
||||
## 2. Current Architecture Assessment
|
||||
|
||||
### 2.1 Kernel Overview
|
||||
|
||||
The Redox microkernel (`recipes/core/kernel/source/`) is a ~20-40k LoC Rust microkernel. It runs in
|
||||
ring 0 and provides:
|
||||
|
||||
- **12 kernel schemes**: `debug`, `event`, `memory`, `pipe`, `irq`, `time`, `sys`, `proc`, `serio`,
|
||||
`acpi`, `dtb`, `user` (userspace scheme wrapper)
|
||||
- **~35 handled syscalls**: file I/O, memory mapping, process control, futex, time
|
||||
- **Catch-all ENOSYS**: all unhandled syscall numbers return `ENOSYS`
|
||||
|
||||
```
|
||||
recipes/core/kernel/source/src/
|
||||
├── syscall/ # Syscall dispatch: mod.rs (handlers), fs.rs, process.rs, futex.rs, time.rs
|
||||
│ └── mod.rs # Main syscall() dispatch: 35 explicit match arms, _ => ENOSYS
|
||||
├── scheme/ # Kernel schemes: debug, event, memory, pipe, irq, time, sys, proc, serio
|
||||
│ ├── mod.rs # Scheme trait definition, SchemeId, FileHandle types
|
||||
│ ├── proc.rs # Process manager scheme (fork, exec, signal, credential setting)
|
||||
│ └── sys/ # System info scheme: context list, syscall debug, uname
|
||||
├── context/ # Process/thread context management
|
||||
│ ├── context.rs # Context struct: euid, egid, pid, files, signals, addr_space
|
||||
│ └── memory.rs # Address space, grants, mmap implementation
|
||||
├── memory/ # Physical/virtual memory management, page tables
|
||||
└── sync/ # Locking primitives (RwLock, Mutex, CleanLockToken)
|
||||
```
|
||||
|
||||
### 2.2 Syscall Dispatch Architecture
|
||||
|
||||
The kernel's `syscall()` function in `syscall/mod.rs` dispatches based on `a` (syscall number):
|
||||
|
||||
```rust
|
||||
// From recipes/core/kernel/source/src/syscall/mod.rs (line 75)
|
||||
match a {
|
||||
SYS_WRITE2 => file_op_generic_ext(..),
|
||||
SYS_WRITE => sys_write(..),
|
||||
SYS_FMAP => { .. }, // Anonymous or file-backed mmap
|
||||
SYS_READ2 => file_op_generic_ext(..),
|
||||
SYS_READ => sys_read(..),
|
||||
SYS_FPATH => file_op_generic(..),
|
||||
SYS_FSTAT => fstat(..),
|
||||
SYS_DUP => dup(..),
|
||||
SYS_DUP2 => dup2(..),
|
||||
SYS_SENDFD => sendfd(..),
|
||||
SYS_OPENAT => openat(..),
|
||||
SYS_UNLINKAT => unlinkat(..),
|
||||
SYS_CLOSE => close(..),
|
||||
SYS_CALL => call(..), // Scheme IPC: send message to scheme
|
||||
SYS_FEVENT => fevent(..), // Register event on fd
|
||||
SYS_YIELD => sched_yield(..),
|
||||
SYS_NANOSLEEP => nanosleep(..),
|
||||
SYS_CLOCK_GETTIME => clock_gettime(..),
|
||||
SYS_FUTEX => futex(..),
|
||||
SYS_MPROTECT => mprotect(..),
|
||||
SYS_MREMAP => mremap(..),
|
||||
// ... ~15 more file operations (fchmod, fchown, fcntl, flink, frename, ftruncate, fsync, etc.)
|
||||
_ => Err(Error::new(ENOSYS)), // ← CATCH-ALL: all credential syscalls fall here
|
||||
}
|
||||
```
|
||||
|
||||
Syscall numbers come from the external `redox_syscall` crate (crates.io), not from the kernel tree.
|
||||
The kernel consumes them via `use syscall::number::*`.
|
||||
|
||||
### 2.3 Credential Architecture (Current)
|
||||
|
||||
**Kernel Context struct** (`context/context.rs`):
|
||||
|
||||
```rust
|
||||
pub struct Context {
|
||||
// Credential fields (initialized to 0):
|
||||
pub euid: u32, // Effective user ID — used for scheme access control
|
||||
pub egid: u32, // Effective group ID
|
||||
pub pid: usize, // Process ID (set via proc scheme)
|
||||
|
||||
// NOT present in kernel:
|
||||
// ruid, suid — real/saved UID (maintained in userspace redox-rt)
|
||||
// rgid, sgid — real/saved GID (maintained in userspace redox-rt)
|
||||
// supplementary groups — not implemented anywhere
|
||||
|
||||
// Access control interface:
|
||||
pub fn caller_ctx(&self) -> CallerCtx {
|
||||
CallerCtx { uid: self.euid, gid: self.egid, pid: self.pid }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Credential read path** (userspace, no kernel involvement):
|
||||
```
|
||||
getuid() → relibc::platform::redox::getuid()
|
||||
→ redox_rt::sys::posix_getresugid()
|
||||
→ reads local DYNAMIC_PROC_INFO { ruid, euid, suid, rgid, egid, sgid }
|
||||
→ returns cached userspace values (NO kernel syscall)
|
||||
```
|
||||
|
||||
**Credential write path** (through `proc:` scheme):
|
||||
```
|
||||
setresuid(ruid, euid, suid) → relibc::platform::redox::setresuid()
|
||||
→ redox_rt::sys::posix_setresugid(&Resugid { ruid, euid, suid, .. })
|
||||
→ packs 6×u32 into buffer
|
||||
→ this_proc_call(&buf, CallFlags::empty(), &[ProcCall::SetResugid as u64])
|
||||
→ SYS_CALL to proc: scheme
|
||||
→ kernel proc scheme handler (scheme/proc.rs:1269):
|
||||
guard.euid = info.euid;
|
||||
guard.egid = info.egid;
|
||||
```
|
||||
|
||||
**Key finding**: The kernel DOES support credential setting through the `proc:` scheme, using
|
||||
`ProcSchemeAttrs` with `euid`/`egid`/`pid`/`prio`/`debug_name` fields. The `getuid()`/`getgid()`
|
||||
functions work through userspace-cached values in `redox-rt`. `setresuid()`/`setresgid()` work
|
||||
through the proc scheme.
|
||||
|
||||
**What's genuinely broken:**
|
||||
|
||||
| Function | Status | Root Cause |
|
||||
|----------|--------|------------|
|
||||
| `setgroups()` | **ENOSYS stub** | relibc/redox/mod.rs:1205 — `todo_skip!(0, "setgroups({}, {:p}): not implemented")` |
|
||||
| `getgroups()` | /etc/group-based | Works via `getpwuid()` + `getgrent()` iteration — doesn't use kernel groups |
|
||||
| `initgroups()` | No-op | No supplementary group infrastructure |
|
||||
|
||||
### 2.4 IPC Architecture
|
||||
|
||||
**Scheme-based IPC** is the primary IPC mechanism:
|
||||
|
||||
```
|
||||
┌─────────────┐ SYS_CALL(syscall) ┌──────────────┐
|
||||
│ Userspace │ ──────────────────────────→│ Kernel │
|
||||
│ Process A │ open/read/write/fevent │ Scheme │
|
||||
│ │ ←──────────────────────────│ Dispatch │
|
||||
└─────────────┘ result (usize/-errno) └──────┬───────┘
|
||||
│
|
||||
┌─────────────────────┤
|
||||
│ │
|
||||
┌────▼──────┐ ┌──────▼──────┐
|
||||
│ Kernel │ │ Userspace │
|
||||
│ Schemes │ │ Scheme │
|
||||
│ (12) │ │ Daemons │
|
||||
│ │ │ (via user:) │
|
||||
│ debug: │ │ │
|
||||
│ event: │ │ ptyd │
|
||||
│ memory: │ │ pcid │
|
||||
│ pipe: │ │ ext4d │
|
||||
│ irq: │ │ fatd │
|
||||
│ time: │ │ redox-drm │
|
||||
│ sys: │ │ ... │
|
||||
│ proc: │ │ │
|
||||
│ serio: │ │ │
|
||||
└───────────┘ └──────────────┘
|
||||
```
|
||||
|
||||
**IPC primitives available:**
|
||||
|
||||
| Primitive | Mechanism | Kernel/Userspace |
|
||||
|-----------|-----------|-----------------|
|
||||
| `pipe:` scheme | Kernel pipe scheme — bidirectional byte streams | Kernel |
|
||||
| `shm_open()` / `mmap(MAP_SHARED)` | Shared memory via memory scheme grants | Kernel |
|
||||
| `SYS_CALL` + scheme messages | Send/receive typed messages to scheme daemons | Kernel dispatch, userspace handler |
|
||||
| `fevent()` | Register kernel-level events on file descriptors | Kernel |
|
||||
| `sendfd()` | Pass file descriptors between processes | Kernel |
|
||||
| `event:` scheme | Kernel event notification (used by eventfd/signalfd/timerfd) | Kernel |
|
||||
| Signals | `sigprocmask` + `sigaction` via proc: scheme | Kernel delivery, userspace handling |
|
||||
| Futex | Fast userspace mutex via `SYS_FUTEX` | Kernel |
|
||||
|
||||
**Current IPC limitations:**
|
||||
|
||||
| Limitation | Impact |
|
||||
|-----------|--------|
|
||||
| No `SYS_PTRACE` | ptrace not available (handled via proc: scheme paths) |
|
||||
| No `SYS_KILL` | Signal sending via proc: scheme only |
|
||||
| eventfd/signalfd/timerfd recipe-applied | Bounded compatibility layers, not plain-source |
|
||||
| `ifaddrs` synthetic | Only `loopback` + `eth0`, not live enumeration |
|
||||
| POSIX message queues not implemented | `mqueue.h` missing entirely |
|
||||
| SysV message queues not implemented | `sys/msg.h` missing entirely |
|
||||
| No UNIX domain sockets (`AF_UNIX`) path | Socket-based IPC limited |
|
||||
|
||||
### 2.5 Process Model
|
||||
|
||||
Redox uses a **userspace process manager** (`procmgr` via `proc:` scheme):
|
||||
|
||||
- **fork**: Implemented through proc: scheme → kernel creates new Context with cloned address space
|
||||
- **exec**: Replaces address space with new executable image
|
||||
- **spawn**: Combined fork+exec via proc: scheme
|
||||
- **wait/waitpid/waitid**: Recipe-applied patch via proc: scheme (signals child exit)
|
||||
- **Credentials on fork**: Address space cloned (userspace `DYNAMIC_PROC_INFO` inherited)
|
||||
- **Credentials on exec**: `setresuid()` behavior (suid-bit not implemented in kernel)
|
||||
|
||||
The kernel's Context struct tracks:
|
||||
- `owner_proc_id: Option<NonZeroUsize>` — parent process for exit notification
|
||||
- `files: Arc<LockedFdTbl>` — file descriptor table (can be shared)
|
||||
- `addr_space: Option<Arc<AddrSpaceWrapper>>` — address space (can be shared = threads)
|
||||
- `sig: Option<SignalState>` — signal handler configuration
|
||||
|
||||
## 3. Critical Gaps and Blockers
|
||||
|
||||
### 3.1 Credential Syscall Blocker (Priority: P0-CRITICAL)
|
||||
|
||||
The `setgroups()` function is **ENOSYS**. This blocks:
|
||||
- `polkit` — uses `setgroups()` for privilege management
|
||||
- `dbus-daemon` — uses credentials for service activation
|
||||
- `logind` / `redbear-sessiond` — needs credential awareness
|
||||
- `sudo` / `su` — uses `initgroups()` → `setgroups()`
|
||||
- Any program that changes user identity
|
||||
|
||||
**Root cause chain:**
|
||||
1. `redox_syscall` crate (crates.io, upstream) has no `SYS_SETGROUPS`/`SYS_GETGROUPS` numbers
|
||||
2. Kernel has no supplementary group table in Context struct
|
||||
3. No group inheritance on fork/exec
|
||||
4. relibc `setgroups()` is a `todo_skip!()` stub
|
||||
5. `getgroups()` bypasses kernel entirely (reads /etc/group)
|
||||
|
||||
### 3.2 Kernel-Level Access Control Gap (Priority: P1)
|
||||
|
||||
The kernel's `caller_ctx()` provides `{euid, egid, pid}` to scheme handlers, but:
|
||||
|
||||
1. **No consistent enforcement**: Kernel schemes may or may not check caller credentials
|
||||
2. **No ruid/suid tracking**: Cannot distinguish real vs effective identity in kernel
|
||||
3. **All processes start as root** (euid=0, egid=0): No privilege separation at boot
|
||||
4. **No supplementary groups in kernel**: Only egid checked
|
||||
|
||||
### 3.3 IPC Completeness Gaps (Priority: P2)
|
||||
|
||||
| Gap | Priority | Blocked By |
|
||||
|-----|----------|------------|
|
||||
| POSIX message queues (`mqueue.h`) | P2 | Scheme design needed |
|
||||
| SysV message queues (`sys/msg.h`) | P2 | Scheme design needed |
|
||||
| UNIX domain sockets (`AF_UNIX`) | P2 | Kernel or scheme implementation |
|
||||
| Non-synthetic `ifaddrs` | P3 | Network stack enumeration |
|
||||
| eventfd/signalfd/timerfd → plain-source | P3 | Upstream relibc convergence |
|
||||
|
||||
### 3.4 Resource Limits (Priority: P2)
|
||||
|
||||
`SYS_GETRLIMIT` / `SYS_SETRLIMIT` return ENOSYS. This is a microkernel design choice:
|
||||
- Resource limits are typically library-level policy in capability systems
|
||||
- Current approach: limits enforced in userspace daemons
|
||||
- Desktop impact: systemd/logind expect rlimit support for service management
|
||||
|
||||
### 3.5 Shutdown Robustness (Priority: P2)
|
||||
|
||||
ACPI shutdown via `kstop` eventing exists but has gaps:
|
||||
- `acpid` startup has panic-grade `expect` paths
|
||||
- `_S5` derivation gated on PCI timing
|
||||
- DMAR orphaned in `acpid` source
|
||||
- See `local/docs/ACPI-IMPROVEMENT-PLAN.md` for full detail
|
||||
|
||||
## 4. Implementation Plan
|
||||
|
||||
### Phase K1: Kernel Credential Foundation (Week 1-2)
|
||||
|
||||
**Goal**: Add supplementary group support to the kernel and wire `setgroups()`/`getgroups()`.
|
||||
|
||||
#### K1.1 — Add supplementary groups to kernel Context
|
||||
|
||||
```rust
|
||||
// Context struct additions (context/context.rs):
|
||||
pub struct Context {
|
||||
// Existing:
|
||||
pub euid: u32,
|
||||
pub egid: u32,
|
||||
pub pid: usize,
|
||||
|
||||
// NEW: Real/saved IDs (moved from userspace redox-rt to kernel):
|
||||
pub ruid: u32,
|
||||
pub rgid: u32,
|
||||
pub suid: u32,
|
||||
pub sgid: u32,
|
||||
|
||||
// NEW: Supplementary groups
|
||||
pub groups: Vec<u32>, // Or Arc<[u32]> for sharing
|
||||
}
|
||||
```
|
||||
|
||||
**Files modified:**
|
||||
- `recipes/core/kernel/source/src/context/context.rs` — add fields, initialize, clone on fork
|
||||
- `recipes/core/kernel/source/src/scheme/proc.rs` — extend `ProcSchemeAttrs` to include ruid/suid/rgid/sgid/groups
|
||||
- `local/patches/kernel/` — new patch: `P4-credential-fields.patch`
|
||||
|
||||
#### K1.2 — Add `SYS_SETGROUPS` and `SYS_GETGROUPS` to redox_syscall
|
||||
|
||||
The `redox_syscall` crate is upstream (crates.io). Red Bear must either:
|
||||
- **Option A (preferred)**: Contribute upstream PR to add syscall numbers
|
||||
- **Option B**: Vendor fork of `redox_syscall` in `local/` overlay
|
||||
- **Option C**: Define Red Bear-local syscall numbers in kernel directly
|
||||
|
||||
**Recommended: Option A + B fallback**:
|
||||
1. Submit upstream PR to `redox_syscall` adding:
|
||||
- `SYS_SETGROUPS`, `SYS_GETGROUPS`
|
||||
- `SYS_SETUID`, `SYS_SETGID`, `SYS_GETUID`, `SYS_GETGID`
|
||||
- `SYS_GETEUID`, `SYS_GETEGID`
|
||||
- `SYS_SETREUID`, `SYS_SETREGID`
|
||||
- `SYS_GETRESUID`, `SYS_GETRESGID`
|
||||
|
||||
2. While upstream PR is pending, use a local `redox_syscall` patch:
|
||||
- Copy `redox_syscall` crate into `local/vendor/redox_syscall/`
|
||||
- Add syscall number constants
|
||||
- Point kernel Cargo.toml to local path
|
||||
- Patch tracked in `local/patches/kernel/P4-redox-syscall-numbers.patch`
|
||||
|
||||
#### K1.3 — Add kernel syscall handlers
|
||||
|
||||
**New file:** `recipes/core/kernel/source/src/syscall/cred.rs`
|
||||
|
||||
```rust
|
||||
// Credential syscall handlers
|
||||
pub fn setresuid(ruid: u32, euid: u32, suid: u32, token: &mut CleanLockToken) -> Result<usize> {
|
||||
let context_lock = context::current();
|
||||
let mut context = context_lock.write(token.token());
|
||||
|
||||
// Permission check: must be root or match current values
|
||||
if context.euid != 0 {
|
||||
if let Some(ruid) = ruid_opt { /* check ruid == current ruid/euid/suid */ }
|
||||
// ... POSIX permission model
|
||||
}
|
||||
|
||||
// Set values
|
||||
if ruid != u32::MAX { context.ruid = ruid; }
|
||||
if euid != u32::MAX { context.euid = euid; }
|
||||
if suid != u32::MAX { context.suid = suid; }
|
||||
Ok(0)
|
||||
}
|
||||
|
||||
pub fn setgroups(groups: &[u32], token: &mut CleanLockToken) -> Result<usize> {
|
||||
// Requires: euid == 0
|
||||
let context_lock = context::current();
|
||||
let mut context = context_lock.write(token.token());
|
||||
if context.euid != 0 { return Err(Error::new(EPERM)); }
|
||||
context.groups = groups.to_vec();
|
||||
Ok(0)
|
||||
}
|
||||
|
||||
pub fn getgroups(token: &mut CleanLockToken) -> Result<Vec<u32>> {
|
||||
let context_lock = context::current();
|
||||
let context = context_lock.read(token.token());
|
||||
Ok(context.groups.clone())
|
||||
}
|
||||
```
|
||||
|
||||
**Modified file:** `recipes/core/kernel/source/src/syscall/mod.rs`
|
||||
```rust
|
||||
match a {
|
||||
// ... existing arms ...
|
||||
SYS_SETRESUID => setresuid(b as u32, c as u32, d as u32, token),
|
||||
SYS_SETRESGID => setresgid(b as u32, c as u32, d as u32, token),
|
||||
SYS_GETRESUID => getresuid(UserSlice::wo(b, c)?, token),
|
||||
SYS_GETRESGID => getresgid(UserSlice::wo(b, c)?, token),
|
||||
SYS_SETUID => setuid(b as u32, token),
|
||||
SYS_SETGID => setgid(b as u32, token),
|
||||
SYS_GETUID => Ok(getuid(token)),
|
||||
SYS_GETGID => Ok(getgid(token)),
|
||||
SYS_GETEUID => Ok(geteuid(token)),
|
||||
SYS_GETEGID => Ok(getegid(token)),
|
||||
SYS_SETGROUPS => setgroups(UserSlice::ro(b, c)?, token).map(|()| 0),
|
||||
SYS_GETGROUPS => getgroups(UserSlice::wo(b, c)?, token),
|
||||
// ... existing arms ...
|
||||
}
|
||||
```
|
||||
|
||||
#### K1.4 — Wire relibc setgroups()/getgroups() through real syscalls
|
||||
|
||||
**Modified:** `recipes/core/relibc/source/src/platform/redox/mod.rs`
|
||||
```rust
|
||||
// Replace todo_skip!() stub:
|
||||
unsafe fn setgroups(size: size_t, list: *const gid_t) -> Result<()> {
|
||||
if size < 0 || size > NGROUPS_MAX { return Err(Errno(EINVAL)); }
|
||||
let groups = core::slice::from_raw_parts(list, size as usize);
|
||||
syscall::setgroups(groups)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Replace /etc/group-based getgroups:
|
||||
fn getgroups(mut list: Out<[gid_t]>) -> Result<c_int> {
|
||||
let mut buf = [0u32; NGROUPS_MAX as usize];
|
||||
let count = syscall::getgroups(&mut buf)?;
|
||||
for (i, gid) in buf[..count].iter().enumerate() {
|
||||
list[i] = *gid as gid_t;
|
||||
}
|
||||
Ok(count as c_int)
|
||||
}
|
||||
```
|
||||
|
||||
#### K1.5 — Add credential syscall stubs in redox-rt
|
||||
|
||||
**Modified:** `recipes/core/relibc/source/redox-rt/src/sys.rs`
|
||||
```rust
|
||||
pub fn setgroups(groups: &[u32]) -> Result<()> {
|
||||
unsafe {
|
||||
redox_syscall::syscall5(
|
||||
redox_syscall::SYS_SETGROUPS,
|
||||
groups.as_ptr() as usize,
|
||||
groups.len(),
|
||||
0, 0, 0,
|
||||
)
|
||||
.map(|_| ())
|
||||
.map_err(|e| Error::new(e.errno as i32))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn getgroups(buf: &mut [u32]) -> Result<usize> {
|
||||
unsafe {
|
||||
redox_syscall::syscall3(
|
||||
redox_syscall::SYS_GETGROUPS,
|
||||
buf.as_mut_ptr() as usize,
|
||||
buf.len(),
|
||||
0,
|
||||
)
|
||||
.map_err(|e| Error::new(e.errno as i32))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### K1.6 — Patch management
|
||||
|
||||
All kernel and relibc source changes must be mirrored into `local/patches/`:
|
||||
|
||||
```bash
|
||||
local/patches/
|
||||
├── kernel/
|
||||
│ ├── redox.patch # Updated symlink target
|
||||
│ ├── P4-credential-fields.patch # Context struct additions
|
||||
│ ├── P4-credential-syscalls.patch # Syscall handlers + dispatch
|
||||
│ └── P4-redox-syscall-numbers.patch # Local redox_syscall additions
|
||||
├── relibc/
|
||||
│ ├── P4-setgroups-kernel.patch # Setgroups through real syscall
|
||||
│ ├── P4-getgroups-kernel.patch # Getgroups through real syscall
|
||||
│ └── P4-redox-rt-cred-syscalls.patch # redox-rt syscall wrappers
|
||||
```
|
||||
|
||||
### Phase K2: Kernel Access Control Hardening (Week 2-3)
|
||||
|
||||
**Goal**: Enforce credential checks in kernel schemes, add proper privilege separation.
|
||||
|
||||
#### K2.1 — Enforce scheme-level credential checks
|
||||
|
||||
Each kernel scheme handler currently receives `CallerCtx { uid, gid, pid }`. Ensure consistent
|
||||
credential enforcement:
|
||||
|
||||
| Scheme | Current Check | Required Check |
|
||||
|--------|--------------|----------------|
|
||||
| `memory:` | Physical memory access → root only | ✅ Already enforced (euid==0 for phys) |
|
||||
| `irq:` | IRQ registration → root only | ✅ Already enforced |
|
||||
| `proc:` | Process inspection → caller == target OR root | 🔄 Review: ensure consistent |
|
||||
| `sys:` | System info → read-only for all | ✅ Appropriate |
|
||||
| `debug:` | Debug output → should be root-only | 🔄 Review: add check |
|
||||
| `serio:` | PS/2 device → root only | 🔄 Review: add check |
|
||||
| `event:` | Event registration → process-own only | 🔄 Review: ensure isolation |
|
||||
|
||||
#### K2.2 — Bootstrap with non-root init process
|
||||
|
||||
Currently all processes start as euid=0/egid=0. The boot sequence should:
|
||||
1. Kernel bootstrap context starts as root (euid=0, egid=0) — required for init
|
||||
2. Init (`/sbin/init`) runs as root
|
||||
3. Init drops privileges before spawning user services:
|
||||
```rust
|
||||
// In init or service manager:
|
||||
setresuid(1000, 1000, 1000); // Drop to regular user
|
||||
setgroups(&[1000, 27, 100]); // Set supplementary groups
|
||||
// Then spawn child services with restricted permissions
|
||||
```
|
||||
|
||||
#### K2.3 — Add `initgroups()` support
|
||||
|
||||
```rust
|
||||
// In relibc/src/platform/redox/mod.rs:
|
||||
fn initgroups(user: CStr, group: gid_t) -> Result<()> {
|
||||
// 1. Set primary group
|
||||
setgid(group)?;
|
||||
// 2. Parse /etc/group for supplementary groups containing this user
|
||||
let mut groups = vec![group];
|
||||
// ... iterate getgrent() to find user memberships ...
|
||||
// 3. Set supplementary groups via kernel syscall
|
||||
setgroups(&groups)?;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Phase K3: IPC Infrastructure Improvements (Week 3-5)
|
||||
|
||||
**Goal**: Complete IPC primitives needed for desktop infrastructure.
|
||||
|
||||
#### K3.1 — POSIX Message Queues (`mqueue.h`)
|
||||
|
||||
**Design decision**: Implement as a userspace scheme daemon (not kernel syscalls).
|
||||
|
||||
```
|
||||
mqd:
|
||||
├── Registers as scheme:mqueue
|
||||
├── Stores queues in memory backed by shm_open() + mmap()
|
||||
├── mq_open() → open scheme:mqueue/{name}
|
||||
├── mq_send() → write to fd
|
||||
├── mq_receive() → read from fd
|
||||
├── mq_notify() → fevent() on fd for async notification
|
||||
├── mq_close() → close fd
|
||||
└── mq_unlink() → unlink scheme:mqueue/{name}
|
||||
```
|
||||
|
||||
**Implementation:**
|
||||
- New Red Bear package: `local/recipes/system/mqueued/`
|
||||
- Relibc header: `recipes/core/relibc/source/src/header/mqueue/`
|
||||
- Recipe in `local/recipes/system/mqueued/recipe.toml`
|
||||
- Init service: `/usr/lib/init.d/50_mqueued.service`
|
||||
|
||||
#### K3.2 — SysV Message Queues (`sys/msg.h`)
|
||||
|
||||
**Design decision**: Implement as scheme daemon or on top of POSIX message queues.
|
||||
- Recommended: implement directly alongside `mqueued` using shared infrastructure.
|
||||
- Low priority — Qt/KDE do not depend on SysV msg queues.
|
||||
|
||||
#### K3.3 — UNIX Domain Sockets (`AF_UNIX` / `SOCK_STREAM`)
|
||||
|
||||
**Current state**: D-Bus uses abstract sockets on Linux. Redox uses scheme-based communication.
|
||||
- For D-Bus compatibility: `redbear-sessiond` already uses `zbus` with custom transport
|
||||
- For general `AF_UNIX`: implement as `scheme:unix` daemon backed by kernel pipe scheme
|
||||
- Priority: P3 — D-Bus is already working through scheme transport
|
||||
|
||||
#### K3.4 — Non-synthetic Interface Enumeration
|
||||
|
||||
Replace the hardcoded `loopback` + `eth0` model with live network interface enumeration:
|
||||
- Query `smolnetd` or equivalent for active interfaces
|
||||
- Expose through `getifaddrs()` properly
|
||||
- Priority: P3 — needed for NetworkManager-like functionality
|
||||
|
||||
#### K3.5 — eventfd/signalfd/timerfd → plain-source convergence
|
||||
|
||||
Current state: all three are recipe-applied patches. Goal: upstream into relibc mainline.
|
||||
- Monitor upstream relibc for equivalent implementations
|
||||
- When upstream absorbs: shrink/drop Red Bear patch chain
|
||||
- When upstream does NOT absorb after 3+ months: promote to durable Red Bear-maintained
|
||||
- See `local/docs/RELIBC-IPC-ASSESSMENT-AND-IMPROVEMENT-PLAN.md` Phase I5
|
||||
|
||||
### Phase K4: Resource Limits and Process Management (Week 4-6)
|
||||
|
||||
#### K4.1 — RLIMIT Support
|
||||
|
||||
**Decision**: Enforce resource limits in userspace, not kernel.
|
||||
- The kernel is a microkernel — resource limits are policy
|
||||
- `getrlimit()` / `setrlimit()` → libc stubs with reasonable defaults
|
||||
- Process enforcement → `procmgr` (userspace process manager) via proc: scheme
|
||||
- File descriptor limits → already enforced via `CONTEXT_MAX_FILES` in kernel
|
||||
- Memory limits → userspace `procmgr` can kill processes exceeding limits
|
||||
|
||||
```rust
|
||||
// relibc implementation (userspace, no kernel changes needed):
|
||||
fn getrlimit(resource: c_int, rlim: *mut rlimit) -> Result<()> {
|
||||
match resource {
|
||||
RLIMIT_NOFILE => { rlim.rlim_cur = 1024; rlim.rlim_max = 4096; }
|
||||
RLIMIT_NPROC => { rlim.rlim_cur = 256; rlim.rlim_max = 1024; }
|
||||
RLIMIT_AS => { rlim.rlim_cur = RLIM_INFINITY; rlim.rlim_max = RLIM_INFINITY; }
|
||||
RLIMIT_CORE => { rlim.rlim_cur = 0; rlim.rlim_max = RLIM_INFINITY; }
|
||||
// ... other resource types with reasonable defaults
|
||||
_ => return Err(Errno(EINVAL)),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
#### K4.2 — PTRACE via proc: scheme
|
||||
|
||||
`SYS_PTRACE` is not implemented as a direct syscall. The Redox model uses the `proc:` scheme
|
||||
for process inspection and manipulation:
|
||||
- Already partially implemented in `scheme/proc.rs`
|
||||
- Memory read/write through proc: scheme file operations
|
||||
- Register read/write through proc: scheme
|
||||
- Signal injection through proc: scheme
|
||||
|
||||
Improvements needed:
|
||||
- Document the proc: scheme ptrace API surface
|
||||
- Ensure all ptrace operations have proc: scheme equivalents
|
||||
- Add `PTRACE_*` constants to redox_syscall for compatibility
|
||||
|
||||
#### K4.3 — clock_settime
|
||||
|
||||
`SYS_CLOCK_SETTIME` returns ENOSYS. Implementation:
|
||||
- Add scheme write path to `/scheme/sys/update_time_offset`
|
||||
- Or implement as direct syscall for precision
|
||||
- Priority: P3 — needed for NTP synchronization
|
||||
|
||||
### Phase K5: Shutdown and Power Management (Week 5-7)
|
||||
|
||||
See `local/docs/ACPI-IMPROVEMENT-PLAN.md` for full ACPI plan. This section covers kernel-specific
|
||||
work only.
|
||||
|
||||
#### K5.1 — Hardened acpid Startup
|
||||
|
||||
- Remove panic-grade `expect` paths in kernel ACPI/AML handling
|
||||
- Add graceful fallback when ACPI tables are missing or malformed
|
||||
- See ACPI-IMPROVEMENT-PLAN.md Wave 1
|
||||
|
||||
#### K5.2 — kstop Shutdown Robustness
|
||||
|
||||
- Current: `_S5` shutdown via `kstop` event exists but gated on PCI timing
|
||||
- Required: deterministic shutdown ordering:
|
||||
1. Notify userspace services of impending shutdown
|
||||
2. Sync filesystems
|
||||
3. Power off via ACPI/FADT
|
||||
- See ACPI-IMPROVEMENT-PLAN.md Wave 2
|
||||
|
||||
#### K5.3 — Sleep State Support
|
||||
|
||||
- S3 (suspend-to-RAM) and S4 (hibernate) are not yet supported
|
||||
- Requires: kernel state serialization, device reinitialization
|
||||
- Priority: P4 — long-term, not blocking desktop
|
||||
|
||||
## 5. Dependency Chain
|
||||
|
||||
```
|
||||
Phase K1 (credential syscalls) ─────────────────────┐
|
||||
│ │
|
||||
├──► polkit compatibility │
|
||||
├──► dbus-daemon credential checks │
|
||||
├──► sudo/su user switching │
|
||||
├──► redbear-sessiond login1 handoff │
|
||||
└──► greeter/session-launch credential drop │
|
||||
│
|
||||
Phase K2 (access control) ────────────────────────────┤
|
||||
│ │
|
||||
├──► Privilege-separated boot sequence │
|
||||
├──► Scheme-level credential enforcement │
|
||||
└──► initgroups() for service launching │
|
||||
│
|
||||
Phase K3 (IPC) ───────────────────────────────────────┤
|
||||
│ │
|
||||
├──► POSIX message queues → needed by some apps │
|
||||
├──► AF_UNIX → broader D-Bus transport options │
|
||||
└──► eventfd/signalfd/timerfd → KDE/Qt runtime │
|
||||
│
|
||||
Phase K4 (limits/ptrace) ─────────────────────────────┤
|
||||
│ │
|
||||
├──► RLIMIT → systemd/logind compatibility │
|
||||
├──► PTRACE → debugging support │
|
||||
└──► clock_settime → NTP synchronization │
|
||||
▼
|
||||
Desktop infrastructure
|
||||
ready for KDE Plasma
|
||||
```
|
||||
|
||||
## 6. Integration with Existing Work
|
||||
|
||||
### 6.1 Already in Progress (do not duplicate)
|
||||
|
||||
| Area | Canonical Plan | Status |
|
||||
|------|---------------|--------|
|
||||
| IRQ / MSI-X / IOMMU | `IRQ-AND-LOWLEVEL-CONTROLLERS-ENHANCEMENT-PLAN.md` | Waves 1-6 complete, hardware validation open |
|
||||
| ACPI shutdown / power | `ACPI-IMPROVEMENT-PLAN.md` | Waves 1-2 complete, sleep states deferred |
|
||||
| relibc IPC surface | `RELIBC-IPC-ASSESSMENT-AND-IMPROVEMENT-PLAN.md` | Phases I1-I5, message queues deferred |
|
||||
| D-Bus / sessiond | `DBUS-INTEGRATION-PLAN.md` | Phase 1 complete, Phase 2 in progress |
|
||||
| Greeter / login | `GREETER-LOGIN-IMPLEMENTATION-PLAN.md` | Active, bounded proof passing |
|
||||
| Desktop path | `CONSOLE-TO-KDE-DESKTOP-PLAN.md` | Phase 1-5 model, KWin building |
|
||||
|
||||
### 6.2 This Plan Covers (uniquely)
|
||||
|
||||
| Area | This Plan | Not Covered By |
|
||||
|------|-----------|---------------|
|
||||
| Kernel credential architecture | §3, Phase K1 | Any existing plan |
|
||||
| Kernel access control hardening | §3.2, Phase K2 | Any existing plan |
|
||||
| `setgroups()` / `getgroups()` kernel implementation | Phase K1.2-K1.4 | Only stub noted elsewhere |
|
||||
| Supplementary group infrastructure | Phase K1.1 | Not covered anywhere |
|
||||
| POSIX/SysV message queues | Phase K3.1-K3.2 | Deferred in relibc-IPC plan |
|
||||
| UNIX domain sockets | Phase K3.3 | Not covered |
|
||||
| RLIMIT design decision | Phase K4.1 | Noted as gap only |
|
||||
| PTRACE via proc: scheme | Phase K4.2 | Not covered |
|
||||
| clock_settime implementation | Phase K4.3 | Noted as gap only |
|
||||
|
||||
## 7. Patch Governance
|
||||
|
||||
All kernel and relibc source changes must follow the durability policy (see `local/AGENTS.md`):
|
||||
|
||||
1. **Make changes** in `recipes/core/kernel/source/` or `recipes/core/relibc/source/`
|
||||
2. **Generate patches**: `git diff` in the source tree → `local/patches/<component>/P4-*.patch`
|
||||
3. **Wire patches** into `recipes/core/<component>/recipe.toml` patches list
|
||||
4. **Commit** patches + recipe changes before session end
|
||||
5. **Assume** source trees may be thrown away by `make distclean` or upstream refresh
|
||||
|
||||
### Patch naming convention:
|
||||
```
|
||||
local/patches/kernel/P4-credential-fields.patch
|
||||
local/patches/kernel/P4-credential-syscalls.patch
|
||||
local/patches/kernel/P4-redox-syscall-numbers.patch
|
||||
local/patches/relibc/P4-setgroups-kernel.patch
|
||||
local/patches/relibc/P4-getgroups-kernel.patch
|
||||
local/patches/relibc/P4-redox-rt-cred-syscalls.patch
|
||||
local/patches/relibc/P4-initgroups.patch
|
||||
```
|
||||
|
||||
## 8. Validation and Evidence
|
||||
|
||||
### 8.1 Build Evidence
|
||||
|
||||
| Check | Command |
|
||||
|-------|---------|
|
||||
| Kernel compiles | `make r.kernel` |
|
||||
| relibc compiles | `make r.relibc` |
|
||||
| Full OS builds | `make all CONFIG_NAME=redbear-full` |
|
||||
|
||||
### 8.2 Runtime Evidence
|
||||
|
||||
| Test | Verification |
|
||||
|------|-------------|
|
||||
| `getuid()` returns non-zero after login | `id` command in guest |
|
||||
| `setgroups()` succeeds for root | `sudo -u user id` in guest |
|
||||
| `setresuid()` properly changes euid | `su user -c 'id'` |
|
||||
| `initgroups()` populates groups | `groups` command in guest |
|
||||
| Credentials survive fork | `bash -c 'id'` |
|
||||
| Credentials dropped on exec (if SUID implemented) | TBD |
|
||||
| polkit can query credentials | `pkexec echo ok` |
|
||||
| dbus-daemon starts without errors | `dbus-monitor` |
|
||||
|
||||
### 8.3 Verification Scripts
|
||||
|
||||
Create bounded proof scripts:
|
||||
```bash
|
||||
local/scripts/test-credential-syscalls-qemu.sh # QEMU launcher
|
||||
local/scripts/test-credential-syscalls-guest.sh # In-guest checker
|
||||
```
|
||||
|
||||
## 9. References
|
||||
|
||||
- `local/docs/CONSOLE-TO-KDE-DESKTOP-PLAN.md` — Canonical comprehensive plan
|
||||
- `docs/01-REDOX-ARCHITECTURE.md` — Architecture reference
|
||||
- `local/docs/IRQ-AND-LOWLEVEL-CONTROLLERS-ENHANCEMENT-PLAN.md` — IRQ/PCI plan (sibling)
|
||||
- `local/docs/RELIBC-IPC-ASSESSMENT-AND-IMPROVEMENT-PLAN.md` — IPC surface plan (companion)
|
||||
- `local/docs/ACPI-IMPROVEMENT-PLAN.md` — ACPI/shutdown plan (sibling)
|
||||
- `local/docs/CONSOLE-TO-KDE-DESKTOP-PLAN.md` — Desktop path plan (consumer)
|
||||
- `recipes/core/kernel/source/src/syscall/mod.rs` — Syscall dispatch (primary implementation target)
|
||||
- `recipes/core/kernel/source/src/context/context.rs` — Context struct (credential fields)
|
||||
- `recipes/core/kernel/source/src/scheme/proc.rs` — Proc scheme (credential setting)
|
||||
- `recipes/core/relibc/source/src/platform/redox/mod.rs` — relibc Redox platform (credential stubs)
|
||||
- `recipes/core/relibc/source/redox-rt/src/sys.rs` — redox-rt credential primitives
|
||||
@@ -0,0 +1,165 @@
|
||||
# Red Bear OS relibc IPC Assessment and Improvement Plan
|
||||
|
||||
## Purpose
|
||||
|
||||
This document is the IPC-focused companion to
|
||||
`local/docs/CONSOLE-TO-KDE-DESKTOP-PLAN.md`.
|
||||
|
||||
Its job is to describe the current IPC-facing relibc surface honestly, especially where the active
|
||||
Red Bear build depends on recipe-applied compatibility layers rather than plain-source upstream
|
||||
relibc.
|
||||
|
||||
## Evidence model
|
||||
|
||||
This document uses the same terms as the canonical relibc plan:
|
||||
|
||||
- **plain-source-visible**
|
||||
- **recipe-applied**
|
||||
- **test-present**
|
||||
- **runtime-unrevalidated in this pass**
|
||||
|
||||
Do not collapse those into one generic "implemented" label.
|
||||
|
||||
## Current IPC inventory
|
||||
|
||||
| Surface | Plain source | Active build | Notes |
|
||||
|---|---|---|---|
|
||||
| `shm_open()` / `shm_unlink()` | yes | yes | provided through `sys_mman` in the live source tree |
|
||||
| named POSIX semaphores | no | yes | added by `P3-semaphore-fixes.patch` on top of `shm_open()` / `mmap()` |
|
||||
| `eventfd` | no | yes | added by `P3-eventfd-mod.patch` through `/scheme/event/eventfd/...` |
|
||||
| `signalfd` | no | yes | added by `P3-signalfd.patch` through `/scheme/event` plus signal-mask handling |
|
||||
| `timerfd` | no | yes | added by `P3-timerfd-relative.patch` through `/scheme/time/{clockid}` |
|
||||
| `waitid()` | no | yes | added by `P3-waitid.patch` |
|
||||
| `ifaddrs` / `net_if` support used by IPC-adjacent consumers | no | yes | added by `P3-ifaddrs-net_if.patch`; currently synthetic |
|
||||
| SysV shm (`sys/shm.h`) | no | yes | activated via `P3-sysv-shm-impl.patch` in recipe (2026-04-29) |
|
||||
| SysV sem (`sys/sem.h`) | no | yes | activated via `P3-sysv-sem-impl.patch` in recipe (2026-04-29) |
|
||||
| POSIX message queues (`mqueue.h`) | no | no | still TODO in the live source tree |
|
||||
| SysV message queues (`sys/msg.h`) | no | no | still TODO in the live source tree |
|
||||
|
||||
## Observed limitations
|
||||
|
||||
### Named POSIX semaphores
|
||||
|
||||
The active patch chain implements named semaphores by storing a `Semaphore` inside shared memory
|
||||
opened through `shm_open()` and mapped with `mmap()`. That is a useful bounded compatibility path,
|
||||
but it should still be described as a Red Bear recipe-applied layer, not a plain-source upstream
|
||||
relibc completion.
|
||||
|
||||
### fd-event APIs
|
||||
|
||||
`eventfd`, `signalfd`, and `timerfd` are present in the active build, but they are all scheme-backed
|
||||
compatibility layers:
|
||||
|
||||
- `eventfd` depends on `/scheme/event/eventfd/...`
|
||||
- `signalfd` depends on `/scheme/event` and blocks the supplied mask with `sigprocmask()`
|
||||
- `timerfd` depends on `/scheme/time/{clockid}` and currently rejects unsupported flag combinations
|
||||
|
||||
These are real compatibility layers, but they should still be described as bounded until broader
|
||||
consumer/runtime proof is recorded.
|
||||
|
||||
### Deferred SysV shm/sem work
|
||||
|
||||
SysV shm/sem carriers were activated in recipe (2026-04-29). Message queues remain deferred follow-up work.
|
||||
|
||||
### Interface enumeration used by networking-adjacent consumers
|
||||
|
||||
The current `P3-ifaddrs-net_if.patch` replaces `ENOSYS`, but it does so with a synthetic two-entry
|
||||
model:
|
||||
|
||||
- `loopback`
|
||||
- `eth0`
|
||||
|
||||
That is enough for some bounded consumers, but it should not be described as live full interface
|
||||
enumeration.
|
||||
|
||||
## Downstream pressure
|
||||
|
||||
### Qt / KDE
|
||||
|
||||
Qt and KDE remain the strongest pressure on relibc IPC semantics.
|
||||
|
||||
They do not only need headers to exist. They need the active compatibility layers to behave well
|
||||
enough for:
|
||||
|
||||
- shared-memory consumers,
|
||||
- named semaphore consumers,
|
||||
- direct `eventfd` / `timerfd` users,
|
||||
- and process-control paths such as `waitid()`.
|
||||
|
||||
### Wayland-facing consumers
|
||||
|
||||
Wayland-facing pressure is strongest on the fd-event side of the IPC story:
|
||||
|
||||
- `eventfd`
|
||||
- `signalfd`
|
||||
- `timerfd`
|
||||
|
||||
That is a different pressure profile from the SysV and named-semaphore side.
|
||||
|
||||
## Fresh verification in this pass
|
||||
|
||||
This pass revalidated the active concrete-wave IPC-facing surface through the relibc test recipe:
|
||||
|
||||
- `sys_eventfd/eventfd`
|
||||
- `sys_signalfd/signalfd`
|
||||
- `sys_timerfd/timerfd`
|
||||
- `waitid`
|
||||
- `semaphore/named`
|
||||
- `semaphore/unnamed`
|
||||
|
||||
These are bounded relibc-target proofs. They improve confidence in the active fd-event and named
|
||||
semaphore surface. SysV shm/sem are now active in the recipe (2026-04-29); message queues remain deferred.
|
||||
|
||||
## Improvement plan
|
||||
|
||||
### Phase I1 — Keep IPC claims aligned with the active build surface
|
||||
|
||||
- document patch-applied IPC layers as patch-applied
|
||||
- stop describing them as plain-source-visible unless they move into the live source tree
|
||||
- keep this doc aligned with `recipes/core/relibc/recipe.toml`
|
||||
|
||||
### Phase I2 — Decide the support contract for bounded IPC layers
|
||||
|
||||
For each major IPC area, choose one of these paths explicitly:
|
||||
|
||||
- bounded compatibility layer with honest documentation,
|
||||
- or broader semantics work with explicit proof targets.
|
||||
|
||||
This is especially important for:
|
||||
|
||||
- SysV shm,
|
||||
- SysV sem,
|
||||
- named semaphores,
|
||||
- and `ifaddrs`-driven interface discovery.
|
||||
|
||||
### Phase I3 — Add proof where current docs only imply confidence
|
||||
|
||||
Highest-value areas:
|
||||
|
||||
- the fd-event slice used by Wayland-facing consumers,
|
||||
- shared-memory and named-semaphore behavior used by Qt/KDE,
|
||||
- and the currently synthetic interface-discovery path.
|
||||
|
||||
### Phase I4 — Triage message queues directly
|
||||
|
||||
Message queues are still genuine absences, not just bounded implementations.
|
||||
|
||||
This doc should keep them visible until Red Bear either:
|
||||
|
||||
- implements them,
|
||||
- proves they are unnecessary for the intended consumer set,
|
||||
- or explicitly documents them as deferred/non-goals.
|
||||
|
||||
### Phase I5 — Converge with upstream deliberately
|
||||
|
||||
When upstream relibc absorbs equivalent IPC functionality, prefer the upstream path and shrink the
|
||||
Red Bear patch chain. Until then, keep the active IPC carrier set explicit and documented.
|
||||
|
||||
## Bottom line
|
||||
|
||||
The current Red Bear relibc IPC story is **material patch-applied compatibility, not plain-source
|
||||
completion**.
|
||||
|
||||
That is still valuable progress, but the repo should describe it honestly: several important IPC
|
||||
surfaces exist in the active build, several of them are still bounded, and message queues remain a
|
||||
real missing area.
|
||||
@@ -0,0 +1,114 @@
|
||||
# Red Bear OS Script Behavior Matrix
|
||||
|
||||
## Purpose
|
||||
|
||||
This document centralizes what the main repository scripts do and do not handle under the Red Bear
|
||||
release fork model.
|
||||
|
||||
The goal is to remove guesswork from the sync/fetch/apply/build workflow.
|
||||
|
||||
## Matrix
|
||||
|
||||
| Script | Primary role | What it handles | What it does **not** guarantee |
|
||||
|---|---|---|---|
|
||||
| `local/scripts/provision-release.sh` | Refresh top-level upstream repo state | fetches upstream, reports conflict risk, rebases repo commits, reapplies build-system release fork via `apply-patches.sh` | does not automatically solve every subsystem release fork conflict; does not by itself make upstream WIP recipes safe shipping inputs |
|
||||
| `local/scripts/apply-patches.sh` | Reapply durable Red Bear release fork | applies build-system patches, relinks recipe patch symlinks, relinks local recipe release fork into `recipes/` | does not fully rebase stale patch carriers; does not validate runtime behavior; does not decide WIP ownership for you |
|
||||
| `local/scripts/build-redbear.sh` | Build Red Bear profiles from upstream base + local release fork | applies release fork, builds cookbook if needed, validates profile naming, launches the actual image build; only allows upstream recipe immutable archived when passed `--upstream` | does not guarantee every nested upstream source tree is fresh; does not replace explicit subsystem/runtime validation |
|
||||
| `scripts/fetch-all-sources.sh` | Fetch mainline recipe source inputs for builds | downloads mainline/upstream recipe sources, reports status/preflight, and supports config-scoped fetches while leaving local release fork in place | does not mean fetched upstream WIP source is the durable shipping source of truth |
|
||||
| `local/scripts/fetch-sources.sh` | Fetch mainline recipe sources for browsing and patching | when passed `--upstream`, fetches `recipes/*` source trees so the upstream-managed side is locally available for reading, editing, and patch preparation | does not decide whether upstream should replace the local release fork |
|
||||
| `local/scripts/build-redbear-wifictl-redox.sh` | Build `redbear-wifictl` for the Redox target with the repo toolchain | prepends `prefix/x86_64-unknown-redox/sysroot/bin` to `PATH` and runs `cargo build --target x86_64-unknown-redox` in the `redbear-wifictl` crate | does not prove runtime Wi-Fi behavior; only closes the target-build environment gap for this crate |
|
||||
| `local/scripts/test-iwlwifi-driver-runtime.sh` | Exercise the bounded Intel driver lifecycle inside a target runtime | validates bounded probe/prepare/init/activate/scan/connect/disconnect/retry surfaces for `redbear-iwlwifi` on a live target runtime | does not prove real AP association, packet flow, DHCP success over Wi-Fi, or end-to-end connectivity |
|
||||
| `local/scripts/test-wifi-control-runtime.sh` | Exercise the bounded Wi-Fi control/profile lifecycle inside a target runtime | validates `/scheme/wifictl` control nodes, bounded connect/disconnect behavior, and profile-manager/runtime reporting surfaces on a live target runtime | does not prove real AP association or end-to-end connectivity |
|
||||
| `local/scripts/test-wifi-baremetal-runtime.sh` | Exercise bounded Intel Wi-Fi runtime lifecycle on a target system | validates driver probe, control probe, bounded connect/disconnect, profile-manager start/stop via the `wifi-open-bounded` profile, Wi-Fi lifecycle reporting, and writes `/tmp/redbear-phase5-wifi-capture.json` on the target | does not prove real AP association, packet flow, DHCP success over Wi-Fi, or end-to-end hardware connectivity |
|
||||
| `local/scripts/test-wifi-passthrough-qemu.sh` | Launch Red Bear with VFIO-passed Intel Wi-Fi hardware | boots a Red Bear guest with a passed-through Intel Wi-Fi PCI function, auto-runs the in-guest bounded Wi-Fi validation command, and can copy the packaged capture bundle back to a host-side file during `--check` | depends on host VFIO setup and still does not by itself guarantee real AP association or end-to-end Wi-Fi connectivity |
|
||||
| `local/scripts/test-bluetooth-runtime.sh` | Compatibility guest entrypoint for the bounded Bluetooth Battery Level slice | runs the packaged `redbear-bluetooth-battery-check` helper inside a Redox guest or target runtime | does not run on the host and does not expand the Bluetooth support claim beyond the packaged checker’s bounded scope |
|
||||
| `local/scripts/test-bluetooth-qemu.sh` | Launch or validate the bounded Bluetooth Battery Level slice in QEMU | boots `redbear-bluetooth-experimental`, auto-runs the packaged checker during `--check`, reruns it in one boot, and reruns it again after a clean reboot | does not by itself guarantee that the current QEMU proof passes; does not prove real controller bring-up, generic BLE/GATT maturity, write/notify support, or real hardware Bluetooth behavior |
|
||||
| `local/scripts/test-drm-display-runtime.sh` | Run the bounded DRM/KMS display checker in a target runtime | invokes the packaged `redbear-drm-display-check` helper for AMD or Intel, proving scheme/card reachability, connector/mode enumeration, and bounded direct modeset proof over the Red Bear DRM ioctl surface when requested | does not prove render command submission, fence semantics, or hardware rendering |
|
||||
| `local/scripts/test-amd-gpu.sh` | AMD wrapper for the bounded DRM/KMS display checker | runs `test-drm-display-runtime.sh --vendor amd` | still only display-path evidence |
|
||||
| `local/scripts/test-intel-gpu.sh` | Intel wrapper for the bounded DRM/KMS display checker | runs `test-drm-display-runtime.sh --vendor intel` | still only display-path evidence |
|
||||
| `local/scripts/test-msix-qemu.sh` | Bounded MSI-X proof in QEMU | validates that the current virtio-net guest path reaches MSI-X-capable interrupt delivery and emits normalized `IRQ_DRIVER`, `IRQ_MODE`, `IRQ_REASON`, and `IRQ_LOG` output for the bounded guest/runtime proof | does not prove broad hardware MSI-X reliability or per-device fallback behavior outside the bounded guest path |
|
||||
| `local/scripts/test-iommu-qemu.sh` | Bounded IOMMU first-use proof in QEMU | validates guest-visible AMD-Vi initialization and bounded event/drain behavior through the current `iommu` runtime path | does not prove real-hardware interrupt remapping quality or full DMA-remapping correctness |
|
||||
| `local/scripts/test-xhci-irq-qemu.sh` | Bounded xHCI interrupt-mode proof in QEMU | validates that the xHCI guest path reaches an interrupt-driven mode under the current bounded runtime checker and emits normalized `IRQ_DRIVER`, `IRQ_MODE`, `IRQ_REASON`, and `IRQ_LOG` output | does not prove full USB topology maturity or broad hardware interrupt robustness |
|
||||
| `local/scripts/test-lowlevel-controllers-qemu.sh` | Aggregate bounded low-level controller proof wrapper | runs MSI-X, xHCI IRQ, IOMMU first-use, PS/2/serio, and monotonic timer proofs in one sequence, defaulting to `redbear-mini` while automatically upgrading only the IOMMU leg to `redbear-full` because that runtime currently ships `/usr/bin/iommu`; if the required `redbear-full` image is absent, that single IOMMU leg is explicitly skipped rather than aborting the rest of the bounded wrapper | does not replace the individual proof helpers and does not prove real-hardware controller quality |
|
||||
| `local/scripts/prepare-wifi-vfio.sh` | Prepare or restore an Intel Wi-Fi PCI function for passthrough | binds a chosen PCI function to `vfio-pci` or restores it to a specified host driver | does not verify guest Wi-Fi functionality and must be used carefully on a host with a safe detachable target device |
|
||||
| `local/scripts/validate-wifi-vfio-host.sh` | Check whether a host looks ready for Wi-Fi VFIO testing | validates PCI presence, current driver, UEFI firmware, Red Bear image presence, QEMU/expect availability, VFIO module state, and IOMMU group visibility; exits non-zero when blockers are found | does not bind devices or prove the guest Wi-Fi stack works |
|
||||
| `local/scripts/run-wifi-passthrough-validation.sh` | End-to-end host-side passthrough validation wrapper | prepares VFIO, runs the packaged in-guest Wi-Fi validation path, captures the guest JSON artifact to the host, writes a host-side metadata sidecar, and restores the host driver afterwards | still depends on real VFIO/hardware support and does not itself guarantee end-to-end Wi-Fi connectivity |
|
||||
| `local/scripts/package-wifi-validation-artifacts.sh` | Bundle Wi-Fi validation evidence into one archive | packages common capture/log artifacts from bare-metal or VFIO validation runs into a single tarball | does not create missing artifacts or validate their contents |
|
||||
| `local/scripts/summarize-wifi-validation-artifacts.sh` | Summarize Wi-Fi validation evidence quickly | extracts key runtime signals from a capture JSON or packaged tarball for fast triage | does not replace full artifact review or prove runtime correctness |
|
||||
| `local/scripts/finalize-wifi-validation-run.sh` | One-shot post-run Wi-Fi triage helper | runs the packaged analyzer on a capture JSON and then packages the chosen artifacts into a tarball | still depends on a real target run having produced the capture/artifacts first |
|
||||
|
||||
The packaged companion command for those scripts is `redbear-phase5-wifi-check`, which performs the
|
||||
bounded in-target Wi-Fi lifecycle checks from inside the guest/runtime itself.
|
||||
|
||||
The packaged Bluetooth companion command is `redbear-bluetooth-battery-check`, which is intended to
|
||||
perform the bounded Bluetooth Battery Level checks from inside the guest/runtime itself, including
|
||||
repeated helper runs, daemon-restart coverage, failure-path honesty checks, and stale-state cleanup
|
||||
checks within the current slice boundary.
|
||||
|
||||
The packaged DRM display companion command is `redbear-drm-display-check`, which is intended to
|
||||
perform bounded DRM/KMS display-side checks from inside the guest/runtime itself. It now covers
|
||||
direct connector/mode enumeration and bounded direct modeset proof over the Red Bear DRM ioctl
|
||||
surface, and explicitly does not claim render or hardware-accelerated graphics completion.
|
||||
|
||||
The packaged evidence companion is `redbear-phase5-wifi-capture`, which collects the bounded driver,
|
||||
control, profile-manager, reporting, interface-listing, and scheme-state surfaces — plus `lspci`
|
||||
and active-profile contents — into a single JSON artifact.
|
||||
|
||||
The packaged link-oriented companion is `redbear-phase5-wifi-link-check`, which focuses on whether
|
||||
the target runtime is exposing interface/address/default-route signals in addition to the bounded
|
||||
Wi-Fi lifecycle state.
|
||||
|
||||
For Redox-target Rust builds of Wi-Fi components such as `redbear-wifictl`, a missing
|
||||
`x86_64-unknown-redox-gcc` on `PATH` should first be treated as a host toolchain/path issue if the
|
||||
repo already contains `prefix/x86_64-unknown-redox/sysroot/bin/x86_64-unknown-redox-gcc`.
|
||||
|
||||
## Policy Mapping
|
||||
|
||||
### Resilience / offline-first package sourcing
|
||||
|
||||
Default Red Bear behavior is local-first:
|
||||
|
||||
- use locally available package/source trees and release fork state for normal builds,
|
||||
- treat upstream immutable archived as an explicit operator action only (`--upstream`, dedicated fetch/sync),
|
||||
- do not fail policy-level expectations just because upstream network access is temporarily broken.
|
||||
|
||||
This is required so builds and recovery workflows remain operable during upstream outages or
|
||||
connectivity failures.
|
||||
|
||||
### Upstream sync
|
||||
|
||||
Use `local/scripts/provision-release.sh` when the goal is to immutable archived the top-level upstream Redox base.
|
||||
|
||||
This is a repository sync operation, not a guarantee that every local subsystem release fork is already
|
||||
rebased cleanly.
|
||||
|
||||
### Overlay reapplication
|
||||
|
||||
Use `local/scripts/apply-patches.sh` when the goal is to reconstruct Red Bear’s release fork on top of a
|
||||
fresh upstream tree.
|
||||
|
||||
This is the core durable-state recovery path.
|
||||
|
||||
### Build execution
|
||||
|
||||
Use `local/scripts/build-redbear.sh` when the goal is to build a tracked Red Bear profile from the
|
||||
current upstream base plus local release fork. Add `--upstream` only when you explicitly want Redox/upstream
|
||||
recipe sources immutable archived during that build.
|
||||
|
||||
### Source immutable archived
|
||||
|
||||
Use `scripts/fetch-all-sources.sh` and `local/scripts/fetch-sources.sh --upstream` when the goal is to
|
||||
immutable archived recipe source inputs, but do not confuse fetched upstream WIP source with a trusted shipping
|
||||
source.
|
||||
|
||||
## WIP Rule in Script Terms
|
||||
|
||||
If a subsystem is still upstream WIP, the scripts should be interpreted this way:
|
||||
|
||||
- fetching upstream WIP source is allowed and useful through the explicit upstream fetch commands or
|
||||
`--upstream` where a wrapper requires it,
|
||||
- syncing upstream WIP source is allowed and useful through the explicit upstream sync command,
|
||||
- but shipping decisions should still prefer the local release fork until upstream promotion and reevaluation happen.
|
||||
|
||||
That means “script fetched it successfully” is not the same as “Red Bear should now ship upstream’s
|
||||
WIP version directly.”
|
||||
@@ -115,41 +115,65 @@ struct Cli {
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum Commands {
|
||||
/// Install a package from the official repo or BUR
|
||||
#[command(visible_alias = "S")]
|
||||
Install { package: String },
|
||||
/// Search packages in the official repo, cached BUR, and AUR
|
||||
#[command(visible_alias = "Ss")]
|
||||
Search { query: String },
|
||||
/// Show AUR package details
|
||||
#[command(visible_alias = "Si")]
|
||||
Info { package: String },
|
||||
/// Refresh cached package metadata
|
||||
#[command(visible_alias = "Sy")]
|
||||
Sync,
|
||||
/// Refresh metadata and update installed packages
|
||||
#[command(visible_alias = "Syu")]
|
||||
SystemUpgrade,
|
||||
/// Build and install a local RBPKGBUILD directory
|
||||
#[command(visible_alias = "B")]
|
||||
Build { dir: String },
|
||||
/// Fetch a BUR recipe into the current directory
|
||||
#[command(visible_alias = "G")]
|
||||
Get { package: String },
|
||||
/// Import an AUR package into ~/.cub/recipes
|
||||
#[command(visible_alias = "Ga")]
|
||||
GetAur { package: String },
|
||||
/// Print raw PKGBUILD from AUR to stdout
|
||||
#[command(visible_alias = "Gp")]
|
||||
GetPkgbuild { package: String },
|
||||
/// Inspect an installed package or local RBPKGBUILD
|
||||
#[command(visible_alias = "x")]
|
||||
Inspect { target: String },
|
||||
/// Convert an AUR PKGBUILD into an RBPKGBUILD tree
|
||||
#[command(visible_alias = "Ia")]
|
||||
ImportAur { target: String },
|
||||
/// Update all installed packages
|
||||
#[command(visible_alias = "U")]
|
||||
UpdateAll,
|
||||
/// Remove an installed package
|
||||
#[command(visible_alias = "R")]
|
||||
Remove { package: String },
|
||||
/// List installed packages
|
||||
#[command(visible_alias = "Q")]
|
||||
QueryLocal,
|
||||
/// Show installed package details
|
||||
#[command(visible_alias = "Qi")]
|
||||
QueryInfo { package: String },
|
||||
/// List files installed by a package
|
||||
#[command(visible_alias = "Ql")]
|
||||
QueryList { package: String },
|
||||
/// Clean cub and pkg download caches
|
||||
#[command(visible_alias = "Sc")]
|
||||
CleanCache,
|
||||
/// Clean all caches including build artifacts (~/.cub/tmp/)
|
||||
#[command(visible_alias = "Scc")]
|
||||
CleanAll,
|
||||
/// Check for available updates without installing
|
||||
#[command(visible_alias = "Qu")]
|
||||
UpdatesAvailable,
|
||||
/// Find which package owns a file
|
||||
#[command(visible_alias = "Qo")]
|
||||
OwnsFile { path: String },
|
||||
}
|
||||
|
||||
struct AppContext {
|
||||
@@ -259,6 +283,8 @@ fn run_command(context: &AppContext, command: Commands, force: bool, noconfirm:
|
||||
Commands::QueryList { package } => query_local_files(context, &package)?,
|
||||
Commands::CleanCache => clean_cache()?,
|
||||
Commands::CleanAll => clean_all()?,
|
||||
Commands::UpdatesAvailable => updates_available(context)?,
|
||||
Commands::OwnsFile { path } => owns_file(context, &path)?,
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -283,7 +309,7 @@ fn rewrite_shortcut_args(
|
||||
};
|
||||
|
||||
let known = ["-S", "-B", "-G", "-R", "-Q", "-Ss", "-Si", "-Sy", "-Syu", "-Sf",
|
||||
"-Sua", "-Sc", "-Scc", "-Pi", "-Qi", "-Ql", "-Gp",
|
||||
"-Sua", "-Sc", "-Scc", "-Pi", "-Qi", "-Ql", "-Qo", "-Qu", "-Gp",
|
||||
"--import-aur"];
|
||||
|
||||
if flag.starts_with('-') && flag.len() > 2 && !flag.starts_with("--") && !known.contains(&flag) {
|
||||
@@ -348,6 +374,8 @@ fn rewrite_shortcut_args(
|
||||
"-Sc" => rewrite_flag("clean-cache"),
|
||||
"-Scc" => rewrite_flag("clean-all"),
|
||||
"-Gp" => rewrite_value("get-pkgbuild", "package"),
|
||||
"-Qu" => rewrite_flag("updates-available"),
|
||||
"-Qo" => rewrite_value("owns-file", "path"),
|
||||
_ => Ok(collected),
|
||||
}
|
||||
}
|
||||
@@ -1124,6 +1152,108 @@ fn clean_all() -> Result<(), Box<dyn std::error::Error>> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn updates_available(
|
||||
context: &AppContext,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
host_only_notice("updates-available")?;
|
||||
let library = context.open_library()?;
|
||||
let installed_packages = library.get_installed_packages()?;
|
||||
let package_state = PackageState::from_sysroot(&context.install_path)?;
|
||||
|
||||
let mut count = 0u32;
|
||||
for pkg_name in &installed_packages {
|
||||
let Some(install_state) = package_state.installed.get(pkg_name) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let remote_path = context
|
||||
.install_path
|
||||
.join(PACKAGES_HEAD_DIR)
|
||||
.join(format!("{}.pkgar_head", install_state.remote));
|
||||
if !remote_path.exists() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let local_path = context
|
||||
.install_path
|
||||
.join(PACKAGES_HEAD_DIR)
|
||||
.join(format!("{}.pkgar_head", pkg_name.as_str()));
|
||||
if !local_path.exists() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let remote_meta = fs::metadata(&remote_path)?;
|
||||
let local_meta = fs::metadata(&local_path)?;
|
||||
if remote_meta.modified()? > local_meta.modified()? {
|
||||
println!("{} (remote newer)", pkg_name.as_str());
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if count == 0 {
|
||||
println!("All packages are up to date.");
|
||||
} else {
|
||||
println!("{count} package(s) have available updates.");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn owns_file(
|
||||
context: &AppContext,
|
||||
target_path: &str,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
host_only_notice("owns-file")?;
|
||||
let library = context.open_library()?;
|
||||
let installed_packages = library.get_installed_packages()?;
|
||||
let package_state = PackageState::from_sysroot(&context.install_path)?;
|
||||
|
||||
let search = target_path.trim_start_matches('/');
|
||||
let mut found = Vec::new();
|
||||
|
||||
for pkg_name in &installed_packages {
|
||||
let Some(install_state) = package_state.installed.get(pkg_name) else {
|
||||
continue;
|
||||
};
|
||||
let Some(repo_key) = package_state.pubkeys.get(&install_state.remote) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let head_path = context
|
||||
.install_path
|
||||
.join(PACKAGES_HEAD_DIR)
|
||||
.join(format!("{}.pkgar_head", pkg_name.as_str()));
|
||||
|
||||
let Ok(mut package_file) = PackageFile::new(&head_path, &repo_key.pkey) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Ok(entries) = package_file.read_entries() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
for entry in entries {
|
||||
let Ok(relative) = entry.check_path() else {
|
||||
continue;
|
||||
};
|
||||
if relative.display().to_string() == search {
|
||||
found.push(pkg_name.as_str().to_string());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if found.is_empty() {
|
||||
println!("No package owns '{}'.", target_path);
|
||||
} else {
|
||||
for pkg in &found {
|
||||
println!("{} is owned by {}", target_path, pkg);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_pkgbuild_stdout(package: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||
validate_git_target(package)?;
|
||||
let repo_url = aur_repo_url(package);
|
||||
|
||||
@@ -16,7 +16,7 @@ use ratatui::prelude::TermionBackend;
|
||||
use ratatui::style::Style;
|
||||
use ratatui::widgets::Block;
|
||||
use ratatui::Terminal;
|
||||
use termion::event::Key;
|
||||
use termion::event::{Event, Key};
|
||||
use termion::input::TermRead;
|
||||
use termion::raw::IntoRawMode;
|
||||
use termion::screen::IntoAlternateScreen;
|
||||
@@ -27,6 +27,7 @@ const DEFAULT_TARGET: &str = "x86_64-unknown-redox";
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum View {
|
||||
Home,
|
||||
Search,
|
||||
PackageInfo,
|
||||
Install,
|
||||
@@ -47,6 +48,16 @@ pub(crate) struct QueryEntry {
|
||||
kind: QueryEntryKind,
|
||||
}
|
||||
|
||||
impl QueryEntry {
|
||||
pub(crate) fn is_recipe(&self) -> bool {
|
||||
self.kind == QueryEntryKind::Recipe
|
||||
}
|
||||
|
||||
pub(crate) fn is_package(&self) -> bool {
|
||||
self.kind == QueryEntryKind::Package
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum QueryEntryKind {
|
||||
Recipe,
|
||||
@@ -79,6 +90,8 @@ pub struct CubApp {
|
||||
action_receiver: Option<Receiver<ActionUpdate>>,
|
||||
active_action: Option<ActionKind>,
|
||||
tick: usize,
|
||||
show_help: bool,
|
||||
last_sync: Option<std::time::Instant>,
|
||||
}
|
||||
|
||||
impl CubApp {
|
||||
@@ -102,9 +115,9 @@ impl CubApp {
|
||||
search_query: String::new(),
|
||||
search_results: Vec::new(),
|
||||
selected_index: 0,
|
||||
current_view: View::Search,
|
||||
current_view: View::Home,
|
||||
status_message: if aur_client.is_some() {
|
||||
"Type a query, press Enter to search AUR, Tab to change views.".into()
|
||||
"Welcome to cub. Tab to change views, ? for help.".into()
|
||||
} else {
|
||||
"AUR offline — Query view available, Tab to change views.".into()
|
||||
},
|
||||
@@ -125,6 +138,8 @@ impl CubApp {
|
||||
action_receiver: None,
|
||||
active_action: None,
|
||||
tick: 0,
|
||||
show_help: false,
|
||||
last_sync: None,
|
||||
};
|
||||
let _ = app.refresh_query_view();
|
||||
app
|
||||
@@ -137,14 +152,14 @@ impl CubApp {
|
||||
let mut terminal = Terminal::new(backend).map_err(terminal_error)?;
|
||||
terminal.clear().map_err(terminal_error)?;
|
||||
|
||||
let mut events = termion::async_stdin().keys();
|
||||
let mut events = termion::async_stdin().events();
|
||||
self.run_inner(&mut terminal, &mut events)
|
||||
}
|
||||
|
||||
pub fn run_inner(
|
||||
&mut self,
|
||||
terminal: &mut Terminal<TermionBackend<termion::screen::AlternateScreen<termion::raw::RawTerminal<io::Stdout>>>>,
|
||||
events: &mut impl Iterator<Item = Result<Key, io::Error>>,
|
||||
events: &mut impl Iterator<Item = Result<Event, io::Error>>,
|
||||
) -> Result<(), CubError> {
|
||||
let run_result = (|| -> Result<(), CubError> {
|
||||
while self.running {
|
||||
@@ -156,7 +171,8 @@ impl CubApp {
|
||||
|
||||
if let Some(event) = events.next() {
|
||||
match event {
|
||||
Ok(key) => self.handle_key(key),
|
||||
Ok(Event::Key(key)) => self.handle_key(key),
|
||||
Ok(_) => {}
|
||||
Err(error) => {
|
||||
self.status_message = format!("Input error: {error}");
|
||||
self.running = false;
|
||||
@@ -185,31 +201,51 @@ impl CubApp {
|
||||
let layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(3),
|
||||
Constraint::Min(8),
|
||||
Constraint::Length(2),
|
||||
])
|
||||
.split(area);
|
||||
|
||||
crate::views::render_tabs(frame, layout[0], self, &theme);
|
||||
let title_bar = crate::widgets::styled_title_bar(&theme, self.tick);
|
||||
frame.render_widget(title_bar, layout[0]);
|
||||
|
||||
crate::views::render_tabs(frame, layout[1], self, &theme);
|
||||
|
||||
match self.current_view {
|
||||
View::Search => crate::views::search::render(frame, layout[1], self, &theme),
|
||||
View::PackageInfo => crate::views::info::render(frame, layout[1], self, &theme),
|
||||
View::Install => crate::views::install::render(frame, layout[1], self, &theme),
|
||||
View::Build => crate::views::build::render(frame, layout[1], self, &theme),
|
||||
View::Query => crate::views::query::render(frame, layout[1], self, &theme),
|
||||
View::Home => crate::views::home::render(frame, layout[2], self, &theme),
|
||||
View::Search => crate::views::search::render(frame, layout[2], self, &theme),
|
||||
View::PackageInfo => crate::views::info::render(frame, layout[2], self, &theme),
|
||||
View::Install => crate::views::install::render(frame, layout[2], self, &theme),
|
||||
View::Build => crate::views::build::render(frame, layout[2], self, &theme),
|
||||
View::Query => crate::views::query::render(frame, layout[2], self, &theme),
|
||||
}
|
||||
|
||||
crate::views::render_status(frame, layout[2], self, &theme);
|
||||
crate::views::render_status(frame, layout[3], self, &theme);
|
||||
|
||||
if self.show_help {
|
||||
crate::widgets::help_overlay(frame, area, &theme);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_key(&mut self, key: Key) {
|
||||
if self.show_help {
|
||||
if matches!(key, Key::Char('?') | Key::Esc) {
|
||||
self.show_help = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
match key {
|
||||
Key::Char('q') | Key::Esc => {
|
||||
self.running = false;
|
||||
return;
|
||||
}
|
||||
Key::Char('?') => {
|
||||
self.show_help = true;
|
||||
return;
|
||||
}
|
||||
Key::Char('/') => {
|
||||
self.current_view = View::Search;
|
||||
self.selected_index = self
|
||||
@@ -230,6 +266,7 @@ impl CubApp {
|
||||
}
|
||||
|
||||
match self.current_view {
|
||||
View::Home => {}
|
||||
View::Search => crate::views::search::handle_key(self, key),
|
||||
View::PackageInfo => crate::views::info::handle_key(self, key),
|
||||
View::Install => crate::views::install::handle_key(self, key),
|
||||
@@ -266,9 +303,24 @@ impl CubApp {
|
||||
self.build_running
|
||||
}
|
||||
|
||||
pub(crate) fn spinner_frame(&self) -> char {
|
||||
const FRAMES: [char; 4] = ['-', '\\', '|', '/'];
|
||||
FRAMES[self.tick % FRAMES.len()]
|
||||
pub fn last_sync_display(&self) -> String {
|
||||
match self.last_sync {
|
||||
Some(instant) => {
|
||||
let elapsed = instant.elapsed();
|
||||
if elapsed.as_secs() < 60 {
|
||||
format!("{}s ago", elapsed.as_secs())
|
||||
} else if elapsed.as_secs() < 3600 {
|
||||
format!("{}m ago", elapsed.as_secs() / 60)
|
||||
} else {
|
||||
format!("{}h ago", elapsed.as_secs() / 3600)
|
||||
}
|
||||
}
|
||||
None => "never".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn tick(&self) -> usize {
|
||||
self.tick
|
||||
}
|
||||
|
||||
pub fn search(&mut self) {
|
||||
@@ -289,6 +341,7 @@ impl CubApp {
|
||||
Ok(results) => {
|
||||
self.search_results = results;
|
||||
self.selected_index = 0;
|
||||
self.last_sync = Some(std::time::Instant::now());
|
||||
self.status_message = format!(
|
||||
"Found {} AUR package(s) for {:?}.",
|
||||
self.search_results.len(),
|
||||
@@ -405,6 +458,7 @@ impl CubApp {
|
||||
|
||||
pub fn move_selection(&mut self, delta: isize) {
|
||||
let len = match self.current_view {
|
||||
View::Home => 0,
|
||||
View::Search | View::PackageInfo => self.search_results.len(),
|
||||
View::Query => self.query_entries.len(),
|
||||
View::Install | View::Build => 0,
|
||||
@@ -471,12 +525,14 @@ impl CubApp {
|
||||
|
||||
fn cycle_view(&mut self, forward: bool) {
|
||||
self.current_view = match (self.current_view, forward) {
|
||||
(View::Home, true) => View::Search,
|
||||
(View::Search, true) => View::PackageInfo,
|
||||
(View::PackageInfo, true) => View::Install,
|
||||
(View::Install, true) => View::Build,
|
||||
(View::Build, true) => View::Query,
|
||||
(View::Query, true) => View::Search,
|
||||
(View::Search, false) => View::Query,
|
||||
(View::Query, true) => View::Home,
|
||||
(View::Home, false) => View::Query,
|
||||
(View::Search, false) => View::Home,
|
||||
(View::PackageInfo, false) => View::Search,
|
||||
(View::Install, false) => View::PackageInfo,
|
||||
(View::Build, false) => View::Install,
|
||||
|
||||
@@ -10,6 +10,11 @@ pub struct RedBearTheme {
|
||||
pub success: Color,
|
||||
pub warning: Color,
|
||||
pub error: Color,
|
||||
pub title_bg: Color,
|
||||
pub title_accent: Color,
|
||||
pub gauge_bg: Color,
|
||||
pub gauge_fg: Color,
|
||||
pub overlay_bg: Color,
|
||||
}
|
||||
|
||||
impl Default for RedBearTheme {
|
||||
@@ -23,6 +28,11 @@ impl Default for RedBearTheme {
|
||||
success: Color::Rgb(116, 199, 147),
|
||||
warning: Color::Rgb(255, 191, 87),
|
||||
error: Color::Rgb(239, 83, 80),
|
||||
title_bg: Color::Rgb(20, 20, 26),
|
||||
title_accent: Color::Rgb(200, 50, 60),
|
||||
gauge_bg: Color::Rgb(36, 36, 42),
|
||||
gauge_fg: Color::Rgb(181, 36, 48),
|
||||
overlay_bg: Color::Rgb(18, 18, 24),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,6 +46,27 @@ impl RedBearTheme {
|
||||
Style::default().bg(self.background).fg(self.muted)
|
||||
}
|
||||
|
||||
pub fn dim_style(self) -> Style {
|
||||
Style::default()
|
||||
.bg(self.background)
|
||||
.fg(self.muted)
|
||||
.add_modifier(Modifier::DIM)
|
||||
}
|
||||
|
||||
pub fn bold_style(self) -> Style {
|
||||
Style::default()
|
||||
.bg(self.background)
|
||||
.fg(self.text)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
}
|
||||
|
||||
pub fn italic_style(self) -> Style {
|
||||
Style::default()
|
||||
.bg(self.background)
|
||||
.fg(self.muted)
|
||||
.add_modifier(Modifier::ITALIC)
|
||||
}
|
||||
|
||||
pub fn status_style(self) -> Style {
|
||||
Style::default().bg(self.surface).fg(self.text)
|
||||
}
|
||||
@@ -56,4 +87,72 @@ impl RedBearTheme {
|
||||
.fg(self.text)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
}
|
||||
|
||||
pub fn success_style(self) -> Style {
|
||||
Style::default()
|
||||
.bg(self.background)
|
||||
.fg(self.success)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
}
|
||||
|
||||
pub fn warning_style(self) -> Style {
|
||||
Style::default()
|
||||
.bg(self.background)
|
||||
.fg(self.warning)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
}
|
||||
|
||||
pub fn error_style(self) -> Style {
|
||||
Style::default()
|
||||
.bg(self.background)
|
||||
.fg(self.error)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
}
|
||||
|
||||
pub fn title_bar_style(self) -> Style {
|
||||
Style::default()
|
||||
.bg(self.title_bg)
|
||||
.fg(self.title_accent)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
}
|
||||
|
||||
pub fn title_text_style(self) -> Style {
|
||||
Style::default()
|
||||
.bg(self.title_bg)
|
||||
.fg(self.text)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
}
|
||||
|
||||
pub fn gauge_style(self) -> Style {
|
||||
Style::default()
|
||||
.bg(self.gauge_bg)
|
||||
.fg(self.gauge_fg)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
}
|
||||
|
||||
pub fn overlay_style(self) -> Style {
|
||||
Style::default()
|
||||
.bg(self.overlay_bg)
|
||||
.fg(self.text)
|
||||
}
|
||||
|
||||
pub fn overlay_border_style(self) -> Style {
|
||||
Style::default()
|
||||
.fg(self.accent)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
}
|
||||
|
||||
pub fn overlay_title_style(self) -> Style {
|
||||
Style::default()
|
||||
.bg(self.accent)
|
||||
.fg(self.text)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
}
|
||||
|
||||
pub fn logo_accent_style(self) -> Style {
|
||||
Style::default()
|
||||
.bg(self.background)
|
||||
.fg(self.accent)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::layout::{Constraint, Direction, Layout, Rect};
|
||||
use ratatui::style::{Modifier, Style};
|
||||
use ratatui::text::{Line, Span};
|
||||
use ratatui::widgets::{Block, BorderType, Borders, Gauge, Paragraph, Wrap};
|
||||
use termion::event::Key;
|
||||
|
||||
use crate::app::CubApp;
|
||||
@@ -6,13 +9,86 @@ use crate::theme::RedBearTheme;
|
||||
use crate::widgets;
|
||||
|
||||
pub fn render(frame: &mut ratatui::Frame<'_>, area: Rect, app: &CubApp, theme: &RedBearTheme) {
|
||||
let title = if app.build_running() {
|
||||
format!("Build {} running", app.spinner_frame())
|
||||
let running = app.build_running();
|
||||
let braille = crate::widgets::braille_frame(app.tick());
|
||||
|
||||
let title = if running {
|
||||
format!(" Build {} running ", braille)
|
||||
} else {
|
||||
"Build Progress".to_string()
|
||||
" Build Progress ".to_string()
|
||||
};
|
||||
let body = widgets::log_paragraph(app.build_log(), &title, theme, true);
|
||||
frame.render_widget(body, area);
|
||||
|
||||
let layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Min(4),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(2),
|
||||
])
|
||||
.split(area);
|
||||
|
||||
let log_block = Block::default()
|
||||
.title(title)
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.style(theme.base_style())
|
||||
.border_style(if running {
|
||||
theme.focused_border_style()
|
||||
} else {
|
||||
theme.border_style()
|
||||
});
|
||||
|
||||
let log_lines = widgets::semantic_log_lines(app.build_log(), theme);
|
||||
let body = Paragraph::new(log_lines)
|
||||
.wrap(Wrap { trim: false })
|
||||
.block(log_block);
|
||||
frame.render_widget(body, layout[0]);
|
||||
|
||||
if running {
|
||||
let pulse = ((app.tick() as f64 * 0.03).sin() * 0.5 + 0.5).clamp(0.0, 1.0);
|
||||
let gauge_label = format!("{} building...", braille);
|
||||
let gauge = Gauge::default()
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.border_style(theme.border_style()),
|
||||
)
|
||||
.gauge_style(theme.gauge_style())
|
||||
.ratio(pulse)
|
||||
.label(gauge_label);
|
||||
frame.render_widget(gauge, layout[1]);
|
||||
|
||||
let hints = Paragraph::new(Line::from(vec![
|
||||
Span::styled(
|
||||
" b",
|
||||
Style::default().fg(theme.accent).add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::styled(" retry ", theme.dim_style()),
|
||||
Span::styled(
|
||||
"←",
|
||||
Style::default().fg(theme.accent).add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::styled(" back", theme.dim_style()),
|
||||
]))
|
||||
.style(theme.base_style());
|
||||
frame.render_widget(hints, layout[2]);
|
||||
} else {
|
||||
let hints = Paragraph::new(Line::from(vec![
|
||||
Span::styled(
|
||||
" b",
|
||||
Style::default().fg(theme.accent).add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::styled(" build ", theme.dim_style()),
|
||||
Span::styled(
|
||||
"←",
|
||||
Style::default().fg(theme.accent).add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::styled(" back", theme.dim_style()),
|
||||
]))
|
||||
.style(theme.base_style());
|
||||
frame.render_widget(hints, layout[2]);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_key(app: &mut CubApp, key: Key) {
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
use ratatui::layout::{Constraint, Direction, Layout, Rect};
|
||||
use ratatui::style::{Modifier, Style};
|
||||
use ratatui::text::{Line, Span, Text};
|
||||
use ratatui::widgets::{Paragraph, Wrap};
|
||||
use termion::event::Key;
|
||||
|
||||
use crate::app::CubApp;
|
||||
use crate::theme::RedBearTheme;
|
||||
use crate::widgets;
|
||||
|
||||
pub fn render(frame: &mut ratatui::Frame<'_>, area: Rect, app: &CubApp, theme: &RedBearTheme) {
|
||||
let layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Length(14),
|
||||
Constraint::Min(4),
|
||||
Constraint::Length(2),
|
||||
])
|
||||
.split(area);
|
||||
|
||||
let logo = build_logo(theme);
|
||||
let logo_block = widgets::block(" Dashboard", theme, true);
|
||||
let logo_para = Paragraph::new(logo)
|
||||
.block(logo_block)
|
||||
.wrap(Wrap { trim: false });
|
||||
frame.render_widget(logo_para, layout[0]);
|
||||
|
||||
let stats = build_stats(app, theme);
|
||||
let stats_block = widgets::block(" Statistics", theme, false);
|
||||
let stats_para = Paragraph::new(stats)
|
||||
.block(stats_block)
|
||||
.wrap(Wrap { trim: false });
|
||||
frame.render_widget(stats_para, layout[1]);
|
||||
|
||||
let hints = Paragraph::new("Tab next view • ? help")
|
||||
.style(theme.dim_style());
|
||||
frame.render_widget(hints, layout[2]);
|
||||
}
|
||||
|
||||
pub fn handle_key(_app: &mut CubApp, _key: Key) {
|
||||
// Home view has no special key handling — navigation is global
|
||||
}
|
||||
|
||||
fn build_logo(theme: &RedBearTheme) -> Text<'_> {
|
||||
let accent = Style::default()
|
||||
.bg(theme.background)
|
||||
.fg(theme.accent)
|
||||
.add_modifier(Modifier::BOLD);
|
||||
let text = Style::default()
|
||||
.bg(theme.background)
|
||||
.fg(theme.text);
|
||||
let dim = Style::default()
|
||||
.bg(theme.background)
|
||||
.fg(theme.muted);
|
||||
|
||||
let lines = vec![
|
||||
Line::from(vec![
|
||||
Span::styled(" ___ ", accent),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::styled(" / \\ ", accent),
|
||||
Span::styled(" Red Bear OS", text),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::styled(" / ◆ \\ ", accent),
|
||||
Span::styled(" Package Builder", dim),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::styled(" / \\ ", accent),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::styled(" / \\ ", accent),
|
||||
Span::styled(" v0.2.3", dim),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::styled(" /___________\\ ", accent),
|
||||
]),
|
||||
Line::from(""),
|
||||
Line::from(vec![
|
||||
Span::styled(" ◆ ", accent),
|
||||
Span::styled("cub", text),
|
||||
Span::styled(" — AUR helper + local recipe build system", dim),
|
||||
]),
|
||||
];
|
||||
Text::from(lines)
|
||||
}
|
||||
|
||||
fn build_stats(app: &CubApp, theme: &RedBearTheme) -> Text<'static> {
|
||||
let label = Style::default()
|
||||
.bg(theme.background)
|
||||
.fg(theme.muted);
|
||||
let value = Style::default()
|
||||
.bg(theme.background)
|
||||
.fg(theme.text)
|
||||
.add_modifier(Modifier::BOLD);
|
||||
let accent_val = Style::default()
|
||||
.bg(theme.background)
|
||||
.fg(theme.accent)
|
||||
.add_modifier(Modifier::BOLD);
|
||||
|
||||
let store_path = app.store.root_dir.display().to_string();
|
||||
let recipe_count = app.query_entries().iter().filter(|e| e.is_recipe()).count();
|
||||
let package_count = app.query_entries().iter().filter(|e| e.is_package()).count();
|
||||
let last_sync = app.last_sync_display();
|
||||
|
||||
let lines = vec![
|
||||
Line::from(vec![
|
||||
Span::styled(" Store ", label),
|
||||
Span::styled(store_path, value),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::styled(" Recipes ", label),
|
||||
Span::styled(format!("{recipe_count}"), accent_val),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::styled(" Packages ", label),
|
||||
Span::styled(format!("{package_count}"), accent_val),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::styled(" Last sync ", label),
|
||||
Span::styled(last_sync, value),
|
||||
]),
|
||||
];
|
||||
Text::from(lines)
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
use ratatui::layout::{Constraint, Direction, Layout, Rect};
|
||||
use ratatui::text::Line;
|
||||
use ratatui::style::{Modifier, Style};
|
||||
use ratatui::text::{Line, Span};
|
||||
use ratatui::widgets::{List, ListItem, Paragraph};
|
||||
use termion::event::Key;
|
||||
|
||||
@@ -14,36 +15,81 @@ pub fn render(frame: &mut ratatui::Frame<'_>, area: Rect, app: &CubApp, theme: &
|
||||
.split(area);
|
||||
|
||||
let Some(package) = app.selected_package() else {
|
||||
let empty = Paragraph::new("Select a package in Search to inspect its details.")
|
||||
.style(theme.base_style())
|
||||
.block(widgets::block("Package Info", theme, true));
|
||||
let empty = Paragraph::new(Line::from(Span::styled(
|
||||
"Select a package in Search to inspect its details.",
|
||||
theme.italic_style(),
|
||||
)))
|
||||
.style(theme.base_style())
|
||||
.block(widgets::block(" Package Info", theme, true));
|
||||
frame.render_widget(empty, layout[0]);
|
||||
return;
|
||||
};
|
||||
|
||||
let items = vec![
|
||||
ListItem::new(Line::from(format!("Name: {}", package.name))),
|
||||
ListItem::new(Line::from(format!("Version: {}", package.version))),
|
||||
ListItem::new(Line::from(format!("Description: {}", package.description))),
|
||||
ListItem::new(Line::from(format!(
|
||||
"Depends: {}",
|
||||
join_or_none(&package.depends)
|
||||
))),
|
||||
ListItem::new(Line::from(format!(
|
||||
"Make depends: {}",
|
||||
join_or_none(&package.makedepends)
|
||||
))),
|
||||
ListItem::new(Line::from(format!(
|
||||
"Optional depends: {}",
|
||||
join_or_none(&package.optdepends)
|
||||
))),
|
||||
ListItem::new(Line::from(vec![
|
||||
Span::styled(" Name ", theme.dim_style()),
|
||||
Span::styled(
|
||||
package.name.clone(),
|
||||
Style::default()
|
||||
.fg(theme.text)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
])),
|
||||
ListItem::new(Line::from(vec![
|
||||
Span::styled(" Version ", theme.dim_style()),
|
||||
Span::styled(package.version.clone(), theme.success_style()),
|
||||
])),
|
||||
ListItem::new(Line::from(vec![
|
||||
Span::styled(" Description ", theme.dim_style()),
|
||||
Span::styled(
|
||||
package.description.clone(),
|
||||
theme.italic_style(),
|
||||
),
|
||||
])),
|
||||
ListItem::new(Line::from(vec![
|
||||
Span::styled(" Depends ", theme.dim_style()),
|
||||
Span::styled(
|
||||
join_or_none(&package.depends),
|
||||
Style::default().fg(theme.text),
|
||||
),
|
||||
])),
|
||||
ListItem::new(Line::from(vec![
|
||||
Span::styled(" Make depends ", theme.dim_style()),
|
||||
Span::styled(
|
||||
join_or_none(&package.makedepends),
|
||||
Style::default().fg(theme.warning),
|
||||
),
|
||||
])),
|
||||
ListItem::new(Line::from(vec![
|
||||
Span::styled(" Opt depends ", theme.dim_style()),
|
||||
Span::styled(
|
||||
join_or_none(&package.optdepends),
|
||||
Style::default().fg(theme.muted),
|
||||
),
|
||||
])),
|
||||
];
|
||||
|
||||
let list = List::new(items).block(widgets::block("Package Info", theme, true));
|
||||
let list = List::new(items).block(widgets::block(" Package Info", theme, true));
|
||||
frame.render_widget(list, layout[0]);
|
||||
|
||||
let hints = Paragraph::new("i install • b build local recipe • ← return to search")
|
||||
.style(theme.muted_style());
|
||||
let hints = Paragraph::new(Line::from(vec![
|
||||
Span::styled(
|
||||
" i",
|
||||
Style::default().fg(theme.accent).add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::styled(" install ", theme.dim_style()),
|
||||
Span::styled(
|
||||
"b",
|
||||
Style::default().fg(theme.accent).add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::styled(" build ", theme.dim_style()),
|
||||
Span::styled(
|
||||
"←",
|
||||
Style::default().fg(theme.accent).add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::styled(" back", theme.dim_style()),
|
||||
]))
|
||||
.style(theme.base_style());
|
||||
frame.render_widget(hints, layout[1]);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::layout::{Constraint, Direction, Layout, Rect};
|
||||
use ratatui::style::{Modifier, Style};
|
||||
use ratatui::text::{Line, Span};
|
||||
use ratatui::widgets::{Block, BorderType, Borders, Gauge, Paragraph, Wrap};
|
||||
use termion::event::Key;
|
||||
|
||||
use crate::app::CubApp;
|
||||
@@ -6,13 +9,86 @@ use crate::theme::RedBearTheme;
|
||||
use crate::widgets;
|
||||
|
||||
pub fn render(frame: &mut ratatui::Frame<'_>, area: Rect, app: &CubApp, theme: &RedBearTheme) {
|
||||
let title = if app.install_running() {
|
||||
format!("Install {} running", app.spinner_frame())
|
||||
let running = app.install_running();
|
||||
let braille = crate::widgets::braille_frame(app.tick());
|
||||
|
||||
let title = if running {
|
||||
format!(" Install {} running ", braille)
|
||||
} else {
|
||||
"Install Progress".to_string()
|
||||
" Install Progress ".to_string()
|
||||
};
|
||||
let body = widgets::log_paragraph(app.install_log(), &title, theme, true);
|
||||
frame.render_widget(body, area);
|
||||
|
||||
let layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Min(4),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(2),
|
||||
])
|
||||
.split(area);
|
||||
|
||||
let log_block = Block::default()
|
||||
.title(title)
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.style(theme.base_style())
|
||||
.border_style(if running {
|
||||
theme.focused_border_style()
|
||||
} else {
|
||||
theme.border_style()
|
||||
});
|
||||
|
||||
let log_lines = widgets::semantic_log_lines(app.install_log(), theme);
|
||||
let body = Paragraph::new(log_lines)
|
||||
.wrap(Wrap { trim: false })
|
||||
.block(log_block);
|
||||
frame.render_widget(body, layout[0]);
|
||||
|
||||
if running {
|
||||
let pulse = ((app.tick() as f64 * 0.03).sin() * 0.5 + 0.5).clamp(0.0, 1.0);
|
||||
let gauge_label = format!("{} installing...", braille);
|
||||
let gauge = Gauge::default()
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.border_style(theme.border_style()),
|
||||
)
|
||||
.gauge_style(theme.gauge_style())
|
||||
.ratio(pulse)
|
||||
.label(gauge_label);
|
||||
frame.render_widget(gauge, layout[1]);
|
||||
|
||||
let hints = Paragraph::new(Line::from(vec![
|
||||
Span::styled(
|
||||
" i",
|
||||
Style::default().fg(theme.accent).add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::styled(" retry ", theme.dim_style()),
|
||||
Span::styled(
|
||||
"←",
|
||||
Style::default().fg(theme.accent).add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::styled(" back", theme.dim_style()),
|
||||
]))
|
||||
.style(theme.base_style());
|
||||
frame.render_widget(hints, layout[2]);
|
||||
} else {
|
||||
let hints = Paragraph::new(Line::from(vec![
|
||||
Span::styled(
|
||||
" i",
|
||||
Style::default().fg(theme.accent).add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::styled(" install ", theme.dim_style()),
|
||||
Span::styled(
|
||||
"←",
|
||||
Style::default().fg(theme.accent).add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::styled(" back", theme.dim_style()),
|
||||
]))
|
||||
.style(theme.base_style());
|
||||
frame.render_widget(hints, layout[2]);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_key(app: &mut CubApp, key: Key) {
|
||||
|
||||
@@ -1,19 +1,27 @@
|
||||
pub mod build;
|
||||
pub mod home;
|
||||
pub mod info;
|
||||
pub mod install;
|
||||
pub mod query;
|
||||
pub mod search;
|
||||
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::text::Line;
|
||||
use ratatui::widgets::{Paragraph, Tabs};
|
||||
use ratatui::style::{Modifier, Style};
|
||||
use ratatui::text::{Line, Span};
|
||||
use ratatui::widgets::{Block, BorderType, Borders, Paragraph, Tabs};
|
||||
|
||||
use crate::app::{CubApp, View};
|
||||
use crate::theme::RedBearTheme;
|
||||
use crate::widgets;
|
||||
|
||||
pub fn render_tabs(frame: &mut ratatui::Frame<'_>, area: Rect, app: &CubApp, theme: &RedBearTheme) {
|
||||
pub fn render_tabs(
|
||||
frame: &mut ratatui::Frame<'_>,
|
||||
area: Rect,
|
||||
app: &CubApp,
|
||||
theme: &RedBearTheme,
|
||||
) {
|
||||
let titles = [
|
||||
View::Home,
|
||||
View::Search,
|
||||
View::PackageInfo,
|
||||
View::Install,
|
||||
@@ -21,24 +29,42 @@ pub fn render_tabs(frame: &mut ratatui::Frame<'_>, area: Rect, app: &CubApp, the
|
||||
View::Query,
|
||||
]
|
||||
.into_iter()
|
||||
.map(view_title)
|
||||
.map(Line::from)
|
||||
.map(|v| {
|
||||
let title = view_title(v);
|
||||
if v == app.current_view {
|
||||
Line::from(Span::styled(
|
||||
title,
|
||||
Style::default()
|
||||
.fg(theme.text)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
))
|
||||
} else {
|
||||
Line::from(Span::styled(title, Style::default().fg(theme.muted)))
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let selected = match app.current_view {
|
||||
View::Search => 0,
|
||||
View::PackageInfo => 1,
|
||||
View::Install => 2,
|
||||
View::Build => 3,
|
||||
View::Query => 4,
|
||||
View::Home => 0,
|
||||
View::Search => 1,
|
||||
View::PackageInfo => 2,
|
||||
View::Install => 3,
|
||||
View::Build => 4,
|
||||
View::Query => 5,
|
||||
};
|
||||
|
||||
let tab_block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.style(Style::default().bg(theme.background).fg(theme.text))
|
||||
.border_style(theme.focused_border_style());
|
||||
|
||||
let tabs = Tabs::new(titles)
|
||||
.block(widgets::block("cub-tui", theme, true))
|
||||
.block(tab_block)
|
||||
.style(theme.base_style())
|
||||
.highlight_style(theme.selected_style())
|
||||
.select(selected)
|
||||
.divider("|");
|
||||
.divider("│");
|
||||
frame.render_widget(tabs, area);
|
||||
}
|
||||
|
||||
@@ -48,16 +74,33 @@ pub fn render_status(
|
||||
app: &CubApp,
|
||||
theme: &RedBearTheme,
|
||||
) {
|
||||
let text = format!(
|
||||
"{} [q/esc quit] [Tab views] [/ search]",
|
||||
app.status_message
|
||||
);
|
||||
let status = Paragraph::new(text).style(theme.status_style());
|
||||
let spinner = widgets::braille_frame(app.tick());
|
||||
let text = Line::from(vec![
|
||||
Span::styled(
|
||||
format!(" {} ", spinner),
|
||||
Style::default()
|
||||
.fg(theme.accent)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::styled(
|
||||
format!(" {} ", app.status_message),
|
||||
Style::default().bg(theme.surface).fg(theme.text),
|
||||
),
|
||||
Span::styled(
|
||||
" [q quit] [Tab views] [/ search] [? help] ",
|
||||
Style::default()
|
||||
.bg(theme.surface)
|
||||
.fg(theme.muted)
|
||||
.add_modifier(Modifier::DIM),
|
||||
),
|
||||
]);
|
||||
let status = Paragraph::new(text).style(Style::default().bg(theme.surface));
|
||||
frame.render_widget(status, area);
|
||||
}
|
||||
|
||||
fn view_title(view: View) -> &'static str {
|
||||
match view {
|
||||
View::Home => " Home ",
|
||||
View::Search => " Search ",
|
||||
View::PackageInfo => " Info ",
|
||||
View::Install => " Install ",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use ratatui::layout::{Constraint, Direction, Layout, Rect};
|
||||
use ratatui::style::Style;
|
||||
use ratatui::text::{Line, Span};
|
||||
use ratatui::widgets::{List, ListItem, ListState};
|
||||
use termion::event::Key;
|
||||
|
||||
@@ -13,11 +15,25 @@ pub fn render(frame: &mut ratatui::Frame<'_>, area: Rect, app: &CubApp, theme: &
|
||||
.split(area);
|
||||
|
||||
let items = if app.query_entries().is_empty() {
|
||||
vec![ListItem::new("No local recipes or cached pkgars.")]
|
||||
vec![ListItem::new(Line::from(Span::styled(
|
||||
"No local recipes or cached pkgars.",
|
||||
theme.italic_style(),
|
||||
)))]
|
||||
} else {
|
||||
app.query_entries()
|
||||
.iter()
|
||||
.map(|entry| ListItem::new(entry.title.clone()))
|
||||
.map(|entry| {
|
||||
let icon = if entry.is_recipe() { "◇" } else { "◈" };
|
||||
let style = if entry.is_recipe() {
|
||||
Style::default().fg(theme.text)
|
||||
} else {
|
||||
Style::default().fg(theme.success)
|
||||
};
|
||||
ListItem::new(Line::from(vec![
|
||||
Span::styled(format!("{} ", icon), Style::default().fg(theme.accent)),
|
||||
Span::styled(entry.title.clone(), style),
|
||||
]))
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
|
||||
@@ -30,12 +46,12 @@ pub fn render(frame: &mut ratatui::Frame<'_>, area: Rect, app: &CubApp, theme: &
|
||||
}
|
||||
|
||||
let list = List::new(items)
|
||||
.block(widgets::block("Local Query", theme, true))
|
||||
.block(widgets::block(" Local Query", theme, true))
|
||||
.highlight_style(theme.selected_style())
|
||||
.highlight_symbol("» ");
|
||||
.highlight_symbol("◆ ");
|
||||
frame.render_stateful_widget(list, layout[0], &mut state);
|
||||
|
||||
let details = widgets::log_paragraph(app.query_details(), "Details", theme, false);
|
||||
let details = widgets::log_paragraph(app.query_details(), " Details", theme, false);
|
||||
frame.render_widget(details, layout[1]);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use ratatui::layout::{Constraint, Direction, Layout, Rect};
|
||||
use ratatui::text::Line;
|
||||
use ratatui::style::{Modifier, Style};
|
||||
use ratatui::text::{Line, Span};
|
||||
use ratatui::widgets::{List, ListItem, ListState, Paragraph};
|
||||
use termion::event::Key;
|
||||
|
||||
@@ -17,15 +18,31 @@ pub fn render(frame: &mut ratatui::Frame<'_>, area: Rect, app: &CubApp, theme: &
|
||||
])
|
||||
.split(area);
|
||||
|
||||
let search_bar = Paragraph::new(app.search_query.as_str())
|
||||
.style(theme.base_style())
|
||||
.block(widgets::block("AUR Search", theme, true));
|
||||
let search_bar = Paragraph::new(Line::from(vec![
|
||||
Span::styled(
|
||||
" ⌕ ",
|
||||
Style::default()
|
||||
.fg(theme.accent)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::styled(
|
||||
app.search_query.clone(),
|
||||
Style::default().fg(theme.text),
|
||||
),
|
||||
Span::styled(
|
||||
"▎",
|
||||
Style::default().fg(theme.accent),
|
||||
),
|
||||
]))
|
||||
.style(theme.base_style())
|
||||
.block(widgets::block(" AUR Search", theme, true));
|
||||
frame.render_widget(search_bar, layout[0]);
|
||||
|
||||
let items = if app.search_results.is_empty() {
|
||||
vec![ListItem::new(
|
||||
vec![ListItem::new(Line::from(Span::styled(
|
||||
"No AUR results yet. Type a query and press Enter.",
|
||||
)]
|
||||
theme.italic_style(),
|
||||
)))]
|
||||
} else {
|
||||
app.search_results
|
||||
.iter()
|
||||
@@ -36,8 +53,24 @@ pub fn render(frame: &mut ratatui::Frame<'_>, area: Rect, app: &CubApp, theme: &
|
||||
package.description.as_str()
|
||||
};
|
||||
ListItem::new(vec![
|
||||
Line::from(format!("{} {}", package.name, package.version)),
|
||||
Line::from(description.to_string()),
|
||||
Line::from(vec![
|
||||
Span::styled(
|
||||
format!("{} ", package.name),
|
||||
Style::default()
|
||||
.fg(theme.text)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::styled(
|
||||
package.version.clone(),
|
||||
Style::default()
|
||||
.fg(theme.muted)
|
||||
.add_modifier(Modifier::DIM),
|
||||
),
|
||||
]),
|
||||
Line::from(Span::styled(
|
||||
format!(" {}", description),
|
||||
theme.italic_style(),
|
||||
)),
|
||||
])
|
||||
})
|
||||
.collect()
|
||||
@@ -52,13 +85,39 @@ pub fn render(frame: &mut ratatui::Frame<'_>, area: Rect, app: &CubApp, theme: &
|
||||
}
|
||||
|
||||
let results = List::new(items)
|
||||
.block(widgets::block("Results", theme, false))
|
||||
.block(widgets::block(" Results", theme, false))
|
||||
.highlight_style(theme.selected_style())
|
||||
.highlight_symbol("» ");
|
||||
.highlight_symbol("◆ ");
|
||||
frame.render_stateful_widget(results, layout[1], &mut state);
|
||||
|
||||
let hints = Paragraph::new("Enter search • ↑/↓ move • i info • I install • b build")
|
||||
.style(theme.muted_style());
|
||||
let hints = Paragraph::new(Line::from(vec![
|
||||
Span::styled(
|
||||
" Enter",
|
||||
Style::default().fg(theme.accent).add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::styled(" search ", theme.dim_style()),
|
||||
Span::styled(
|
||||
"↑/↓",
|
||||
Style::default().fg(theme.accent).add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::styled(" move ", theme.dim_style()),
|
||||
Span::styled(
|
||||
"i",
|
||||
Style::default().fg(theme.accent).add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::styled(" info ", theme.dim_style()),
|
||||
Span::styled(
|
||||
"I",
|
||||
Style::default().fg(theme.accent).add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::styled(" install ", theme.dim_style()),
|
||||
Span::styled(
|
||||
"b",
|
||||
Style::default().fg(theme.accent).add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::styled(" build", theme.dim_style()),
|
||||
]))
|
||||
.style(theme.base_style());
|
||||
frame.render_widget(hints, layout[2]);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use ratatui::style::Style;
|
||||
use ratatui::text::Text;
|
||||
use ratatui::widgets::{Block, Borders, Paragraph, Wrap};
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::{Modifier, Style};
|
||||
use ratatui::text::{Line, Span, Text};
|
||||
use ratatui::widgets::{Block, BorderType, Borders, Clear, Gauge, Paragraph, Wrap};
|
||||
|
||||
use crate::theme::RedBearTheme;
|
||||
|
||||
@@ -14,6 +15,7 @@ pub fn block<'a>(title: &'a str, theme: &RedBearTheme, focused: bool) -> Block<'
|
||||
Block::default()
|
||||
.title(title)
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.style(Style::default().bg(theme.background).fg(theme.text))
|
||||
.border_style(border_style)
|
||||
}
|
||||
@@ -35,3 +37,184 @@ pub fn log_paragraph<'a>(
|
||||
.style(theme.base_style())
|
||||
.block(block(title, theme, focused))
|
||||
}
|
||||
|
||||
pub fn styled_title_bar(theme: &RedBearTheme, tick: usize) -> Paragraph<'_> {
|
||||
let braille = braille_frame(tick);
|
||||
let title = Line::from(vec![
|
||||
Span::styled(
|
||||
" ◆ ",
|
||||
Style::default()
|
||||
.bg(theme.accent)
|
||||
.fg(theme.text)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::styled(
|
||||
"RED BEAR OS",
|
||||
Style::default()
|
||||
.bg(theme.title_bg)
|
||||
.fg(theme.title_accent)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::styled(
|
||||
" cub ",
|
||||
Style::default()
|
||||
.bg(theme.title_bg)
|
||||
.fg(theme.text)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::styled(
|
||||
format!(" {} ", braille),
|
||||
Style::default().bg(theme.title_bg).fg(theme.muted),
|
||||
),
|
||||
]);
|
||||
Paragraph::new(title).style(Style::default().bg(theme.title_bg))
|
||||
}
|
||||
|
||||
pub fn progress_block<'a>(
|
||||
title: &'a str,
|
||||
theme: &RedBearTheme,
|
||||
focused: bool,
|
||||
running: bool,
|
||||
tick: usize,
|
||||
) -> (Block<'a>, Option<Gauge<'a>>) {
|
||||
let block = block(title, theme, focused);
|
||||
if running {
|
||||
let pulse = ((tick as f64 * 0.03).sin() * 0.5 + 0.5).clamp(0.0, 1.0);
|
||||
let gauge = Gauge::default()
|
||||
.gauge_style(theme.gauge_style())
|
||||
.ratio(pulse)
|
||||
.label("");
|
||||
(block, Some(gauge))
|
||||
} else {
|
||||
(block, None)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn help_overlay(frame: &mut ratatui::Frame<'_>, area: Rect, theme: &RedBearTheme) {
|
||||
let help_width = 52u16;
|
||||
let help_height = 22u16;
|
||||
let x = area
|
||||
.width
|
||||
.saturating_sub(help_width)
|
||||
.saturating_sub(2)
|
||||
/ 2;
|
||||
let y = area
|
||||
.height
|
||||
.saturating_sub(help_height)
|
||||
.saturating_sub(2)
|
||||
/ 2;
|
||||
let overlay_area = Rect::new(
|
||||
x,
|
||||
y,
|
||||
help_width.min(area.width.saturating_sub(4)),
|
||||
help_height.min(area.height.saturating_sub(4)),
|
||||
);
|
||||
|
||||
frame.render_widget(Clear, overlay_area);
|
||||
|
||||
let keybinds = vec![
|
||||
Line::from(""),
|
||||
Line::from(vec![Span::styled(
|
||||
" KEYBINDINGS",
|
||||
theme.overlay_title_style(),
|
||||
)]),
|
||||
Line::from(""),
|
||||
Line::from(vec![
|
||||
Span::styled(" Tab / Shift+Tab ", theme.bold_style()),
|
||||
Span::styled("Cycle views", theme.muted_style()),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::styled(" / ", theme.bold_style()),
|
||||
Span::styled("Focus search", theme.muted_style()),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::styled(" ↑/k ↓/j ", theme.bold_style()),
|
||||
Span::styled("Move selection", theme.muted_style()),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::styled(" Enter ", theme.bold_style()),
|
||||
Span::styled("Execute search", theme.muted_style()),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::styled(" i ", theme.bold_style()),
|
||||
Span::styled("Open package info", theme.muted_style()),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::styled(" I ", theme.bold_style()),
|
||||
Span::styled("Install selected", theme.muted_style()),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::styled(" b ", theme.bold_style()),
|
||||
Span::styled("Build local recipe", theme.muted_style()),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::styled(" r ", theme.bold_style()),
|
||||
Span::styled("Refresh query view", theme.muted_style()),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::styled(" ← ", theme.bold_style()),
|
||||
Span::styled("Go back", theme.muted_style()),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::styled(" ? ", theme.bold_style()),
|
||||
Span::styled("Toggle this help", theme.muted_style()),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::styled(" q / Esc ", theme.bold_style()),
|
||||
Span::styled("Quit", theme.muted_style()),
|
||||
]),
|
||||
Line::from(""),
|
||||
Line::from(vec![Span::styled(
|
||||
" Press ? or Esc to dismiss",
|
||||
theme.dim_style(),
|
||||
)]),
|
||||
];
|
||||
|
||||
let help_block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.style(theme.overlay_style())
|
||||
.border_style(theme.overlay_border_style());
|
||||
|
||||
let paragraph = Paragraph::new(Text::from(keybinds))
|
||||
.block(help_block)
|
||||
.wrap(Wrap { trim: false });
|
||||
frame.render_widget(paragraph, overlay_area);
|
||||
}
|
||||
|
||||
pub fn braille_frame(tick: usize) -> char {
|
||||
const FRAMES: [char; 10] = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
||||
FRAMES[tick % FRAMES.len()]
|
||||
}
|
||||
|
||||
pub fn semantic_log_lines<'a>(lines: &[String], theme: &RedBearTheme) -> Vec<Line<'a>> {
|
||||
lines
|
||||
.iter()
|
||||
.map(|line| {
|
||||
let lower = line.to_lowercase();
|
||||
if lower.contains("completed successfully")
|
||||
|| lower.contains("success")
|
||||
|| lower.contains("installed")
|
||||
{
|
||||
Line::from(Span::styled(line.clone(), theme.success_style()))
|
||||
} else if lower.contains("warning")
|
||||
|| lower.contains("partial")
|
||||
|| lower.contains("skipped")
|
||||
{
|
||||
Line::from(Span::styled(line.clone(), theme.warning_style()))
|
||||
} else if lower.contains("failed")
|
||||
|| lower.contains("error")
|
||||
|| lower.contains("fatal")
|
||||
{
|
||||
Line::from(Span::styled(line.clone(), theme.error_style()))
|
||||
} else if lower.starts_with("command:")
|
||||
|| lower.starts_with("stdout:")
|
||||
|| lower.starts_with("stderr:")
|
||||
{
|
||||
Line::from(Span::styled(line.clone(), theme.dim_style()))
|
||||
} else {
|
||||
Line::from(Span::styled(line.clone(), theme.base_style()))
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user