use std::collections::{HashMap, HashSet};
use std::io;
use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
use crate::terminal_widget::VtTerminal;
use crate::ui;
use anyhow::Result;
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyModifiers};
use crossterm::execute;
use crossterm::terminal::{
EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
};
use ratatui::Terminal;
use ratatui::backend::CrosstermBackend;
use tokio::sync::{broadcast, mpsc, watch};
use vex_hub::{FrontendCommand, FrontendEvent, Hub, HubState};
use vex_proto::{AgentId, AgentStatus, ShellId, WorkstreamId};
#[derive(Debug, Clone)]
pub(crate) enum SidebarEntry {
RepoHeader {
name: String,
path: String,
},
WorkstreamItem {
id: WorkstreamId,
repo_path: Option<String>,
name: String,
},
#[allow(dead_code)]
NestedAgent {
id: AgentId,
shell_id: ShellId,
label: String,
needs: bool,
},
#[allow(dead_code)]
NestedShell {
id: ShellId,
label: String,
},
Divider,
AgentSectionHeader,
AgentItem {
id: AgentId,
shell_id: ShellId,
label: String,
needs: bool,
},
CreateAgentPlaceholder,
ShellSectionHeader,
ShellItem {
id: ShellId,
label: String,
},
}
impl SidebarEntry {
fn is_selectable(&self) -> bool {
matches!(
self,
SidebarEntry::RepoHeader { .. }
| SidebarEntry::WorkstreamItem { .. }
| SidebarEntry::NestedAgent { .. }
| SidebarEntry::NestedShell { .. }
| SidebarEntry::AgentItem { .. }
| SidebarEntry::CreateAgentPlaceholder
| SidebarEntry::ShellItem { .. }
)
}
#[allow(dead_code)]
fn is_in_agent_section(&self) -> bool {
matches!(
self,
SidebarEntry::AgentSectionHeader
| SidebarEntry::AgentItem { .. }
| SidebarEntry::CreateAgentPlaceholder
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum RightPane {
Empty,
AgentConversation(AgentId),
ShellTerminal(ShellId),
RepoDetail(String), WorkstreamDetail(WorkstreamId, String), }
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Dialog {
None,
SpawnAgent { repo_idx: usize },
PromptAgent { agent_id: AgentId },
NewWorkstream { repo_idx: usize, name_buf: String },
}
pub struct App {
pub state: HubState,
pub cursor: usize,
pub right_pane: RightPane,
pub dialog: Dialog,
pub running: bool,
pub status_message: Option<String>,
pub vt_terminals: HashMap<ShellId, VtTerminal>,
pub agent_lines: HashMap<AgentId, Vec<String>>,
pub prompt_buf: String,
pub show_raw: bool,
pub conversation_scroll: usize,
}
pub(crate) fn repo_display_name(path: &str) -> String {
Path::new(path)
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_else(|| path.to_string())
}
impl App {
fn new() -> Self {
Self {
state: HubState::default(),
cursor: 0,
right_pane: RightPane::Empty,
dialog: Dialog::None,
running: true,
status_message: None,
vt_terminals: HashMap::new(),
agent_lines: HashMap::new(),
prompt_buf: String::new(),
show_raw: false,
conversation_scroll: 0,
}
}
pub(crate) fn sidebar(&self) -> Vec<SidebarEntry> {
let mut entries = Vec::new();
let agent_shell_ids: HashSet<ShellId> =
self.state.agents.iter().map(|a| a.shell_id).collect();
let mut claimed_agents: HashSet<AgentId> = HashSet::new();
for repo in &self.state.repos {
let display_name = repo_display_name(&repo.path);
entries.push(SidebarEntry::RepoHeader {
name: display_name,
path: repo.path.clone(),
});
for ws in &self.state.workstreams {
if ws.repo_path.as_deref() == Some(repo.path.as_str()) {
entries.push(SidebarEntry::WorkstreamItem {
id: ws.id,
repo_path: ws.repo_path.clone(),
name: ws.name.clone(),
});
for agent in &self.state.agents {
if agent.workstream_id == Some(ws.id) {
claimed_agents.insert(agent.id);
let label = agent_label(agent);
entries.push(SidebarEntry::NestedAgent {
id: agent.id,
shell_id: agent.shell_id,
label,
needs: agent.status == AgentStatus::Waiting,
});
}
}
}
}
}
entries.push(SidebarEntry::Divider);
let unclaimed: Vec<_> = self
.state
.agents
.iter()
.filter(|a| !claimed_agents.contains(&a.id))
.collect();
entries.push(SidebarEntry::AgentSectionHeader);
if unclaimed.is_empty() && claimed_agents.is_empty() {
entries.push(SidebarEntry::CreateAgentPlaceholder);
} else {
for agent in unclaimed {
let label = agent_label(agent);
entries.push(SidebarEntry::AgentItem {
id: agent.id,
shell_id: agent.shell_id,
label,
needs: agent.status == AgentStatus::Waiting,
});
}
}
let non_agent_shells: Vec<_> = self
.state
.shells
.iter()
.filter(|s| !agent_shell_ids.contains(&s.id) && !s.is_agent)
.collect();
if !non_agent_shells.is_empty() {
entries.push(SidebarEntry::ShellSectionHeader);
for shell in non_agent_shells {
entries.push(SidebarEntry::ShellItem {
id: shell.id,
label: format!("{} ({}c)", shell.name, shell.client_count),
});
}
}
entries
}
pub(crate) fn cursor_down(&mut self) {
let entries = self.sidebar();
let mut next = self.cursor + 1;
while next < entries.len() {
if entries[next].is_selectable() {
self.cursor = next;
return;
}
next += 1;
}
}
pub(crate) fn cursor_up(&mut self) {
let entries = self.sidebar();
if self.cursor == 0 {
return;
}
let mut prev = self.cursor - 1;
loop {
if entries[prev].is_selectable() {
self.cursor = prev;
return;
}
if prev == 0 {
break;
}
prev -= 1;
}
}
fn clamp_cursor(&mut self) {
let entries = self.sidebar();
if entries.is_empty() {
self.cursor = 0;
return;
}
if self.cursor >= entries.len() {
self.cursor = entries.len().saturating_sub(1);
}
if !entries[self.cursor].is_selectable() {
let saved = self.cursor;
self.cursor_down();
if self.cursor == saved {
self.cursor_up();
}
}
}
pub(crate) fn selected(&self) -> Option<SidebarEntry> {
self.sidebar().get(self.cursor).cloned()
}
#[allow(dead_code)]
pub(crate) fn in_agent_section(&self) -> bool {
let entries = self.sidebar();
for i in (0..=self.cursor).rev() {
match &entries[i] {
SidebarEntry::AgentSectionHeader => return true,
SidebarEntry::ShellSectionHeader
| SidebarEntry::Divider
| SidebarEntry::RepoHeader { .. } => return false,
_ => continue,
}
}
false
}
}
fn agent_label(agent: &vex_proto::AgentInfo) -> String {
let fallback = format!("agent-{}", agent.id);
let id_part = agent.title.as_deref().unwrap_or(&fallback);
let status = if agent.status == AgentStatus::Waiting {
"NEEDS *"
} else {
match agent.status {
AgentStatus::Starting => "starting",
AgentStatus::Running => "running",
AgentStatus::Stalled => "stalled",
AgentStatus::Completed => "done",
AgentStatus::Error => "error",
_ => "idle",
}
};
format!("{} {}", id_part, status)
}
pub async fn run(port: u16) -> Result<()> {
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
if tokio::net::TcpStream::connect(("127.0.0.1", port))
.await
.is_err()
{
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
terminal.show_cursor()?;
anyhow::bail!(
"cannot connect to daemon on port {} (is it running? try `vex daemon start`)",
port
);
}
let hub = Arc::new(Hub::new(port));
let mut state_rx = hub.state_rx();
let command_tx = hub.command_tx();
let mut event_rx = hub.event_rx();
let hub_clone = Arc::clone(&hub);
let hub_task = tokio::spawn(async move {
if let Err(e) = hub_clone.run().await {
eprintln!("hub error: {}", e);
}
});
let mut app = App::new();
if let Ok(()) = state_rx.changed().await {
app.state = state_rx.borrow_and_update().clone();
app.clamp_cursor();
}
let _result = run_loop(
&mut terminal,
&mut app,
&mut state_rx,
&mut event_rx,
&command_tx,
)
.await;
hub_task.abort();
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
terminal.show_cursor()?;
Ok(())
}
async fn run_loop(
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
app: &mut App,
state_rx: &mut watch::Receiver<HubState>,
event_rx: &mut broadcast::Receiver<FrontendEvent>,
command_tx: &mpsc::Sender<FrontendCommand>,
) -> Result<()> {
loop {
if !app.running {
break;
}
terminal.draw(|f| ui::draw(f, app))?;
tokio::select! {
_ = state_rx.changed() => {
app.state = state_rx.borrow_and_update().clone();
app.clamp_cursor();
let shell_ids: HashSet<ShellId> = app.state.shells.iter().map(|s| s.id).collect();
let agent_ids: HashSet<AgentId> = app.state.agents.iter().map(|a| a.id).collect();
app.vt_terminals.retain(|id, _| shell_ids.contains(id));
app.agent_lines.retain(|id, _| agent_ids.contains(id));
}
result = event_rx.recv() => {
match result {
Ok(FrontendEvent::ShellOutput { shell_id, data }) => {
if let Some(vt) = app.vt_terminals.get_mut(&shell_id) {
vt.process(&data);
}
}
Ok(FrontendEvent::AgentConversationLine { agent_id, line }) => {
app.agent_lines.entry(agent_id).or_default().push(line);
}
Ok(FrontendEvent::AgentWatchEnd { agent_id }) => {
app.agent_lines.entry(agent_id).or_default().push("[agent turn complete]".to_string());
}
Ok(_) => {}
Err(broadcast::error::RecvError::Lagged(_)) => continue,
Err(_) => break,
}
}
_ = tokio::time::sleep(Duration::from_millis(50)) => {
while event::poll(Duration::ZERO)? {
match event::read()? {
Event::Key(key) => handle_key(app, key, command_tx).await?,
Event::Resize(cols, rows) => {
if let RightPane::ShellTerminal(id) = &app.right_pane {
let id = *id;
if let Some(vt) = app.vt_terminals.get_mut(&id) {
vt.resize(rows, cols);
}
let _ = command_tx.send(FrontendCommand::ShellResize { id, cols, rows }).await;
}
}
_ => {}
}
}
}
}
}
Ok(())
}
async fn handle_key(
app: &mut App,
key: KeyEvent,
tx: &mpsc::Sender<FrontendCommand>,
) -> Result<()> {
if key.code == KeyCode::Char('c') && key.modifiers.contains(KeyModifiers::CONTROL) {
app.running = false;
return Ok(());
}
if app.dialog != Dialog::None {
return handle_dialog_key(app, key, tx).await;
}
if let RightPane::ShellTerminal(id) = &app.right_pane {
let id = *id;
if key.code == KeyCode::Char(']') && key.modifiers.contains(KeyModifiers::CONTROL) {
let _ = tx.send(FrontendCommand::ShellDetach { id }).await;
app.right_pane = RightPane::Empty;
return Ok(());
}
let data = key_to_bytes(key);
if !data.is_empty() {
let _ = tx.send(FrontendCommand::ShellInput { id, data }).await;
}
return Ok(());
}
if matches!(app.right_pane, RightPane::AgentConversation(_)) {
match key.code {
KeyCode::PageUp => {
app.conversation_scroll = app.conversation_scroll.saturating_add(20);
return Ok(());
}
KeyCode::PageDown => {
app.conversation_scroll = app.conversation_scroll.saturating_sub(20);
return Ok(());
}
KeyCode::Char('u') if key.modifiers.contains(KeyModifiers::CONTROL) => {
app.conversation_scroll = app.conversation_scroll.saturating_add(10);
return Ok(());
}
KeyCode::Char('d') if key.modifiers.contains(KeyModifiers::CONTROL) => {
app.conversation_scroll = app.conversation_scroll.saturating_sub(10);
return Ok(());
}
KeyCode::Char('G') => {
app.conversation_scroll = 0; return Ok(());
}
_ => {}
}
}
match key.code {
KeyCode::Char('q') => app.running = false,
KeyCode::Down | KeyCode::Char('j') => app.cursor_down(),
KeyCode::Up | KeyCode::Char('k') => app.cursor_up(),
KeyCode::Enter => {
if let Some(entry) = app.selected() {
match entry {
SidebarEntry::RepoHeader { path, .. } => {
app.right_pane = RightPane::RepoDetail(path);
}
SidebarEntry::WorkstreamItem { id, name, .. } => {
app.right_pane = RightPane::WorkstreamDetail(id, name);
}
SidebarEntry::AgentItem { id, .. } | SidebarEntry::NestedAgent { id, .. } => {
app.right_pane = RightPane::AgentConversation(id);
app.conversation_scroll = 0;
}
SidebarEntry::ShellItem { id, .. } | SidebarEntry::NestedShell { id, .. } => {
open_shell(app, id, tx).await;
}
SidebarEntry::CreateAgentPlaceholder => {
if !app.state.repos.is_empty() {
app.dialog = Dialog::SpawnAgent { repo_idx: 0 };
} else {
app.status_message =
Some("register a repo first with `vex repo add`".to_string());
}
}
_ => {}
}
}
}
KeyCode::Char('s') => {
if key.modifiers.contains(KeyModifiers::CONTROL) {
if let RightPane::AgentConversation(agent_id) = &app.right_pane {
let agent_id = *agent_id;
if let Some(agent) = app.state.agents.iter().find(|a| a.id == agent_id) {
let shell_id = agent.shell_id;
open_shell(app, shell_id, tx).await;
}
}
} else if let Some(
SidebarEntry::AgentItem { shell_id, .. }
| SidebarEntry::NestedAgent { shell_id, .. },
) = app.selected()
{
open_shell(app, shell_id, tx).await;
} else if let RightPane::RepoDetail(ref repo_path) = app.right_pane {
let workdir = repo_path.clone();
let _ = tx
.send(FrontendCommand::ShellCreate {
workdir: Some(workdir.clone()),
workstream_id: None,
})
.await;
app.status_message = Some(format!("creating shell in {}...", workdir));
} else if let RightPane::WorkstreamDetail(ws_id, ref ws_name) = app.right_pane {
let ws_name = ws_name.clone();
let _ = tx
.send(FrontendCommand::ShellCreate {
workdir: None,
workstream_id: Some(ws_id),
})
.await;
app.status_message = Some(format!("creating shell in ws {}...", ws_name));
}
}
KeyCode::Char('c') => {
if let RightPane::RepoDetail(ref repo_path) = app.right_pane {
let workdir = repo_path.clone();
let _ = tx
.send(FrontendCommand::AgentSpawn {
workdir: Some(workdir.clone()),
prompt: None,
workstream_id: None,
})
.await;
app.status_message = Some(format!("spawning agent in {}...", workdir));
} else if let RightPane::WorkstreamDetail(ws_id, ref ws_name) = app.right_pane {
let ws_name = ws_name.clone();
let _ = tx
.send(FrontendCommand::AgentSpawn {
workdir: None,
prompt: None,
workstream_id: Some(ws_id),
})
.await;
app.status_message = Some(format!("spawning agent in ws {}...", ws_name));
} else if app.in_agent_section()
|| matches!(app.selected(), Some(SidebarEntry::CreateAgentPlaceholder))
{
if !app.state.repos.is_empty() {
app.dialog = Dialog::SpawnAgent { repo_idx: 0 };
} else {
app.status_message = Some("register a repo first".to_string());
}
}
}
KeyCode::Char('x') => {
if let RightPane::WorkstreamDetail(ws_id, ref ws_name) = app.right_pane {
let ws_name = ws_name.clone();
let _ = tx.send(FrontendCommand::WsAbandon { id: ws_id }).await;
app.right_pane = RightPane::Empty;
app.status_message = Some(format!("abandoning workstream {}...", ws_name));
} else if let Some(entry) = app.selected() {
match entry {
SidebarEntry::AgentItem { id, shell_id, .. }
| SidebarEntry::NestedAgent { id, shell_id, .. } => {
let _ = tx.send(FrontendCommand::ShellKill { id: shell_id }).await;
if app.right_pane == RightPane::AgentConversation(id)
|| app.right_pane == RightPane::ShellTerminal(shell_id)
{
app.right_pane = RightPane::Empty;
}
app.status_message = Some(format!("killing agent {}...", id));
}
SidebarEntry::ShellItem { id, .. } | SidebarEntry::NestedShell { id, .. } => {
let _ = tx.send(FrontendCommand::ShellKill { id }).await;
if app.right_pane == RightPane::ShellTerminal(id) {
app.right_pane = RightPane::Empty;
}
app.status_message = Some(format!("killing shell {}...", id));
}
SidebarEntry::WorkstreamItem { id, name, .. } => {
let _ = tx.send(FrontendCommand::WsAbandon { id }).await;
if app.right_pane == RightPane::WorkstreamDetail(id, name.clone()) {
app.right_pane = RightPane::Empty;
}
app.status_message = Some(format!("abandoning workstream {}...", name));
}
_ => {}
}
}
}
KeyCode::Char('p') => {
let agent_id = match app.selected() {
Some(SidebarEntry::AgentItem { id, .. } | SidebarEntry::NestedAgent { id, .. }) => {
Some(id)
}
_ => {
if let RightPane::AgentConversation(id) = &app.right_pane {
Some(*id)
} else {
None
}
}
};
if let Some(id) = agent_id {
app.prompt_buf.clear();
app.dialog = Dialog::PromptAgent { agent_id: id };
}
}
KeyCode::Char('w') => {
if !app.state.repos.is_empty() {
let repo_idx = match app.selected() {
Some(SidebarEntry::RepoHeader { ref path, .. }) => app
.state
.repos
.iter()
.position(|r| r.path == *path)
.unwrap_or(0),
Some(SidebarEntry::WorkstreamItem {
repo_path: Some(ref rp),
..
}) => app
.state
.repos
.iter()
.position(|r| r.path == *rp)
.unwrap_or(0),
Some(SidebarEntry::WorkstreamItem { .. }) => 0,
_ => 0,
};
app.dialog = Dialog::NewWorkstream {
repo_idx,
name_buf: String::new(),
};
} else {
app.status_message = Some("register a repo first".to_string());
}
}
KeyCode::Char('r') => {
let _ = tx.send(FrontendCommand::RefreshState).await;
}
KeyCode::Char('v') => {
if matches!(app.right_pane, RightPane::AgentConversation(_)) {
app.show_raw = !app.show_raw;
}
}
KeyCode::Char('?') => {
app.status_message = Some(
"Enter:open s:shell c:agent x:kill p:prompt w:ws r:refresh q:quit"
.to_string(),
);
}
KeyCode::Esc => {
app.status_message = None;
if app.right_pane != RightPane::Empty {
app.right_pane = RightPane::Empty;
}
}
_ => {}
}
Ok(())
}
async fn open_shell(app: &mut App, id: ShellId, tx: &mpsc::Sender<FrontendCommand>) {
let (cols, rows) = crossterm::terminal::size().unwrap_or((80, 24));
let pane_cols = cols / 2;
app.vt_terminals
.entry(id)
.or_insert_with(|| VtTerminal::new(pane_cols, rows.saturating_sub(2)));
let _ = tx
.send(FrontendCommand::ShellAttach {
id,
cols: pane_cols,
rows: rows.saturating_sub(2),
})
.await;
app.right_pane = RightPane::ShellTerminal(id);
}
async fn handle_dialog_key(
app: &mut App,
key: KeyEvent,
tx: &mpsc::Sender<FrontendCommand>,
) -> Result<()> {
match &mut app.dialog {
Dialog::SpawnAgent { repo_idx } => match key.code {
KeyCode::Esc => app.dialog = Dialog::None,
KeyCode::Down | KeyCode::Char('j') => {
if *repo_idx + 1 < app.state.repos.len() {
*repo_idx += 1;
}
}
KeyCode::Up | KeyCode::Char('k') => {
*repo_idx = repo_idx.saturating_sub(1);
}
KeyCode::Enter => {
if let Some(repo) = app.state.repos.get(*repo_idx) {
let workdir = repo.path.clone();
let display = repo_display_name(&repo.path);
let _ = tx
.send(FrontendCommand::AgentSpawn {
workdir: Some(workdir),
prompt: None,
workstream_id: None,
})
.await;
app.status_message = Some(format!("spawning agent in {}...", display));
}
app.dialog = Dialog::None;
}
_ => {}
},
Dialog::PromptAgent { agent_id } => match key.code {
KeyCode::Esc => app.dialog = Dialog::None,
KeyCode::Enter if key.modifiers.contains(KeyModifiers::ALT) => {
if !app.prompt_buf.is_empty() {
let text = app.prompt_buf.clone();
let id = *agent_id;
let _ = tx
.send(FrontendCommand::AgentSend { id, message: text })
.await;
app.prompt_buf.clear();
app.right_pane = RightPane::AgentConversation(id);
}
app.dialog = Dialog::None;
}
KeyCode::Enter => {
app.prompt_buf.push('\n');
}
KeyCode::Backspace => {
app.prompt_buf.pop();
}
KeyCode::Char(c) => app.prompt_buf.push(c),
_ => {}
},
Dialog::NewWorkstream { repo_idx, name_buf } => match key.code {
KeyCode::Esc => app.dialog = Dialog::None,
KeyCode::Down | KeyCode::Char('j') => {
if *repo_idx + 1 < app.state.repos.len() {
*repo_idx += 1;
}
}
KeyCode::Up | KeyCode::Char('k') => {
*repo_idx = repo_idx.saturating_sub(1);
}
KeyCode::Enter => {
let normalized = normalize_name(name_buf);
if !normalized.is_empty()
&& let Some(repo) = app.state.repos.get(*repo_idx)
{
let _ = tx
.send(FrontendCommand::WsCreate {
name: normalized.clone(),
repo: Some(repo.path.clone()),
})
.await;
app.status_message = Some(format!("creating workstream '{}'...", normalized));
}
app.dialog = Dialog::None;
}
KeyCode::Backspace => {
name_buf.pop();
}
KeyCode::Char(c) => name_buf.push(c),
_ => {}
},
Dialog::None => {}
}
Ok(())
}
#[allow(dead_code)]
pub(crate) fn fuzzy_match(text: &str, pattern: &str) -> bool {
if pattern.is_empty() {
return true;
}
let mut pattern_chars = pattern.chars().flat_map(|c| c.to_lowercase());
let mut current = pattern_chars.next().unwrap();
for ch in text.chars().flat_map(|c| c.to_lowercase()) {
if ch == current {
match pattern_chars.next() {
Some(next) => current = next,
None => return true,
}
}
}
false
}
fn normalize_name(s: &str) -> String {
s.to_lowercase()
.replace(' ', "-")
.chars()
.filter(|c| c.is_alphanumeric() || *c == '-' || *c == '_')
.collect()
}
fn key_to_bytes(key: KeyEvent) -> Vec<u8> {
match key.code {
KeyCode::Char(c) => {
if key.modifiers.contains(KeyModifiers::CONTROL) && c.is_ascii_lowercase() {
vec![(c as u8) - b'a' + 1]
} else if key.modifiers.contains(KeyModifiers::CONTROL) {
vec![]
} else {
let mut buf = [0u8; 4];
c.encode_utf8(&mut buf);
buf[..c.len_utf8()].to_vec()
}
}
KeyCode::Enter => vec![b'\r'],
KeyCode::Backspace => vec![0x7f],
KeyCode::Tab => vec![b'\t'],
KeyCode::Esc => vec![0x1b],
KeyCode::Up => b"\x1b[A".to_vec(),
KeyCode::Down => b"\x1b[B".to_vec(),
KeyCode::Right => b"\x1b[C".to_vec(),
KeyCode::Left => b"\x1b[D".to_vec(),
KeyCode::Home => b"\x1b[H".to_vec(),
KeyCode::End => b"\x1b[F".to_vec(),
KeyCode::Delete => b"\x1b[3~".to_vec(),
_ => vec![],
}
}