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:
2026-06-10 13:02:30 +03:00
parent 333bbbd314
commit e82a86e440
15 changed files with 2417 additions and 90 deletions
+391
View File
@@ -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 | 100200 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`
+748
View File
@@ -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.
+114
View File
@@ -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 checkers 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 Bears 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 upstreams
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()
}