use cansi::v3::{categorise_text, construct_text_no_codes};
use clap::ValueEnum;
use colored::*;
use crossterm::{
cursor,
event::{
self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseEvent, MouseEventKind,
},
execute,
style::Print,
terminal,
};
use futures_lite::stream::StreamExt;
use std::env;
use std::io::stdout;
use std::io::Write;
use std::sync::{mpsc, Arc, Mutex};
use std::thread;
use cyme::config::Config;
use cyme::display::*;
use cyme::error::{Error, ErrorKind, Result};
use cyme::profiler::{watch::SystemProfileStreamBuilder, DeviceFilter, Filter, SystemProfile};
use cyme::types::VidPid;
#[derive(Debug, Clone)]
#[allow(dead_code)]
enum WatchEvent {
ScrollUp(usize),
ScrollUpHalf,
ScrollUpPage,
ScrollDown(usize),
ScrollDownHalf,
ScrollDownPage,
MoveUp(usize),
MoveDown(usize),
EditFilter(FilterField),
ToggleBuses,
ToggleHubs,
ExpandSelected,
PushFilter(char),
PopFilter,
EditBlock(BlockType),
MoveBlockUp(usize),
MoveBlockDown(usize),
ToggleBlock,
Error(String),
Resize,
DrawDevices,
DrawEditBlocks,
WriteEditBlocks,
ConfirmSave(String),
SaveConfig,
Draw,
ShowHelp,
Enter,
Esc,
Quit,
}
#[derive(Debug, Clone, Copy, strum_macros::Display)]
enum BlockType {
Device,
Bus,
Config,
Interface,
Endpoint,
}
impl BlockType {
fn next(&self) -> Self {
match self {
BlockType::Bus => BlockType::Device,
BlockType::Device => BlockType::Config,
BlockType::Config => BlockType::Interface,
BlockType::Interface => BlockType::Endpoint,
BlockType::Endpoint => BlockType::Bus,
}
}
fn prev(&self) -> Self {
match self {
BlockType::Bus => BlockType::Endpoint,
BlockType::Device => BlockType::Bus,
BlockType::Config => BlockType::Device,
BlockType::Interface => BlockType::Config,
BlockType::Endpoint => BlockType::Interface,
}
}
fn key_number(&self) -> char {
match self {
BlockType::Bus => '1',
BlockType::Device => '2',
BlockType::Config => '3',
BlockType::Interface => '4',
BlockType::Endpoint => '5',
}
}
}
#[derive(Debug, Clone)]
enum State {
Normal,
EditingFilter {
field: FilterField,
buffer: Option<String>,
original: Option<String>,
},
BlockEditor {
block_type: BlockType,
selected_index: usize,
blocks: Vec<(ColoredString, bool)>,
},
Help,
Error(String),
Confirm(String, WatchEvent),
}
#[derive(Debug, Clone)]
enum FilterField {
Name,
Serial,
VidPid,
Class,
}
#[derive(Debug)]
struct Display {
buffer: Vec<u8>,
spusb: Arc<Mutex<SystemProfile>>,
print_settings: Arc<Mutex<PrintSettings>>,
filter: DeviceFilter,
state: Arc<Mutex<State>>,
terminal_size: (u16, u16),
selected_line: Option<usize>,
available_rows: usize,
max_offset: usize,
lines: usize,
scroll_offset: usize,
line_context: Vec<LineItem>,
selected_item: Option<LineItem>,
}
fn set_filter(field: &FilterField, value: Option<String>, filter: &mut DeviceFilter) -> Result<()> {
let mut f = filter.filters.first().cloned().unwrap_or_default();
match field {
FilterField::Name => f.name = value,
FilterField::Serial => f.serial = value,
FilterField::VidPid => match value {
Some(s) => {
let vp = s.parse::<VidPid>()?;
f.vid = vp.0;
f.pid = vp.1;
}
None => {
f.vid = None;
f.pid = None;
}
},
FilterField::Class => match value {
Some(s) => {
f.class = Some(
cyme::usb::BaseClass::from_str(&s, true)
.map_err(|e| Error::new(ErrorKind::Parsing, &e.to_string()))?,
);
}
None => f.class = None,
},
};
filter.filters = if f == Filter::default() {
vec![]
} else {
vec![f]
};
Ok(())
}
fn strip_ansi_codes(input: &str) -> String {
let decolored = categorise_text(input);
construct_text_no_codes(&decolored)
}
fn print_json(
spusb: &mut SystemProfile,
filter: Option<&DeviceFilter>,
print_settings: &PrintSettings,
) -> Result<()> {
cyme::display::prepare(spusb, filter, print_settings);
let json = serde_json::to_string(&spusb)?;
println!("{json}");
Ok(())
}
pub fn watch_usb_devices_json(
spusb: SystemProfile,
filter: Option<DeviceFilter>,
print_settings: PrintSettings,
) -> Result<()> {
let mut profile_stream = SystemProfileStreamBuilder::new()
.with_spusb(spusb)
.is_verbose(print_settings.verbosity > 0)
.build()
.map_err(|e| Error::new(ErrorKind::Nusb, &e.to_string()))?;
let (tx, rx) = mpsc::channel::<WatchEvent>();
let print_settings = Arc::new(Mutex::new(print_settings));
let spusb = profile_stream.get_profile();
tx.send(WatchEvent::DrawDevices).unwrap();
let tx_clone = tx.clone();
thread::spawn(move || {
futures_lite::future::block_on(async {
while profile_stream.next().await.is_some() {
tx_clone.send(WatchEvent::DrawDevices).unwrap();
}
});
});
loop {
match rx.recv() {
Ok(WatchEvent::DrawDevices) => {
let print_settings = print_settings.lock().unwrap();
print_json(&mut spusb.lock().unwrap(), filter.as_ref(), &print_settings)?;
}
Ok(_) => {
}
Err(_) => {
break;
}
}
}
Ok(())
}
pub fn watch_usb_devices(
spusb: SystemProfile,
filter: Option<DeviceFilter>,
mut print_settings: PrintSettings,
mut config: Config,
) -> Result<()> {
print_settings.print_mode = PrintMode::Dynamic;
if print_settings.device_blocks.is_none() {
print_settings.device_blocks = Some(DeviceBlocks::default_watch_blocks(
print_settings.verbosity > 0,
print_settings.tree,
));
}
print_settings.bus_blocks = print_settings.bus_blocks.or(Some(BusBlocks::default_blocks(
print_settings.verbosity > 0,
)));
print_settings.config_blocks =
print_settings
.config_blocks
.or(Some(ConfigurationBlocks::default_blocks(
print_settings.verbosity > 0,
)));
print_settings.interface_blocks =
print_settings
.interface_blocks
.or(Some(InterfaceBlocks::default_blocks(
print_settings.verbosity > 0,
)));
print_settings.endpoint_blocks =
print_settings
.endpoint_blocks
.or(Some(EndpointBlocks::default_blocks(
print_settings.verbosity > 0,
)));
let mut stdout = stdout();
execute!(
stdout,
terminal::EnterAlternateScreen,
cursor::Hide,
terminal::Clear(terminal::ClearType::All)
)
.map_err(|e| Error::new(ErrorKind::Other("crossterm"), &e.to_string()))?;
terminal::enable_raw_mode()?;
let mut profile_stream = SystemProfileStreamBuilder::new()
.with_spusb(spusb) .is_verbose(true) .build()
.map_err(|e| Error::new(ErrorKind::Nusb, &e.to_string()))?;
let (tx, rx) = mpsc::channel::<WatchEvent>();
let print_settings = Arc::new(Mutex::new(print_settings));
let input_mode = Arc::new(Mutex::new(State::Normal));
tx.send(WatchEvent::DrawDevices).unwrap();
let mut display = Display {
buffer: Vec::new(),
spusb: profile_stream.get_profile(),
print_settings: print_settings.clone(),
filter: filter.unwrap_or_default(),
state: input_mode.clone(),
terminal_size: terminal::size().unwrap_or((80, 24)),
lines: 0,
available_rows: 0,
max_offset: 0,
scroll_offset: 0,
selected_line: None,
selected_item: None,
line_context: Vec::new(),
};
let tx_clone = tx.clone();
let stop_flag = Arc::new(Mutex::new(false));
let stop_clone = stop_flag.clone();
thread::spawn(move || {
futures_lite::future::block_on(async {
while profile_stream.next().await.is_some() {
if *stop_clone.lock().unwrap() {
break;
}
tx_clone.send(WatchEvent::DrawDevices).unwrap();
}
});
});
let stop_clone = stop_flag.clone();
thread::spawn(move || loop {
let send_wrapper = |e: WatchEvent| {
if *stop_clone.lock().unwrap() {
return true;
}
tx.send(e).unwrap();
false
};
match event::read().unwrap() {
Event::Resize(_, _) if send_wrapper(WatchEvent::Resize) => {
break;
}
Event::Mouse(MouseEvent { kind, .. }) => match kind {
MouseEventKind::ScrollUp if send_wrapper(WatchEvent::ScrollUp(1)) => {
break;
}
MouseEventKind::ScrollDown if send_wrapper(WatchEvent::ScrollDown(1)) => {
break;
}
_ => (),
},
Event::Key(key_event) => {
if *stop_clone.lock().unwrap() {
break;
}
input_mode.lock().unwrap().process_key_event(
key_event,
tx.clone(),
print_settings.clone(),
);
}
_ => (),
}
if *stop_clone.lock().unwrap() {
break;
}
});
loop {
match rx.recv() {
Ok(WatchEvent::ScrollUp(n)) => {
display.scroll_offset = display.scroll_offset.saturating_sub(n);
display.scroll_check_selected_visable();
}
Ok(WatchEvent::ScrollUpHalf) => {
display.scroll_offset = display
.scroll_offset
.saturating_sub(display.available_rows / 2);
display.scroll_check_selected_visable();
}
Ok(WatchEvent::ScrollUpPage) => {
display.scroll_offset =
display.scroll_offset.saturating_sub(display.available_rows);
display.scroll_check_selected_visable();
}
Ok(WatchEvent::ScrollDown(n)) => {
display.scroll_offset = display
.max_offset
.min(display.scroll_offset.saturating_add(n));
display.scroll_check_selected_visable();
}
Ok(WatchEvent::ScrollDownHalf) => {
display.scroll_offset = display.max_offset.min(
display
.scroll_offset
.saturating_add(display.available_rows / 2),
);
display.scroll_check_selected_visable();
}
Ok(WatchEvent::ScrollDownPage) => {
display.scroll_offset = display
.max_offset
.min(display.scroll_offset.saturating_add(display.max_offset));
display.scroll_check_selected_visable();
}
Ok(WatchEvent::MoveUp(n)) => {
display.move_up(n);
if let Some(l) = display.selected_line {
if l < display.scroll_offset {
display.scroll_offset = display
.scroll_offset
.saturating_sub(display.scroll_offset - l);
}
}
}
Ok(WatchEvent::MoveDown(n)) => {
display.move_down(n);
if let Some(l) = display.selected_line {
if l >= display.scroll_offset + display.available_rows {
display.scroll_offset = display.scroll_offset.saturating_add(
l - (display.scroll_offset + display.available_rows) + 1,
);
}
}
}
Ok(WatchEvent::ExpandSelected) => {
display.toggle_selected(false);
}
Ok(WatchEvent::EditFilter(field)) => {
let original = match field {
FilterField::Name => {
display.filter.filters.first().and_then(|f| f.name.clone())
}
FilterField::Serial => display
.filter
.filters
.first()
.and_then(|f| f.serial.clone()),
FilterField::VidPid => {
display
.filter
.filters
.first()
.and_then(|f| match (f.vid, f.pid) {
(Some(vid), Some(pid)) => Some(format!("{vid:04x}:{pid:04x}")),
(Some(vid), None) => Some(format!("{vid:04x}")),
_ => None,
})
}
FilterField::Class => display
.filter
.filters
.first()
.and_then(|f| f.class.map(|c| c.to_string())),
};
*display.state.lock().unwrap() = State::EditingFilter {
field,
buffer: original.clone(),
original,
};
}
Ok(WatchEvent::PushFilter(c)) => {
let mut input_mode = display.state.lock().unwrap();
if let State::EditingFilter { field, buffer, .. } = &mut *input_mode {
buffer.get_or_insert_with(String::new).push(c);
match field {
FilterField::Serial | FilterField::Name => {
if let Err(e) = set_filter(field, buffer.clone(), &mut display.filter) {
*input_mode = State::Error(e.to_string());
}
}
_ => (),
}
}
}
Ok(WatchEvent::PopFilter) => {
let mut input_mode = display.state.lock().unwrap();
if let State::EditingFilter { field, buffer, .. } = &mut *input_mode {
if let Some(b) = buffer {
let ret = b.pop();
if b.is_empty() | ret.is_none() {
buffer.take();
}
};
match field {
FilterField::Serial | FilterField::Name => {
if let Err(e) = set_filter(field, buffer.clone(), &mut display.filter) {
*input_mode = State::Error(e.to_string());
}
}
_ => (),
}
}
}
Ok(WatchEvent::ToggleBuses) => {
display.filter.exclude_empty_bus = !display.filter.exclude_empty_bus;
}
Ok(WatchEvent::ToggleHubs) => {
display.filter.exclude_empty_hub = !display.filter.exclude_empty_hub;
}
Ok(WatchEvent::Enter) => {
let new_mode = match &*display.state.lock().unwrap() {
State::EditingFilter { field, buffer, .. } => {
if let Err(e) = set_filter(field, buffer.to_owned(), &mut display.filter) {
State::Error(e.to_string())
} else {
State::Normal
}
}
State::Normal => {
display.toggle_selected(true);
State::Normal
}
_ => State::Normal,
};
*display.state.lock().unwrap() = new_mode;
display.prepare_devices();
display.draw_devices()?;
}
Ok(WatchEvent::Error(msg)) => {
*display.state.lock().unwrap() = State::Error(msg);
display.draw()?;
}
Ok(WatchEvent::ConfirmSave(msg)) => {
*display.state.lock().unwrap() = State::Confirm(msg, WatchEvent::SaveConfig);
display.draw()?;
}
Ok(WatchEvent::Resize) => {
if !matches!(&*display.state.lock().unwrap(), State::BlockEditor { .. }) {
display.print_settings.lock().unwrap().terminal_size = terminal::size().ok();
display.draw_devices()?;
}
display.draw()?;
}
Ok(WatchEvent::DrawDevices) => {
display.prepare_devices();
display.draw_devices()?;
}
Ok(WatchEvent::EditBlock(block_type)) => {
display.prepare_edit_blocks(block_type);
}
Ok(WatchEvent::MoveBlockUp(n)) => {
if let State::BlockEditor {
selected_index,
blocks,
..
} = &mut *display.state.lock().unwrap()
{
if *selected_index != 0 {
blocks.swap(*selected_index, selected_index.saturating_sub(n));
*selected_index = selected_index.saturating_sub(n);
display.selected_line = Some(*selected_index);
}
}
}
Ok(WatchEvent::MoveBlockDown(n)) => {
if let State::BlockEditor {
selected_index,
blocks,
..
} = &mut *display.state.lock().unwrap()
{
if *selected_index < blocks.len() - 1 {
blocks.swap(*selected_index, selected_index.saturating_add(n));
*selected_index = selected_index.saturating_add(n);
display.selected_line = Some(*selected_index);
}
}
}
Ok(WatchEvent::ToggleBlock) => {
if let State::BlockEditor {
selected_index,
blocks,
..
} = &mut *display.state.lock().unwrap()
{
if *selected_index < blocks.len() {
blocks[*selected_index].1 = !blocks[*selected_index].1;
}
}
}
Ok(WatchEvent::DrawEditBlocks) => {
display.draw_edit_blocks()?;
}
Ok(WatchEvent::WriteEditBlocks) => {
display.write_edit_blocks();
}
Ok(WatchEvent::SaveConfig) => {
config.merge_print_settings(&display.print_settings.lock().unwrap());
if let Err(e) = config.save() {
*display.state.lock().unwrap() = State::Error(e.to_string());
display.prepare_devices();
display.draw_devices()?;
}
}
Ok(WatchEvent::ShowHelp) => {
*display.state.lock().unwrap() = State::Help;
display.draw_help()?;
}
Ok(WatchEvent::Draw) => {
display.draw()?;
}
Ok(WatchEvent::Esc) => {
let new_mode = match &*display.state.lock().unwrap() {
State::Normal => {
display.selected_line = None;
display.selected_item = None;
State::Normal
}
State::EditingFilter {
field, original, ..
} => match set_filter(field, original.to_owned(), &mut display.filter) {
Ok(_) => State::Normal,
Err(e) => {
set_filter(field, None, &mut display.filter)?;
State::Error(e.to_string())
}
},
_ => State::Normal,
};
*display.state.lock().unwrap() = new_mode;
display.prepare_devices();
display.draw_devices()?;
}
Ok(WatchEvent::Quit) => {
*stop_flag.lock().unwrap() = true;
break;
}
Err(_) => {
break;
}
}
}
execute!(
stdout,
terminal::Clear(terminal::ClearType::All),
terminal::LeaveAlternateScreen,
cursor::Show,
)
.map_err(|e| Error::new(ErrorKind::Other("crossterm"), &e.to_string()))?;
terminal::disable_raw_mode()?;
Ok(())
}
impl State {
fn process_normal_mode(
event: KeyEvent,
tx: mpsc::Sender<WatchEvent>,
print_settings: Arc<Mutex<PrintSettings>>,
) {
let KeyEvent {
code, modifiers, ..
} = event;
match (code, modifiers) {
(KeyCode::Enter, _) => {
tx.send(WatchEvent::Enter).unwrap();
}
(KeyCode::Char(' '), _) => {
tx.send(WatchEvent::ExpandSelected).unwrap();
tx.send(WatchEvent::DrawDevices).unwrap();
}
(KeyCode::Char('?'), _) => {
tx.send(WatchEvent::ShowHelp).unwrap();
}
(KeyCode::Char('q'), _) => {
tx.send(WatchEvent::Quit).unwrap();
}
(KeyCode::Char('v'), _) => {
let mut print_settings = print_settings.lock().unwrap();
print_settings.verbosity = (print_settings.verbosity + 1) % 4;
tx.send(WatchEvent::DrawDevices).unwrap();
}
(KeyCode::Char('t'), _) => {
let mut print_settings = print_settings.lock().unwrap();
print_settings.tree = !print_settings.tree;
tx.send(WatchEvent::DrawDevices).unwrap();
}
(KeyCode::Char('h'), _) => {
let mut print_settings = print_settings.lock().unwrap();
print_settings.headings = !print_settings.headings;
tx.send(WatchEvent::DrawDevices).unwrap();
}
(KeyCode::Char('o'), _) => {
let mut print_settings = print_settings.lock().unwrap();
print_settings.decimal = !print_settings.decimal;
tx.send(WatchEvent::DrawDevices).unwrap();
}
(KeyCode::Char('p'), _) => {
let mut print_settings = print_settings.lock().unwrap();
match print_settings.sort_devices {
Sort::BranchPosition => print_settings.sort_devices = Sort::DeviceNumber,
_ => print_settings.sort_devices = Sort::BranchPosition,
}
tx.send(WatchEvent::DrawDevices).unwrap();
}
(KeyCode::Char('a'), _) => {
let mut print_settings = print_settings.lock().unwrap();
match print_settings.encoding {
Encoding::Glyphs => print_settings.encoding = Encoding::Ascii,
_ => print_settings.encoding = Encoding::Glyphs,
}
tx.send(WatchEvent::DrawDevices).unwrap();
}
(KeyCode::Char('j'), KeyModifiers::NONE) | (KeyCode::Down, _) => {
tx.send(WatchEvent::MoveDown(1)).unwrap();
tx.send(WatchEvent::Draw).unwrap();
}
(KeyCode::Char('k'), KeyModifiers::NONE) | (KeyCode::Up, _) => {
tx.send(WatchEvent::MoveUp(1)).unwrap();
tx.send(WatchEvent::Draw).unwrap();
}
(KeyCode::Char('/'), _) => {
tx.send(WatchEvent::EditFilter(FilterField::Name)).unwrap();
tx.send(WatchEvent::DrawDevices).unwrap();
}
(KeyCode::Char('\\'), _) => {
tx.send(WatchEvent::ToggleBuses).unwrap();
tx.send(WatchEvent::DrawDevices).unwrap();
}
(KeyCode::Char(';'), _) => {
tx.send(WatchEvent::ToggleHubs).unwrap();
tx.send(WatchEvent::DrawDevices).unwrap();
}
(KeyCode::Char('#'), _) => {
tx.send(WatchEvent::EditFilter(FilterField::VidPid))
.unwrap();
tx.send(WatchEvent::DrawDevices).unwrap();
}
(KeyCode::Char('c'), _) => {
tx.send(WatchEvent::EditFilter(FilterField::Class)).unwrap();
tx.send(WatchEvent::DrawDevices).unwrap();
}
(KeyCode::Char('s'), KeyModifiers::NONE) => {
tx.send(WatchEvent::EditFilter(FilterField::Serial))
.unwrap();
tx.send(WatchEvent::DrawDevices).unwrap();
}
(KeyCode::Char('b'), _) => {
tx.send(WatchEvent::EditBlock(BlockType::Device)).unwrap();
tx.send(WatchEvent::DrawEditBlocks).unwrap();
}
(KeyCode::Char('s'), KeyModifiers::CONTROL) => {
tx.send(WatchEvent::ConfirmSave(
"Save current settings to global config?".into(),
))
.unwrap();
}
(KeyCode::Char('d'), KeyModifiers::NONE) => {
tx.send(WatchEvent::ScrollDownHalf).unwrap();
tx.send(WatchEvent::Draw).unwrap();
}
(KeyCode::Char('u'), KeyModifiers::NONE) => {
tx.send(WatchEvent::ScrollUpHalf).unwrap();
tx.send(WatchEvent::Draw).unwrap();
}
_ => (),
};
}
fn process_key_event(
&self,
event: KeyEvent,
tx: mpsc::Sender<WatchEvent>,
print_settings: Arc<Mutex<PrintSettings>>,
) {
let KeyEvent {
code,
modifiers,
kind,
..
} = event;
if !matches!(kind, KeyEventKind::Press) {
return;
}
match (code, modifiers) {
(KeyCode::Char('c'), KeyModifiers::CONTROL) => {
tx.send(WatchEvent::Quit).unwrap();
}
(KeyCode::Esc, _) => {
tx.send(WatchEvent::Esc).unwrap();
}
(KeyCode::Char('d'), KeyModifiers::CONTROL) | (KeyCode::PageDown, _) => {
tx.send(WatchEvent::ScrollDownPage).unwrap();
tx.send(WatchEvent::Draw).unwrap();
}
(KeyCode::Char('u'), KeyModifiers::CONTROL) | (KeyCode::PageUp, _) => {
tx.send(WatchEvent::ScrollUpPage).unwrap();
tx.send(WatchEvent::Draw).unwrap();
}
_ => match self {
State::Normal | State::Help => {
Self::process_normal_mode(event, tx, print_settings);
}
State::EditingFilter { field, .. } => match (code, modifiers) {
(KeyCode::Char(c), _) => {
tx.send(WatchEvent::PushFilter(c)).unwrap();
match field {
FilterField::Serial | FilterField::Name => {
tx.send(WatchEvent::DrawDevices).unwrap()
}
_ => tx.send(WatchEvent::Draw).unwrap(),
}
}
(KeyCode::Backspace, _) => {
tx.send(WatchEvent::PopFilter).unwrap();
match field {
FilterField::Serial | FilterField::Name => {
tx.send(WatchEvent::DrawDevices).unwrap()
}
_ => tx.send(WatchEvent::Draw).unwrap(),
}
}
(KeyCode::Enter, _) => {
tx.send(WatchEvent::Enter).unwrap();
}
_ => {}
},
State::BlockEditor { block_type, .. } => match (code, modifiers) {
(KeyCode::Char('q'), _) => {
tx.send(WatchEvent::Esc).unwrap();
}
(KeyCode::Up, _) | (KeyCode::Char('k'), _) => {
tx.send(WatchEvent::MoveUp(1)).unwrap();
tx.send(WatchEvent::DrawEditBlocks).unwrap();
}
(KeyCode::Down, _) | (KeyCode::Char('j'), _) => {
tx.send(WatchEvent::MoveDown(1)).unwrap();
tx.send(WatchEvent::DrawEditBlocks).unwrap();
}
(KeyCode::Tab, KeyModifiers::NONE) | (KeyCode::Char('b'), _) => {
tx.send(WatchEvent::EditBlock(block_type.next())).unwrap();
tx.send(WatchEvent::DrawEditBlocks).unwrap();
}
(KeyCode::Tab, KeyModifiers::SHIFT) => {
tx.send(WatchEvent::EditBlock(block_type.prev())).unwrap();
tx.send(WatchEvent::DrawEditBlocks).unwrap();
}
(KeyCode::Char('1'), _) => {
tx.send(WatchEvent::EditBlock(BlockType::Bus)).unwrap();
tx.send(WatchEvent::DrawEditBlocks).unwrap();
}
(KeyCode::Char('2'), _) => {
tx.send(WatchEvent::EditBlock(BlockType::Device)).unwrap();
tx.send(WatchEvent::DrawEditBlocks).unwrap();
}
(KeyCode::Char('3'), _) => {
tx.send(WatchEvent::EditBlock(BlockType::Config)).unwrap();
tx.send(WatchEvent::DrawEditBlocks).unwrap();
}
(KeyCode::Char('4'), _) => {
tx.send(WatchEvent::EditBlock(BlockType::Interface))
.unwrap();
tx.send(WatchEvent::DrawEditBlocks).unwrap();
}
(KeyCode::Char('5'), _) => {
tx.send(WatchEvent::EditBlock(BlockType::Endpoint)).unwrap();
tx.send(WatchEvent::DrawEditBlocks).unwrap();
}
(KeyCode::Char(' '), _) => {
tx.send(WatchEvent::ToggleBlock).unwrap();
tx.send(WatchEvent::DrawEditBlocks).unwrap();
}
(KeyCode::Enter, _) => {
tx.send(WatchEvent::WriteEditBlocks).unwrap();
tx.send(WatchEvent::Enter).unwrap();
}
(KeyCode::Char('<'), _)
| (KeyCode::Char(','), _)
| (KeyCode::Char('h'), _)
| (KeyCode::Left, _)
| (KeyCode::Char('['), _) => {
tx.send(WatchEvent::MoveBlockUp(1)).unwrap();
tx.send(WatchEvent::DrawEditBlocks).unwrap();
}
(KeyCode::Char('>'), _)
| (KeyCode::Char('.'), _)
| (KeyCode::Char('l'), _)
| (KeyCode::Right, _)
| (KeyCode::Char(']'), _) => {
tx.send(WatchEvent::MoveBlockDown(1)).unwrap();
tx.send(WatchEvent::DrawEditBlocks).unwrap();
}
_ => (),
},
State::Confirm(_, n) => match (code, modifiers) {
(KeyCode::Char('n'), _) | (KeyCode::Esc, _) | (KeyCode::Char('N'), _) => {
tx.send(WatchEvent::Esc).unwrap();
}
_ => {
tx.send(n.clone()).unwrap();
tx.send(WatchEvent::Esc).unwrap();
}
},
e => {
log::debug!("Exiting state {:?} on other key event", e);
tx.send(WatchEvent::Quit).unwrap();
}
},
}
}
}
impl Display {
fn toggle_selected(&self, all: bool) {
match self.selected_item.as_ref() {
Some(LineItem::Device(path)) => {
if let Some(node) = self.spusb.lock().unwrap().get_node_mut(path) {
if all {
node.set_all_expanded(!node.is_expanded());
} else {
node.toggle_expanded();
}
}
}
Some(LineItem::Config(path)) => {
if let Some(config) = self.spusb.lock().unwrap().get_config_mut(&path.0, path.1) {
if all {
config.set_all_expanded(!config.is_expanded());
} else {
config.toggle_expanded();
}
}
}
Some(LineItem::Interface(path)) => {
if let Some(interface) = self.spusb.lock().unwrap().get_interface_mut(path) {
if all {
interface.set_all_expanded(!interface.is_expanded());
} else {
interface.toggle_expanded();
}
}
}
Some(LineItem::Endpoint(path)) => {
if let Some(endpoint) = self.spusb.lock().unwrap().get_endpoint_mut(path) {
endpoint.toggle_expanded();
}
}
_ => (),
}
}
fn prepare_devices(&mut self) {
let print_settings = self.print_settings.lock().unwrap();
let mut spusb = self.spusb.lock().unwrap();
cyme::display::prepare(&mut spusb, Some(&self.filter), &print_settings);
}
fn move_up(&mut self, n: usize) {
match &mut *self.state.lock().unwrap() {
State::BlockEditor { selected_index, .. } => {
*selected_index = selected_index.saturating_sub(n);
self.selected_line = Some(*selected_index);
}
State::Normal => {
let li = self
.line_context
.iter()
.enumerate()
.rev()
.skip(
self.line_context.len().saturating_sub(
self.selected_line
.unwrap_or(n)
.saturating_sub(n.saturating_sub(1)),
),
)
.find(|(_, l)| !(matches!(l, LineItem::None) || matches!(l, LineItem::Bus(_))));
if let Some((i, item)) = li {
self.selected_line = Some(i);
self.selected_item = Some(item.clone());
}
}
_ => (),
};
log::debug!(
"Selected line: {:?} ({:?})",
self.selected_line,
self.selected_item
);
}
fn move_down(&mut self, n: usize) {
match &mut *self.state.lock().unwrap() {
State::BlockEditor {
selected_index,
blocks,
..
} => {
*selected_index = selected_index
.saturating_add(n)
.min(blocks.len().saturating_sub(1));
self.selected_line = Some(*selected_index);
}
State::Normal => {
let li = self
.line_context
.iter()
.enumerate()
.skip(self.selected_line.map_or(n, |i| i.saturating_add(n)))
.find(|(_, l)| !(matches!(l, LineItem::None) || matches!(l, LineItem::Bus(_))));
if let Some((i, item)) = li {
self.selected_line = Some(i);
self.selected_item = Some(item.clone());
}
}
_ => (),
}
log::debug!(
"Selected line: {:?} ({:?})",
self.selected_line,
self.selected_item
);
}
fn scroll_check_selected_visable(&mut self) {
if let Some(l) = self.selected_line {
if l < self.scroll_offset {
self.move_down(self.scroll_offset - l);
} else if l >= self.scroll_offset + self.available_rows {
self.move_up(l - (self.scroll_offset + self.available_rows) + 1);
}
}
}
fn draw_devices(&mut self) -> Result<()> {
self.buffer.clear();
let mut dw = DisplayWriter::new(&mut self.buffer);
dw.set_raw_mode(true);
{
let spusb = self.spusb.lock().unwrap();
let print_settings = self.print_settings.lock().unwrap();
dw.print_sp_usb(&spusb, &print_settings);
}
self.line_context = dw.line_context().to_owned();
if let Some(selected_device) = self.selected_item.as_ref() {
if let Some((i, _)) = self
.line_context
.iter()
.enumerate()
.find(|(_, l)| *l == selected_device)
{
self.selected_line = Some(i);
}
} else {
self.selected_line = None;
}
self.draw()
}
fn draw_help(&mut self) -> Result<()> {
self.buffer.clear();
writeln!(
self.buffer,
r#" {} v{} - USB Device Watcher
vim bindings where possible!
[q]/[Esc]: Exit program/abort mode
[v]: Cycle verbosity
[t]: Toggle tree
[h]: Toggle headings
[o]: Toggle decimal/hex
[p]: Cycle sort mode
[b]: Enter block editor
[/]: Edit name filter
[s]: Edit serial filter
[#]: Edit VID:PID filter
[c]: Edit class filter
[j]/[k], Up/Down: Move selection
[u]/[d], PgUp/PgDn Ctrl+u/Ctrl+d: Scroll page
[CR]: Expand selected/Accept or commit changes
[space]: Toggle selected
[C-s]: Save current display settings to cyme config.json
[C-c]: Exit program
[?]: Show this help
"#,
env!("CARGO_PKG_NAME"),
env!("CARGO_PKG_VERSION")
)?;
self.draw()
}
fn draw_header<W: Write>(&mut self, writer: &mut W) -> Result<()> {
let (term_width, _) = self.terminal_size;
let mut header = match &*self.state.lock().unwrap() {
State::BlockEditor { block_type, .. } => {
format!(
" EDITING BLOCKS: [{}] {}",
block_type.key_number(),
block_type
)
}
_ => {
format!(
" FILTERS Name={:?} Serial={:?} VID:PID={}:{} Class={:?}",
self.filter.filters.first().and_then(|f| f.name.as_ref()),
self.filter.filters.first().and_then(|f| f.serial.as_ref()),
self.filter
.filters
.first()
.and_then(|f| f.vid)
.map_or("".to_string(), |v| format!("{v:04x}")),
self.filter
.filters
.first()
.and_then(|f| f.pid)
.map_or("".to_string(), |p| format!("{p:04x}")),
self.filter.filters.first().and_then(|f| f.class),
)
}
};
truncate_string(&mut header, term_width as usize);
execute!(
writer,
cursor::MoveTo(0, 0),
terminal::Clear(terminal::ClearType::CurrentLine),
Print(
format!("{:<width$}", header, width = term_width as usize)
.bold()
.black()
.on_blue()
),
)?;
Ok(())
}
fn draw_footer<W: Write>(&mut self, writer: &mut W) -> Result<()> {
let (term_width, term_height) = self.terminal_size;
let footer = match &*self.state.lock().unwrap() {
State::EditingFilter { field, buffer, .. } => {
let mut footer = match field {
FilterField::Name => "Filter Name: ".to_string(),
FilterField::Serial => "Filter Serial: ".to_string(),
FilterField::VidPid => "Filter VID:PID: ".to_string(),
FilterField::Class => "Filter Class: ".to_string(),
};
if let Some(buffer) = buffer {
footer.push_str(buffer);
}
format!("{:<width$}", footer, width = term_width as usize)
.black()
.on_yellow()
}
State::Error(msg) => format!("{:<width$}", msg, width = term_width as usize)
.bold()
.red()
.on_white(),
State::Confirm(msg, _) => format!("{:<width$}", format!("{} Y/n", msg), width = term_width as usize)
.bold()
.black()
.on_white(),
State::BlockEditor { .. } => {
let mut footer = String::from(" [Up/Down]-Navigate [Space]-Toggle [<]/[>]-Move [Tab]-Switch [Enter]-Accept [q]-Exit");
truncate_string(&mut footer, term_width as usize);
format!("{:<width$}", footer, width = term_width as usize)
.bold()
.black()
.on_green()
}
_ => {
let print_settings = self.print_settings.lock().unwrap();
let verbosity = (print_settings.verbosity).to_string();
let mut footer = format!(
" [q]-Quit [b]-Edit Blocks [v]-Verbosity-(→{}) [t]-Tree-(→{}) [h]-Headings-(→{}) [o]-Decimal-(→{}) [p]-Sort-(→{})",
verbosity,
if print_settings.tree { "X" } else { "O" },
if print_settings.headings { "X" } else { "O" },
if print_settings.decimal { "X" } else { "O" },
if print_settings.sort_devices == Sort::BranchPosition {
"D#"
} else {
"P#"
}
);
truncate_string(&mut footer, term_width as usize);
format!("{:<width$}", footer, width = term_width as usize)
.bold()
.black()
.on_green()
}
}.to_string();
execute!(
writer,
cursor::MoveTo(0, term_height - 1),
terminal::Clear(terminal::ClearType::CurrentLine),
Print(footer),
)?;
Ok(())
}
fn prepare_edit_blocks(&mut self, block_type: BlockType) {
let print_settings = self.print_settings.lock().unwrap();
let ct = &print_settings.colours;
let blocks: Vec<(ColoredString, bool)> = match block_type {
BlockType::Device => {
let enabled_blocks = print_settings.device_blocks.as_ref().unwrap();
enabled_blocks
.iter()
.map(|b| (b, true))
.chain(
DeviceBlocks::all_blocks()
.iter()
.filter(|b| !enabled_blocks.contains(b))
.map(|b| (b, false)),
)
.map(|(b, enabled)| {
let cs = if let Some(ct) = ct.as_ref() {
let block = b.to_possible_value().unwrap().get_name().to_string();
b.colour(&block, ct)
} else {
b.to_possible_value().unwrap().get_name().into()
};
(cs, enabled)
})
.collect()
}
BlockType::Bus => {
let enabled_blocks = print_settings.bus_blocks.as_ref().unwrap();
enabled_blocks
.iter()
.map(|b| (b, true))
.chain(
BusBlocks::all_blocks()
.iter()
.filter(|b| !enabled_blocks.contains(b))
.map(|b| (b, false)),
)
.map(|(b, enabled)| {
let cs = if let Some(ct) = ct.as_ref() {
let block = b.to_possible_value().unwrap().get_name().to_string();
b.colour(&block, ct)
} else {
b.to_possible_value().unwrap().get_name().into()
};
(cs, enabled)
})
.collect()
}
BlockType::Config => {
let enabled_blocks = print_settings.config_blocks.as_ref().unwrap();
enabled_blocks
.iter()
.map(|b| (b, true))
.chain(
ConfigurationBlocks::all_blocks()
.iter()
.filter(|b| !enabled_blocks.contains(b))
.map(|b| (b, false)),
)
.map(|(b, enabled)| {
let cs = if let Some(ct) = ct.as_ref() {
let block = b.to_possible_value().unwrap().get_name().to_string();
b.colour(&block, ct)
} else {
b.to_possible_value().unwrap().get_name().into()
};
(cs, enabled)
})
.collect()
}
BlockType::Interface => {
let enabled_blocks = print_settings.interface_blocks.as_ref().unwrap();
enabled_blocks
.iter()
.map(|b| (b, true))
.chain(
InterfaceBlocks::all_blocks()
.iter()
.filter(|b| !enabled_blocks.contains(b))
.map(|b| (b, false)),
)
.map(|(b, enabled)| {
let cs = if let Some(ct) = ct.as_ref() {
let block = b.to_possible_value().unwrap().get_name().to_string();
b.colour(&block, ct)
} else {
b.to_possible_value().unwrap().get_name().into()
};
(cs, enabled)
})
.collect()
}
BlockType::Endpoint => {
let enabled_blocks = print_settings.endpoint_blocks.as_ref().unwrap();
enabled_blocks
.iter()
.map(|b| (b, true))
.chain(
EndpointBlocks::all_blocks()
.iter()
.filter(|b| !enabled_blocks.contains(b))
.map(|b| (b, false)),
)
.map(|(b, enabled)| {
let cs = if let Some(ct) = ct.as_ref() {
let block = b.to_possible_value().unwrap().get_name().to_string();
b.colour(&block, ct)
} else {
b.to_possible_value().unwrap().get_name().into()
};
(cs, enabled)
})
.collect()
}
};
let index =
if let State::BlockEditor { selected_index, .. } = &mut *self.state.lock().unwrap() {
(*selected_index).min(blocks.len() - 1)
} else {
0
};
let new_mode = State::BlockEditor {
block_type,
selected_index: index,
blocks,
};
self.selected_line = Some(index);
*self.state.lock().unwrap() = new_mode;
}
fn write_edit_blocks(&mut self) {
if let State::BlockEditor {
block_type, blocks, ..
} = &*self.state.lock().unwrap()
{
let mut print_settings = self.print_settings.lock().unwrap();
match block_type {
BlockType::Device => {
print_settings.device_blocks = Some(
blocks
.iter()
.filter(|(_, enabled)| *enabled)
.map(|b| DeviceBlocks::from_str(&b.0, true).unwrap())
.collect(),
);
}
BlockType::Bus => {
print_settings.bus_blocks = Some(
blocks
.iter()
.filter(|(_, enabled)| *enabled)
.map(|b| BusBlocks::from_str(&b.0, true).unwrap())
.collect(),
);
}
BlockType::Config => {
print_settings.config_blocks = Some(
blocks
.iter()
.filter(|(_, enabled)| *enabled)
.map(|b| ConfigurationBlocks::from_str(&b.0, true).unwrap())
.collect(),
);
}
BlockType::Interface => {
print_settings.interface_blocks = Some(
blocks
.iter()
.filter(|(_, enabled)| *enabled)
.map(|b| InterfaceBlocks::from_str(&b.0, true).unwrap())
.collect(),
);
}
BlockType::Endpoint => {
print_settings.endpoint_blocks = Some(
blocks
.iter()
.filter(|(_, enabled)| *enabled)
.map(|b| EndpointBlocks::from_str(&b.0, true).unwrap())
.collect(),
);
}
}
};
}
fn draw_edit_blocks(&mut self) -> Result<()> {
self.buffer.clear();
if let State::BlockEditor {
selected_index,
blocks,
..
} = &mut *self.state.lock().unwrap()
{
for (i, (block, enabled)) in blocks.iter().enumerate() {
let block_string = if i == *selected_index {
block.clone().normal().to_string()
} else {
block.to_string()
};
if *enabled {
writeln!(self.buffer, " [x] {block_string}")?;
} else {
writeln!(self.buffer, " [ ] {block_string}")?;
}
}
}
self.draw()
}
fn draw(&mut self) -> Result<()> {
let mut stdout = stdout();
self.terminal_size = terminal::size().unwrap_or((80, 24));
let (_, term_height) = self.terminal_size;
self.available_rows = term_height.saturating_sub(2) as usize;
execute!(
stdout,
cursor::MoveTo(0, 0),
terminal::Clear(terminal::ClearType::All),
)
.map_err(|e| Error::new(ErrorKind::Other("crossterm"), &e.to_string()))?;
self.draw_header(&mut stdout)?;
let output = String::from_utf8_lossy(&self.buffer);
let lines: Vec<String> = output.lines().map(|line| line.to_string()).collect();
self.lines = lines.len();
self.max_offset = self.lines.saturating_sub(self.available_rows);
self.scroll_offset = (self.scroll_offset).min(self.max_offset);
for (i, line) in lines
.iter()
.enumerate()
.skip(self.scroll_offset)
.take(self.available_rows)
{
if self.selected_line == Some(i) && !matches!(&*self.state.lock().unwrap(), State::Help)
{
let stripped_line = strip_ansi_codes(line);
if self.print_settings.lock().unwrap().colours.is_none()
|| env::var("NO_COLOR").is_ok_and(|v| v == "1")
{
let mut indicated = stripped_line.chars().skip(1).collect::<String>();
indicated.insert(0, '>');
write!(stdout, "{}\n\r", indicated.bold().on_bright_purple())?;
} else {
write!(stdout, "{}\n\r", stripped_line.bold().on_bright_purple())?;
}
} else {
write!(stdout, "{line}\n\r")?;
}
}
self.draw_footer(&mut stdout)?;
stdout.flush()?;
Ok(())
}
}