use std::fs;
use crate::batbelt::bat_dialoguer::BatDialoguer;
use crate::batbelt::git::git_commit::GitCommit;
use crate::batbelt::metadata::enums_source_code_metadata::EnumMetadataType;
use crate::batbelt::metadata::functions_source_code_metadata::{
FunctionMetadataType, FunctionSourceCodeMetadata,
};
use crate::batbelt::metadata::miro_metadata::{MiroCodeOverhaulMetadata, SignerInfo, SignerType};
use crate::batbelt::metadata::structs_source_code_metadata::StructMetadataType;
use crate::batbelt::metadata::traits_source_code_metadata::TraitMetadataType;
use crate::batbelt::metadata::{
BatMetadata, BatMetadataCommit, BatMetadataEnvVariables, BatMetadataParser, BatMetadataType,
MiroMetadata, SourceCodeMetadata,
};
use crate::batbelt::miro::connector::{create_connector, ConnectorOptions};
use crate::batbelt::miro::frame::{MiroCodeOverhaulConfig, MiroFrame};
use crate::batbelt::miro::frame::{
MIRO_BOARD_COLUMNS, MIRO_FRAME_HEIGHT, MIRO_FRAME_WIDTH, MIRO_INITIAL_X, MIRO_INITIAL_Y,
};
use crate::batbelt::miro::image::{MiroImage, MiroImageType};
use crate::batbelt::miro::sticky_note::MiroStickyNote;
use crate::batbelt::miro::MiroConfig;
use crate::batbelt::parser::code_overhaul_parser::CodeOverhaulParser;
use crate::batbelt::parser::entrypoint_parser::EntrypointParser;
use crate::batbelt::parser::function_parser::FunctionParser;
use crate::batbelt::parser::solana_account_parser::SolanaAccountType;
use crate::batbelt::parser::source_code_parser::{SourceCodeParser, SourceCodeScreenshotOptions};
use crate::batbelt::path::{BatFile, BatFolder};
use crate::batbelt::templates::code_overhaul_template::CoderOverhaulTemplatePlaceholders;
use crate::batbelt::BatEnumerator;
use crate::commands::{BatCommandEnumerator, CommandResult};
use crate::config::BatConfig;
use crate::{batbelt, Suggestion};
use clap::Subcommand;
use colored::Colorize;
use error_stack::{FutureExt, IntoReport, Report, Result, ResultExt};
use inflector::Inflector;
use regex::Regex;
use super::CommandError;
#[derive(
Subcommand, Debug, strum_macros::Display, PartialEq, Clone, strum_macros::EnumIter, Default,
)]
pub enum MiroCommand {
#[default]
CodeOverhaulFrames,
CodeOverhaulScreenshots {
#[arg(long)]
entry_point_name: Option<String>,
},
EntrypointScreenshots,
SourceCodeScreenshots,
FunctionDependencies,
}
impl BatEnumerator for MiroCommand {}
impl BatCommandEnumerator for MiroCommand {
fn execute_command(&self) -> CommandResult<()> {
unimplemented!()
}
fn check_metadata_is_initialized(&self) -> bool {
true
}
fn check_correct_branch(&self) -> bool {
false
}
}
impl MiroCommand {
pub async fn execute_command(&self) -> Result<(), CommandError> {
MiroConfig::check_miro_enabled().change_context(CommandError)?;
match self {
MiroCommand::CodeOverhaulFrames => self.deploy_co_frames().await,
MiroCommand::CodeOverhaulScreenshots { entry_point_name } => {
self.deploy_co_screenshots(entry_point_name.clone()).await
}
MiroCommand::EntrypointScreenshots => self.entrypoint_screenshots().await,
MiroCommand::SourceCodeScreenshots => self.source_code_screenshots().await,
MiroCommand::FunctionDependencies => self.function_dependencies().await,
}
}
async fn entrypoint_screenshots(&self) -> Result<(), CommandError> {
let code_overhaul_frame_title_regex = Regex::new(r"co: [A-Za-z0-9_]+")
.into_report()
.change_context(CommandError)?;
let selected_miro_frame =
MiroFrame::prompt_select_frame(Some(vec![code_overhaul_frame_title_regex]))
.await
.change_context(CommandError)?;
let bat_config = BatConfig::get_config().change_context(CommandError)?;
let entrypoints_names = if bat_config.is_multi_program() {
let program_name = bat_config
.prompt_select_program()
.change_context(CommandError)?;
let lib_path = bat_config
.get_program_lib_path_by_name(&program_name)
.unwrap();
EntrypointParser::get_entrypoint_names_filtered(true, Some(&lib_path))
.change_context(CommandError)?
} else {
EntrypointParser::get_entrypoint_names_from_program_lib(true)
.change_context(CommandError)?
};
let prompt_text = "Select the entry points to deploy";
let selected_entrypoints_index =
batbelt::bat_dialoguer::multiselect(prompt_text, entrypoints_names.clone(), None)
.unwrap();
let entrypoint_sc_options = SourceCodeScreenshotOptions {
include_path: false,
offset_to_start_line: true,
filter_comments: true,
font_size: None,
filters: None,
show_line_number: true,
};
let context_accounts_sc_options = SourceCodeScreenshotOptions {
include_path: false,
offset_to_start_line: false,
filter_comments: true,
font_size: None,
filters: None,
show_line_number: false,
};
let dependency_sc_options = SourceCodeScreenshotOptions {
include_path: true,
offset_to_start_line: true,
filter_comments: true,
font_size: None,
filters: None,
show_line_number: true,
};
let selected_entrypoints_amount = if selected_entrypoints_index.len() % 2 == 0 {
selected_entrypoints_index.len()
} else {
selected_entrypoints_index.len() + 1
};
let grid_amount = 24;
let height_grid = selected_miro_frame.height as i64 / grid_amount;
let (ep_multiplier, ca_multiplier, dep_multiplier) = (1, 2, 4);
for (index, selected_ep_index) in selected_entrypoints_index.iter().enumerate() {
let (x_position, ep_y_position, ca_y_position, dep_y_position) =
if index < selected_entrypoints_amount / 2 {
let x_position = (selected_miro_frame.width as i64
/ selected_entrypoints_amount as i64)
* (2 * index as i64 + 1);
(
x_position,
ep_multiplier * height_grid,
ca_multiplier * height_grid,
dep_multiplier * height_grid,
)
} else {
let x_position = (selected_miro_frame.width as i64
/ selected_entrypoints_amount as i64)
* (2 * (index as i64 - (selected_entrypoints_amount as i64 / 2)) + 1);
(
x_position,
(grid_amount - ep_multiplier) * height_grid,
(grid_amount - ca_multiplier) * height_grid,
(grid_amount - dep_multiplier) * height_grid,
)
};
let selected_entrypoint = &entrypoints_names[*selected_ep_index];
let entrypoint = EntrypointParser::new_from_name(selected_entrypoint.as_str())
.change_context(CommandError)?;
let ep_source_code = entrypoint.entry_point_function.to_source_code_parser(Some(
miro_command_functions::parse_screenshot_name(
&entrypoint.entry_point_function.name,
&selected_miro_frame.title,
),
));
let ca_source_code = entrypoint.context_accounts.to_source_code_parser(Some(
miro_command_functions::parse_screenshot_name(
&entrypoint.context_accounts.name,
&selected_miro_frame.title,
),
));
let ep_image = ep_source_code
.deploy_screenshot_to_miro_frame(
selected_miro_frame.clone(),
x_position,
ep_y_position,
entrypoint_sc_options.clone(),
)
.await
.change_context(CommandError)?;
let ca_image = ca_source_code
.deploy_screenshot_to_miro_frame(
selected_miro_frame.clone(),
x_position,
ca_y_position,
context_accounts_sc_options.clone(),
)
.await
.change_context(CommandError)?;
create_connector(&ep_image.item_id, &ca_image.item_id, None)
.await
.change_context(CommandError)?;
let mut prev_image_id = ca_image.item_id.clone();
for (dep_idx, dep_function) in entrypoint.dependencies.iter().enumerate() {
let dep_source_code = dep_function.to_source_code_parser(Some(
miro_command_functions::parse_screenshot_name(
&dep_function.name,
&selected_miro_frame.title,
),
));
let y_offset = dep_y_position + (dep_idx as i64 * 400);
let dep_image = dep_source_code
.deploy_screenshot_to_miro_frame(
selected_miro_frame.clone(),
x_position,
y_offset,
dependency_sc_options.clone(),
)
.await
.change_context(CommandError)?;
create_connector(&prev_image_id, &dep_image.item_id, None)
.await
.change_context(CommandError)?;
prev_image_id = dep_image.item_id;
}
}
Ok(())
}
async fn source_code_screenshots(&self) -> Result<(), CommandError> {
let selected_miro_frame = MiroFrame::prompt_select_frame(None)
.await
.change_context(CommandError)?;
miro_command_functions::prompt_deploy_source_code(selected_miro_frame, false).await?;
Ok(())
}
async fn function_dependencies(&self) -> Result<(), CommandError> {
let selected_miro_frame = MiroFrame::prompt_select_frame(None)
.await
.change_context(CommandError)?;
let bat_metadata = BatMetadata::read_metadata().change_context(CommandError)?;
let function_metadata_vec = bat_metadata.source_code.functions_source_code.clone();
let mut keep_deploying = true;
let mut deployed_function_ids: std::collections::HashSet<String> =
std::collections::HashSet::new();
let mut id_to_image: std::collections::HashMap<String, String> =
std::collections::HashMap::new();
let mut program_names: Vec<String> = function_metadata_vec
.iter()
.map(|f| {
let without_prefix = f.path.trim_start_matches("../");
let prefix = without_prefix
.split("/src/")
.next()
.unwrap_or(without_prefix);
prefix.to_string()
})
.collect::<std::collections::BTreeSet<_>>()
.into_iter()
.collect();
program_names.sort();
while keep_deploying {
let program_prompt = "Select the program containing the function";
let selected_program_index =
batbelt::bat_dialoguer::select(program_prompt, program_names.clone(), None)?;
let selected_program = &program_names[selected_program_index];
let filtered_functions: Vec<&FunctionSourceCodeMetadata> = function_metadata_vec
.iter()
.filter(|f| {
let without_prefix = f.path.trim_start_matches("../");
let prefix = without_prefix
.split("/src/")
.next()
.unwrap_or(without_prefix);
prefix == selected_program.as_str()
})
.collect();
let function_names_vec = filtered_functions
.iter()
.map(|f_meta| {
miro_command_functions::get_formatted_path(
f_meta.name.clone(),
f_meta.path.clone(),
f_meta.start_line_index,
)
})
.collect::<Result<Vec<_>, _>>()?;
let prompt_text = "Select the root function to deploy";
let selected_function_index =
batbelt::bat_dialoguer::select(prompt_text, function_names_vec.clone(), None)?;
let root_function = filtered_functions[selected_function_index].clone();
let function_sc_options = SourceCodeScreenshotOptions {
include_path: true,
offset_to_start_line: true,
filter_comments: false,
font_size: None,
filters: None,
show_line_number: true,
};
if !deployed_function_ids.contains(&root_function.metadata_id) {
let root_image = root_function
.to_source_code_parser(Some(miro_command_functions::parse_screenshot_name(
&root_function.name,
&selected_miro_frame.title,
)))
.deploy_screenshot_to_miro_frame(
selected_miro_frame.clone(),
(selected_miro_frame.height as i64) / 2,
-(selected_miro_frame.width as i64) / 2,
function_sc_options.clone(),
)
.await
.change_context(CommandError)?;
id_to_image.insert(
root_function.metadata_id.clone(),
root_image.item_id.clone(),
);
deployed_function_ids.insert(root_function.metadata_id.clone());
}
let direct_deps_of = |function_id: &str| -> Vec<(String, String)> {
match bat_metadata.get_functions_dependencies_metadata_by_function_metadata_id(
function_id.to_string(),
) {
Ok(fd_meta) => fd_meta
.dependencies
.iter()
.map(|d| (d.function_metadata_id.clone(), d.function_name.clone()))
.collect(),
Err(_) => vec![],
}
};
let mut bfs_queue: std::collections::VecDeque<(String, String)> =
std::collections::VecDeque::new();
bfs_queue.push_back((
root_function.metadata_id.clone(),
root_function.name.clone(),
));
const CASCADE_START_X: i64 = (MIRO_FRAME_WIDTH as i64) / 2 - 400;
const CASCADE_START_Y: i64 = (MIRO_FRAME_HEIGHT as i64) / 2 - 400;
const CASCADE_STEP: i64 = 60;
while let Some((caller_id, caller_name)) = bfs_queue.pop_front() {
let direct_deps = direct_deps_of(&caller_id);
let new_deps: Vec<(String, String)> = direct_deps
.into_iter()
.filter(|(id, _)| !deployed_function_ids.contains(id))
.collect();
if new_deps.is_empty() {
continue;
}
let mut new_dep_functions: Vec<FunctionSourceCodeMetadata> = vec![];
for (dep_id, _) in &new_deps {
match bat_metadata.source_code.get_function_by_id(dep_id.clone()) {
Ok(func_meta) => new_dep_functions.push(func_meta),
Err(_) => {
log::warn!(
"Could not find function metadata for dependency id {}",
dep_id
);
}
}
}
if new_dep_functions.is_empty() {
continue;
}
let prompt = format!(
"Press Enter to deploy {} dependencies of `{}`",
new_dep_functions.len(),
caller_name
);
BatDialoguer::input_with_default(prompt, "".to_string())
.change_context(CommandError)?;
for (idx, dep_function) in new_dep_functions.iter().enumerate() {
let dep_sc = dep_function.to_source_code_parser(Some(
miro_command_functions::parse_screenshot_name(
&dep_function.name,
&selected_miro_frame.title,
),
));
let cascade_x = CASCADE_START_X + (idx as i64) * CASCADE_STEP;
let cascade_y = CASCADE_START_Y + (idx as i64) * CASCADE_STEP;
let dep_image = dep_sc
.deploy_screenshot_to_miro_frame(
selected_miro_frame.clone(),
cascade_x,
cascade_y,
SourceCodeScreenshotOptions {
include_path: true,
offset_to_start_line: true,
filter_comments: false,
font_size: Some(
crate::batbelt::parser::code_overhaul_parser::DEPENDENCY_SCREENSHOT_FONT_SIZE,
),
filters: None,
show_line_number: true,
},
)
.await
.change_context(CommandError)?;
if let Some(caller_image_id) = id_to_image.get(&caller_id) {
batbelt::miro::connector::create_connector(
caller_image_id,
&dep_image.item_id,
None,
)
.await
.change_context(CommandError)?;
}
id_to_image.insert(dep_function.metadata_id.clone(), dep_image.item_id.clone());
deployed_function_ids.insert(dep_function.metadata_id.clone());
bfs_queue
.push_back((dep_function.metadata_id.clone(), dep_function.name.clone()));
}
}
let prompt_text = format!(
"Do you want to {} in the {} frame?",
"continue creating screenshots".yellow(),
selected_miro_frame.title.yellow()
);
keep_deploying = batbelt::bat_dialoguer::select_yes_or_no(&prompt_text).unwrap();
}
Ok(())
}
async fn deploy_co_frames(&self) -> Result<(), CommandError> {
println!("Deploying code-overhaul frames to the Miro board");
let bat_config = BatConfig::get_config().change_context(CommandError)?;
let is_multi = bat_config.is_multi_program();
let programs_entrypoints: Vec<(Option<String>, Vec<String>)> = if is_multi {
let mut result = vec![];
for program_name in bat_config.get_program_names() {
let lib_path = bat_config
.get_program_lib_path_by_name(&program_name)
.unwrap();
let ep_names =
EntrypointParser::get_entrypoint_names_filtered(false, Some(&lib_path))
.change_context(CommandError)?;
result.push((Some(program_name), ep_names));
}
result
} else {
let ep_names = EntrypointParser::get_entrypoint_names_from_program_lib(false)
.change_context(CommandError)?;
vec![(None, ep_names)]
};
let mut global_index: usize = 0;
for (program_name, entry_point_names) in &programs_entrypoints {
for entrypoint_name in entry_point_names {
let frame_prefix = match program_name {
Some(pn) => format!("{}:{}", pn, entrypoint_name),
None => entrypoint_name.clone(),
};
match MiroMetadata::get_co_metadata_by_entrypoint_name(entrypoint_name.clone())
.change_context(CommandError)
{
Ok(miro_co_metadata) => {
match MiroFrame::new_from_item_id(&miro_co_metadata.miro_frame_id)
.await
.change_context(CommandError)
{
Ok(miro_frame) => {
println!(
"Frame already deployed for {}, \nurl: {}\n",
frame_prefix.clone().green(),
MiroFrame::get_frame_url_by_frame_id(&miro_frame.item_id)
.change_context(CommandError)?
);
}
Err(_) => {
println!(
"Incorrect frame deployed for {}, \nurl: {}\nUpdating the Miro metadata\n",
frame_prefix.clone().red(),
MiroFrame::get_frame_url_by_frame_id(&miro_co_metadata.miro_frame_id)
.change_context(CommandError)?
);
let new_frame = miro_command_functions::deploy_miro_frame_for_co(
entrypoint_name,
global_index,
program_name.as_deref(),
)
.await?;
let new_co_metadata = MiroCodeOverhaulMetadata {
metadata_id: miro_co_metadata.metadata_id.clone(),
entry_point_name: entrypoint_name.clone(),
miro_frame_id: new_frame.item_id.clone(),
images_deployed: false,
entry_point_image_id: "".to_string(),
context_accounts_image_id: "".to_string(),
validations_image_id: "".to_string(),
handler_image_id: "".to_string(),
dependency_image_ids: vec![],
signers: vec![],
};
new_co_metadata
.update_code_overhaul_metadata()
.change_context(CommandError)?;
}
}
}
Err(_) => {
let mut miro_co_metadata = MiroCodeOverhaulMetadata {
metadata_id: BatMetadata::create_metadata_id(),
entry_point_name: entrypoint_name.clone(),
miro_frame_id: "".to_string(),
images_deployed: false,
entry_point_image_id: "".to_string(),
context_accounts_image_id: "".to_string(),
validations_image_id: "".to_string(),
handler_image_id: "".to_string(),
dependency_image_ids: vec![],
signers: vec![],
};
let miro_frame = miro_command_functions::deploy_miro_frame_for_co(
entrypoint_name,
global_index,
program_name.as_deref(),
)
.await?;
miro_co_metadata.miro_frame_id = miro_frame.item_id.clone();
miro_co_metadata
.update_code_overhaul_metadata()
.change_context(CommandError)?;
}
}
global_index += 1;
}
}
GitCommit::UpdateMetadataJson {
bat_metadata_commit: BatMetadataCommit::MiroMetadataCommit,
}
.create_commit(true)
.change_context(CommandError)?;
Ok(())
}
async fn deploy_co_screenshots(&self, entry_point_name: Option<String>) -> CommandResult<()> {
Self::deploy_co_screenshots_with_program(entry_point_name, None).await
}
pub async fn deploy_co_screenshots_with_program(
entry_point_name: Option<String>,
program_name: Option<String>,
) -> CommandResult<()> {
MiroConfig::check_miro_enabled().change_context(CommandError)?;
let bat_metadata = BatMetadata::read_metadata().change_context(CommandError)?;
if bat_metadata.miro.code_overhaul.is_empty() {
let message = format!(
"Miro code-overhaul's metadata is not initialized yet.\n \
This action is {} to proceed with this function.",
"required".red()
);
let suggestion_message = format!(
"Run {} to deploy the code-overhaul frames",
"bat-cli miro deploy-co-frames".green()
);
return Err(Report::new(CommandError)
.attach_printable(message)
.attach(Suggestion(suggestion_message)));
}
let bat_config = BatConfig::get_config().change_context(CommandError)?;
let selected_program_name = if program_name.is_some() {
program_name
} else if bat_config.is_multi_program() {
Some(
bat_config
.prompt_select_program()
.change_context(CommandError)?,
)
} else {
None
};
let co_started_bat_folder = BatFolder::CodeOverhaulStarted {
program_name: selected_program_name.clone(),
};
let co_finished_bat_folder = BatFolder::CodeOverhaulFinished {
program_name: selected_program_name.clone(),
};
let started_files_names = co_started_bat_folder
.get_all_files_names(true, None, None)
.change_context(CommandError)?;
let finished_files_names = co_finished_bat_folder
.get_all_files_names(true, None, None)
.change_context(CommandError)?;
if started_files_names.is_empty() && finished_files_names.is_empty() {
return Err(Report::new(CommandError)
.attach_printable("code-overhaul's to-review and finished folders are empty"));
}
let mut co_files_names = vec![];
co_files_names.append(&mut started_files_names.clone());
co_files_names.append(&mut finished_files_names.clone());
co_files_names.sort();
let entrypoint_name = match entry_point_name {
None => {
let prompt_text = "Select the co file to deploy to Miro".to_string();
let selection = BatDialoguer::select(prompt_text, co_files_names.clone(), None)?;
let selected_file_name = co_files_names[selection].clone();
let entrypoint_name = selected_file_name.trim_end_matches(".md").to_string();
entrypoint_name
}
Some(ep_name) => {
let entrypoint_name = ep_name.trim_end_matches(".md").to_string();
let co_file_name = format!("{}.md", entrypoint_name.clone());
if !co_files_names.contains(&co_file_name) {
return Err(Report::new(CommandError).attach_printable(format!(
"code-overhaul's file with name {} not found on {} and {} folders",
co_file_name.clone(),
"to-review".bright_red(),
"finished".bright_red()
)));
}
entrypoint_name
}
};
let (co_miro_frame, mut miro_co_metadata) =
match MiroMetadata::get_co_metadata_by_entrypoint_name(entrypoint_name.clone()) {
Ok(co_meta) => {
let frame_id = co_meta.miro_frame_id.clone();
let miro_frame = MiroFrame::new_from_item_id(&frame_id)
.change_context(CommandError)
.await?;
println!(
"Deploying {} to {:#?}",
entrypoint_name.green(),
miro_frame.title
);
(miro_frame, co_meta)
}
Err(_) => {
let message = format!(
"Miro code-overhaul's metadata not found for {}.\n \
This action is {} to proceed with this function.",
entrypoint_name,
"required".red()
);
let suggestion_message = format!(
"Run {} to deploy the code-overhaul frames",
"bat-cli miro deploy-co".green()
);
return Err(Report::new(CommandError)
.attach_printable(message)
.attach(Suggestion(suggestion_message)));
}
};
if let Some(ref frame_url) = co_miro_frame.frame_url {
println!(
"Miro frame url for {}: {}",
miro_co_metadata.entry_point_name.clone().green(),
frame_url.green()
);
let miro_placeholder =
CoderOverhaulTemplatePlaceholders::CompleteWithMiroFrameUrl.to_placeholder();
let co_file_name = format!("{}.md", entrypoint_name);
let co_bat_file = if (BatFolder::CodeOverhaulStarted {
program_name: selected_program_name.clone(),
})
.get_all_files_names(true, None, None)
.unwrap_or_default()
.contains(&co_file_name)
{
BatFile::CodeOverhaulStarted {
file_name: co_file_name,
program_name: selected_program_name.clone(),
}
} else {
BatFile::CodeOverhaulFinished {
file_name: co_file_name,
program_name: selected_program_name.clone(),
}
};
if let Ok(content) = co_bat_file.read_content(true) {
if content.contains(&miro_placeholder) {
let new_content = content.replace(&miro_placeholder, frame_url);
let _ = co_bat_file.write_content(true, &new_content);
}
}
}
if !miro_co_metadata.images_deployed {
let entrypoint_parser =
EntrypointParser::new_from_name(&entrypoint_name).change_context(CommandError)?;
let co_parser = CodeOverhaulParser::new_from_entry_point_name_and_program(
entrypoint_name.clone(),
selected_program_name.clone(),
)
.change_context(CommandError)?;
let mut signers_info: Vec<SignerInfo> = vec![];
if !co_parser.signers.is_empty() {
for signer in co_parser.signers.clone().into_iter() {
let prompt_text = format!(
"is the signer {} a validated signer?",
signer.name.to_string().red()
);
let is_validated =
BatDialoguer::select_yes_or_no(prompt_text).change_context(CommandError)?;
let signer_type = if is_validated {
SignerType::Validated
} else {
SignerType::NotValidated
};
let signer_title = if is_validated {
format!("Validated signer:<br> <strong>{}</strong>", signer.name)
} else {
format!("Not validated signer:<br> <strong>{}</strong>", signer.name)
};
signers_info.push(SignerInfo {
signer_text: signer_title,
sticky_note_id: "".to_string(),
user_figure_id: "".to_string(),
signer_type,
})
}
} else {
signers_info.push(SignerInfo {
signer_text: SignerType::Permissionless.to_string(),
sticky_note_id: "".to_string(),
user_figure_id: "".to_string(),
signer_type: SignerType::Permissionless,
})
}
println!(
"Creating signers figures in Miro for {}",
entrypoint_name.green()
);
for (signer_index, signer) in signers_info.iter_mut().enumerate() {
let x_position = 550;
let y_position = (150 + signer_index * 270) as i64;
let width = 374;
let mut signer_sticky_note = MiroStickyNote::new(
&signer.signer_text,
signer.signer_type.get_sticky_note_color(),
&co_miro_frame.item_id,
x_position,
y_position,
width,
0,
);
signer_sticky_note
.deploy()
.await
.change_context(CommandError)?;
let user_figure_url = "https://mirostatic.com/app/static/12079327f83ff492.svg";
let y_position = (150 + signer_index * 270) as i64;
let mut user_figure = MiroImage::new_from_url(
user_figure_url,
&co_miro_frame.item_id,
150,
y_position,
200,
);
user_figure.deploy().await.change_context(CommandError)?;
*signer = SignerInfo {
signer_text: signer.signer_text.clone(),
sticky_note_id: signer_sticky_note.item_id,
user_figure_id: user_figure.item_id,
signer_type: signer.signer_type,
}
}
miro_co_metadata.signers = signers_info.clone();
let (entrypoint_x_position, entrypoint_y_position) =
MiroCodeOverhaulConfig::EntryPoint.get_positions();
let entrypoint_function_image = entrypoint_parser
.entry_point_function
.to_source_code_parser(Some(miro_command_functions::parse_screenshot_name(
&entrypoint_parser.entry_point_function.name,
&co_miro_frame.title,
)))
.deploy_screenshot_to_miro_frame(
co_miro_frame.clone(),
entrypoint_x_position,
entrypoint_y_position,
SourceCodeScreenshotOptions {
include_path: false,
offset_to_start_line: true,
filter_comments: false,
font_size: None,
filters: None,
show_line_number: true,
},
)
.await
.change_context(CommandError)?;
miro_co_metadata.entry_point_image_id = entrypoint_function_image.item_id.clone();
let validations_miro_image = co_parser
.deploy_new_validations_image_for_miro_co_frame(co_miro_frame.clone())
.await
.change_context(CommandError)?;
let ca_miro_image = co_parser
.deploy_new_context_accounts_image_for_miro_co_frame(co_miro_frame.clone())
.await
.change_context(CommandError)?;
miro_co_metadata.validations_image_id = validations_miro_image.item_id.clone();
miro_co_metadata.context_accounts_image_id = ca_miro_image.item_id.clone();
miro_co_metadata.images_deployed = true;
miro_co_metadata
.update_code_overhaul_metadata()
.change_context(CommandError)?;
GitCommit::UpdateMetadataJson {
bat_metadata_commit: BatMetadataCommit::MiroMetadataCommit,
}
.create_commit(true)
.change_context(CommandError)?;
println!("Connecting signers to entrypoint");
for signer_miro_ids in signers_info {
batbelt::miro::connector::create_connector(
&signer_miro_ids.user_figure_id,
&signer_miro_ids.sticky_note_id,
None,
)
.await
.change_context(CommandError)?;
batbelt::miro::connector::create_connector(
&signer_miro_ids.sticky_note_id,
&miro_co_metadata.entry_point_image_id,
Some(ConnectorOptions {
start_x_position: "100%".to_string(),
start_y_position: "50%".to_string(),
end_x_position: "0%".to_string(),
end_y_position: "50%".to_string(),
}),
)
.await
.change_context(CommandError)?;
}
println!("Connecting entrypoint screenshot to context accounts screenshot in Miro");
batbelt::miro::connector::create_connector(
&miro_co_metadata.entry_point_image_id,
&miro_co_metadata.context_accounts_image_id,
None,
)
.await
.change_context(CommandError)?;
println!("Connecting context accounts screenshot to validations screenshot in Miro");
batbelt::miro::connector::create_connector(
&miro_co_metadata.context_accounts_image_id,
&miro_co_metadata.validations_image_id,
None,
)
.await
.change_context(CommandError)?;
let bat_metadata_for_deps =
BatMetadata::read_metadata().change_context(CommandError)?;
let direct_deps_of = |function_id: &str| -> Vec<(String, String)> {
match bat_metadata_for_deps
.get_functions_dependencies_metadata_by_function_metadata_id(
function_id.to_string(),
) {
Ok(fd_meta) => fd_meta
.dependencies
.iter()
.map(|d| (d.function_metadata_id.clone(), d.function_name.clone()))
.collect(),
Err(_) => vec![],
}
};
let mut id_to_image: std::collections::HashMap<String, String> =
std::collections::HashMap::new();
id_to_image.insert(
entrypoint_parser.entry_point_function.metadata_id.clone(),
miro_co_metadata.entry_point_image_id.clone(),
);
let mut deployed_function_ids: std::collections::HashSet<String> =
std::collections::HashSet::new();
deployed_function_ids
.insert(entrypoint_parser.entry_point_function.metadata_id.clone());
let mut bfs_queue: std::collections::VecDeque<(String, String)> =
std::collections::VecDeque::new();
bfs_queue.push_back((
entrypoint_parser.entry_point_function.metadata_id.clone(),
entrypoint_parser.entry_point_function.name.clone(),
));
const CASCADE_START_X: i64 = (MIRO_FRAME_WIDTH as i64) / 2 - 400;
const CASCADE_START_Y: i64 = (MIRO_FRAME_HEIGHT as i64) / 2 - 400;
const CASCADE_STEP: i64 = 60;
let mut dependency_image_ids: Vec<String> = vec![];
const DEP_ARROW_COLORS: &[(&str, &str)] = &[
("#ff0000", "red"),
("#0000ff", "blue"),
("#00aa00", "green"),
("#ff8800", "orange"),
("#aa00ff", "purple"),
("#00cccc", "teal"),
("#ff00aa", "pink"),
("#888800", "olive"),
("#0088ff", "sky blue"),
("#aa5500", "brown"),
("#ff4444", "light red"),
("#4444ff", "light blue"),
];
let mut color_index: usize = 0;
while let Some((caller_id, caller_name)) = bfs_queue.pop_front() {
let direct_deps = direct_deps_of(&caller_id);
let new_deps: Vec<(String, String)> = direct_deps
.into_iter()
.filter(|(id, _)| !deployed_function_ids.contains(id))
.collect();
if new_deps.is_empty() {
continue;
}
let mut new_dep_functions: Vec<FunctionSourceCodeMetadata> = vec![];
for (dep_id, _) in &new_deps {
match bat_metadata_for_deps
.source_code
.get_function_by_id(dep_id.clone())
{
Ok(func_meta) => new_dep_functions.push(func_meta),
Err(_) => {
log::warn!(
"Could not find function metadata for dependency id {}",
dep_id
);
}
}
}
if new_dep_functions.is_empty() {
continue;
}
let (arrow_hex, arrow_name) =
DEP_ARROW_COLORS[color_index % DEP_ARROW_COLORS.len()];
color_index += 1;
let prompt = format!(
"Press Enter to deploy {} dependencies of `{}` (arrow color: {})",
new_dep_functions.len(),
caller_name,
arrow_name
);
BatDialoguer::input_with_default(prompt, "".to_string())
.change_context(CommandError)?;
for (idx, dep_function) in new_dep_functions.iter().enumerate() {
let dep_sc = dep_function.to_source_code_parser(Some(
miro_command_functions::parse_screenshot_name(
&dep_function.name,
&co_miro_frame.title,
),
));
let cascade_x = CASCADE_START_X + (idx as i64) * CASCADE_STEP;
let cascade_y = CASCADE_START_Y + (idx as i64) * CASCADE_STEP;
let dep_image = dep_sc
.deploy_screenshot_to_miro_frame(
co_miro_frame.clone(),
cascade_x,
cascade_y,
SourceCodeScreenshotOptions {
include_path: true,
offset_to_start_line: true,
filter_comments: false,
font_size: Some(
crate::batbelt::parser::code_overhaul_parser::DEPENDENCY_SCREENSHOT_FONT_SIZE,
),
filters: None,
show_line_number: true,
},
)
.await
.change_context(CommandError)?;
if let Some(caller_image_id) = id_to_image.get(&caller_id) {
batbelt::miro::connector::create_connector_with_color(
&dep_image.item_id,
caller_image_id,
None,
Some(arrow_hex),
)
.await
.change_context(CommandError)?;
}
id_to_image.insert(dep_function.metadata_id.clone(), dep_image.item_id.clone());
deployed_function_ids.insert(dep_function.metadata_id.clone());
dependency_image_ids.push(dep_image.item_id.clone());
bfs_queue
.push_back((dep_function.metadata_id.clone(), dep_function.name.clone()));
}
}
miro_co_metadata.dependency_image_ids = dependency_image_ids.clone();
miro_co_metadata
.update_code_overhaul_metadata()
.change_context(CommandError)?;
GitCommit::UpdateMetadataJson {
bat_metadata_commit: BatMetadataCommit::MiroMetadataCommit,
}
.create_commit(true)
.change_context(CommandError)?;
let bat_metadata = BatMetadata::read_metadata().change_context(CommandError)?;
let context_accounts_metadata = bat_metadata
.get_context_accounts_metadata_by_struct_source_code_metadata_id(
entrypoint_parser.context_accounts.metadata_id,
)
.change_context(CommandError)?;
let mut_program_owned_accounts = context_accounts_metadata
.context_accounts_info
.into_iter()
.filter(|ca_info| {
ca_info.is_mut
&& ca_info.solana_account_type == SolanaAccountType::ProgramStateAccount
});
for mut_account in mut_program_owned_accounts {
let struct_metadata_vec = SourceCodeMetadata::get_filtered_structs(
Some(mut_account.account_struct_name),
Some(StructMetadataType::SolanaAccount),
)
.change_context(CommandError)?;
if struct_metadata_vec.len() != 1 {
return Err(Report::new(CommandError).attach_printable(format!(
"Error looking for Solana Accounts, expected 1 result, got:\n{:#?}",
struct_metadata_vec
)));
}
let struct_metadata = struct_metadata_vec[0].clone();
struct_metadata
.to_source_code_parser(Some(miro_command_functions::parse_screenshot_name(
&struct_metadata.name,
&co_miro_frame.title,
)))
.deploy_screenshot_to_miro_frame(
co_miro_frame.clone(),
0,
co_miro_frame.height as i64,
SourceCodeScreenshotOptions {
include_path: true,
offset_to_start_line: true,
filter_comments: false,
font_size: None,
filters: None,
show_line_number: true,
},
)
.await
.change_context(CommandError)?;
}
for dep_function in &entrypoint_parser.dependencies {
let dep_function_parser = FunctionParser::new_from_metadata(dep_function.clone())
.change_context(CommandError)?;
for dep_function_parameter in dep_function_parser.parameters {
if let Ok(parameter_metadata_vec) = SourceCodeMetadata::get_filtered_structs(
Some(
dep_function_parameter
.parameter_type
.trim_start_matches("&")
.to_string(),
),
Some(StructMetadataType::Other),
) {
if parameter_metadata_vec.len() == 1 {
let parameter_metadata = parameter_metadata_vec[0].clone();
parameter_metadata
.to_source_code_parser(Some(
miro_command_functions::parse_screenshot_name(
¶meter_metadata.name,
&co_miro_frame.title,
),
))
.deploy_screenshot_to_miro_frame(
co_miro_frame.clone(),
0,
co_miro_frame.height as i64,
SourceCodeScreenshotOptions {
include_path: true,
offset_to_start_line: true,
filter_comments: false,
font_size: None,
filters: None,
show_line_number: true,
},
)
.await
.change_context(CommandError)?;
}
}
}
}
} else {
let options = vec![
"Entrypoint function".to_string(),
"Context accounts".to_string(),
"Validations".to_string(),
"Dependency functions".to_string(),
];
let prompt_text = "Which screenshots you want to update?".to_string();
let selections = BatDialoguer::multiselect(prompt_text, options.clone(), None, true)?;
let co_parser = CodeOverhaulParser::new_from_entry_point_name_and_program(
entrypoint_name.clone(),
selected_program_name.clone(),
)
.change_context(CommandError)?;
let ep_parser =
EntrypointParser::new_from_name(&entrypoint_name).change_context(CommandError)?;
for selection in selections {
match selection {
0 => {
let ep_sc_parser = ep_parser.entry_point_function.to_source_code_parser(
Some(miro_command_functions::parse_screenshot_name(
&ep_parser.entry_point_function.name,
&co_miro_frame.title,
)),
);
let ep_screenshot_path = ep_sc_parser
.create_screenshot(SourceCodeScreenshotOptions {
include_path: false,
offset_to_start_line: true,
filter_comments: false,
font_size: None,
filters: None,
show_line_number: true,
})
.change_context(CommandError)?;
let mut ep_image = MiroImage::new_from_item_id(
&miro_co_metadata.entry_point_image_id,
MiroImageType::FromPath,
)
.await
.change_context(CommandError)?;
println!(
"\nUpdating entrypoint screenshot in {} frame",
co_miro_frame.title.green()
);
ep_image
.update_from_path(&ep_screenshot_path)
.await
.change_context(CommandError)?;
fs::remove_file(&ep_screenshot_path)
.into_report()
.change_context(CommandError)?;
}
1 => co_parser
.update_context_accounts_screenshot()
.await
.change_context(CommandError)?,
2 => co_parser
.update_validations_screenshot()
.await
.change_context(CommandError)?,
3 => {
if ep_parser.dependencies.is_empty() {
println!("No dependency functions");
continue;
}
for (dep_idx, dep_function) in ep_parser.dependencies.iter().enumerate() {
if dep_idx >= miro_co_metadata.dependency_image_ids.len() {
println!(
"No Miro image ID for dependency {}, skipping update",
dep_function.name
);
continue;
}
let dep_sc_parser = dep_function.to_source_code_parser(Some(
miro_command_functions::parse_screenshot_name(
&dep_function.name,
&co_miro_frame.title,
),
));
let dep_screenshot_path = dep_sc_parser
.create_screenshot(SourceCodeScreenshotOptions {
include_path: true,
offset_to_start_line: true,
filter_comments: true,
font_size: None,
filters: None,
show_line_number: true,
})
.change_context(CommandError)?;
let mut dep_image = MiroImage::new_from_item_id(
&miro_co_metadata.dependency_image_ids[dep_idx],
MiroImageType::FromPath,
)
.await
.change_context(CommandError)?;
println!(
"\nUpdating dependency {} screenshot in {} frame",
dep_function.name.green(),
co_miro_frame.title.green()
);
dep_image
.update_from_path(&dep_screenshot_path)
.await
.change_context(CommandError)?;
fs::remove_file(&dep_screenshot_path)
.into_report()
.change_context(CommandError)?;
}
}
_ => {
unimplemented!()
}
};
}
}
Ok(())
}
}
pub mod miro_command_functions {
use super::*;
use crate::batbelt::path::prettify_source_code_path;
pub async fn prompt_deploy_source_code(
selected_miro_frame: MiroFrame,
select_all: bool,
) -> CommandResult<()> {
let metadata_types_vec = BatMetadataType::get_type_vec();
let metadata_types_colorized_vec = BatMetadataType::get_colorized_type_vec(true);
let mut continue_selection = true;
while continue_selection {
let prompt_text = format!("Please enter the {}", "metadata type".green());
let selection = batbelt::bat_dialoguer::select(
&prompt_text,
metadata_types_colorized_vec.clone(),
None,
)
.unwrap();
let metadata_type_selected = &metadata_types_vec[selection];
let (sourcecode_metadata_vec, screenshot_options): (
Vec<SourceCodeParser>,
SourceCodeScreenshotOptions,
) = match metadata_type_selected {
BatMetadataType::Struct => {
let prompt_text =
format!("Please enter the {}", "struct type to deploy".green());
let struct_types_colorized = StructMetadataType::get_colorized_type_vec(true);
let selection = batbelt::bat_dialoguer::select(
&prompt_text,
struct_types_colorized.clone(),
None,
)
.unwrap();
let selected_struct_type = StructMetadataType::get_type_vec()[selection];
let struct_metadata_vec =
SourceCodeMetadata::get_filtered_structs(None, Some(selected_struct_type))
.change_context(CommandError)?;
let struct_metadata_names = struct_metadata_vec
.iter()
.map(|struct_metadata| {
miro_command_functions::get_formatted_path(
struct_metadata.name.clone(),
struct_metadata.path.clone(),
struct_metadata.start_line_index,
)
})
.collect::<Result<Vec<_>, _>>()?;
let prompt_text = format!("Please enter the {}", "struct to deploy".green());
let selections = BatDialoguer::multiselect(
prompt_text,
struct_metadata_names.clone(),
Some(&vec![select_all; struct_metadata_names.len()]),
true,
)
.unwrap();
let default_config = SourceCodeScreenshotOptions::get_default_metadata_options(
BatMetadataType::Struct,
);
let use_default = batbelt::bat_dialoguer::select_yes_or_no(&format!(
"Do you want to {}\n{:#?}",
"use the default screenshot config?".yellow(),
default_config
))
.unwrap();
let screenshot_options = if use_default {
default_config
} else {
SourceCodeParser::prompt_screenshot_options()
};
let sc_vec = struct_metadata_vec
.into_iter()
.enumerate()
.filter_map(|(sc_index, sc_metadata)| {
if selections.iter().any(|selection| &sc_index == selection) {
Some(sc_metadata.to_source_code_parser(None))
} else {
None
}
})
.collect::<Vec<_>>();
(sc_vec, screenshot_options)
}
BatMetadataType::Function => {
let prompt_text =
format!("Please enter the {}", "function type to deploy".green());
let function_types_colorized =
FunctionMetadataType::get_colorized_type_vec(true);
let selection = batbelt::bat_dialoguer::select(
&prompt_text,
function_types_colorized.clone(),
None,
)
.unwrap();
let selected_function_type = FunctionMetadataType::get_type_vec()[selection];
let function_metadata_vec = SourceCodeMetadata::get_filtered_functions(
None,
Some(selected_function_type),
)
.change_context(CommandError)?;
let function_metadata_names = function_metadata_vec
.iter()
.map(|function_metadata| {
miro_command_functions::get_formatted_path(
function_metadata.name.clone(),
function_metadata.path.clone(),
function_metadata.start_line_index,
)
})
.collect::<Result<Vec<_>, _>>()?;
let prompt_text = format!("Please enter the {}", "function to deploy".green());
let selections = BatDialoguer::multiselect(
prompt_text,
function_metadata_names.clone(),
Some(&vec![select_all; function_metadata_names.len()]),
true,
)
.unwrap();
let default_config = SourceCodeScreenshotOptions::get_default_metadata_options(
BatMetadataType::Function,
);
let use_default = batbelt::bat_dialoguer::select_yes_or_no(&format!(
"Do you want to {}\n{:#?}",
"use the default screenshot config?".yellow(),
default_config
))
.unwrap();
let screenshot_options = if use_default {
default_config
} else {
SourceCodeParser::prompt_screenshot_options()
};
let sc_vec = function_metadata_vec
.into_iter()
.enumerate()
.filter_map(|(sc_index, sc_metadata)| {
if selections.iter().any(|selection| &sc_index == selection) {
Some(sc_metadata.to_source_code_parser(None))
} else {
None
}
})
.collect::<Vec<_>>();
(sc_vec, screenshot_options)
}
BatMetadataType::Trait => {
let prompt_text =
format!("Please enter the {}", "trait type to deploy".green());
let trait_types_colorized = TraitMetadataType::get_colorized_type_vec(true);
let selection = batbelt::bat_dialoguer::select(
&prompt_text,
trait_types_colorized.clone(),
None,
)
.unwrap();
let selected_trait_type = TraitMetadataType::get_type_vec()[selection];
let trait_metadata_vec =
SourceCodeMetadata::get_filtered_traits(None, Some(selected_trait_type))
.change_context(CommandError)?;
let trait_metadata_names = trait_metadata_vec
.iter()
.map(|trait_metadata| {
miro_command_functions::get_formatted_path(
trait_metadata.name.clone(),
trait_metadata.path.clone(),
trait_metadata.start_line_index,
)
})
.collect::<Result<Vec<_>, _>>()?;
let prompt_text = format!("Please enter the {}", "trait to deploy".green());
let selections = BatDialoguer::multiselect(
prompt_text,
trait_metadata_names.clone(),
Some(&vec![select_all; trait_metadata_names.len()]),
true,
)
.unwrap();
let default_config = SourceCodeScreenshotOptions::get_default_metadata_options(
BatMetadataType::Function,
);
let use_default = batbelt::bat_dialoguer::select_yes_or_no(&format!(
"Do you want to {}\n{:#?}",
"use the default screenshot config?".yellow(),
default_config
))
.unwrap();
let screenshot_options = if use_default {
default_config
} else {
SourceCodeParser::prompt_screenshot_options()
};
let sc_vec = trait_metadata_vec
.into_iter()
.enumerate()
.filter_map(|(sc_index, sc_metadata)| {
if selections.iter().any(|selection| &sc_index == selection) {
Some(sc_metadata.to_source_code_parser(None))
} else {
None
}
})
.collect::<Vec<_>>();
(sc_vec, screenshot_options)
}
BatMetadataType::Enum => {
let prompt_text = format!("Please enter the {}", "enum type to deploy".green());
let enum_types_colorized = EnumMetadataType::get_colorized_type_vec(true);
let selection = batbelt::bat_dialoguer::select(
&prompt_text,
enum_types_colorized.clone(),
None,
)
.unwrap();
let selected_enum_type = EnumMetadataType::get_type_vec()[selection];
let enum_metadata_vec =
SourceCodeMetadata::get_filtered_enums(None, Some(selected_enum_type))
.change_context(CommandError)?;
let enum_metadata_names = enum_metadata_vec
.iter()
.map(|enum_metadata| {
miro_command_functions::get_formatted_path(
enum_metadata.name.clone(),
enum_metadata.path.clone(),
enum_metadata.start_line_index,
)
})
.collect::<Result<Vec<_>, _>>()?;
let prompt_text = format!("Please enter the {}", "enum to deploy".green());
let selections = BatDialoguer::multiselect(
prompt_text,
enum_metadata_names.clone(),
Some(&vec![select_all; enum_metadata_names.len()]),
true,
)
.unwrap();
let default_config = SourceCodeScreenshotOptions::get_default_metadata_options(
BatMetadataType::Function,
);
let use_default = batbelt::bat_dialoguer::select_yes_or_no(&format!(
"Do you want to {}\n{:#?}",
"use the default screenshot config?".yellow(),
default_config
))
.unwrap();
let screenshot_options = if use_default {
default_config
} else {
SourceCodeParser::prompt_screenshot_options()
};
let sc_vec = enum_metadata_vec
.into_iter()
.enumerate()
.filter_map(|(sc_index, sc_metadata)| {
if selections.iter().any(|selection| &sc_index == selection) {
Some(sc_metadata.to_source_code_parser(None))
} else {
None
}
})
.collect::<Vec<_>>();
(sc_vec, screenshot_options)
}
};
for mut sc_metadata in sourcecode_metadata_vec {
sc_metadata.name = miro_command_functions::parse_screenshot_name(
&sc_metadata.name,
&selected_miro_frame.title,
);
sc_metadata
.deploy_screenshot_to_miro_frame(
selected_miro_frame.clone(),
0,
selected_miro_frame.height as i64,
screenshot_options.clone(),
)
.await
.change_context(CommandError)?;
}
let prompt_text = format!(
"Do you want to {} in the {} frame?",
"continue creating screenshots".yellow(),
selected_miro_frame.title.yellow()
);
continue_selection = batbelt::bat_dialoguer::select_yes_or_no(&prompt_text).unwrap();
BatMetadataEnvVariables::BatMetadataFileSelected.clean_value();
}
Ok(())
}
pub async fn deploy_miro_frame_for_co(
entry_point_name: &str,
entry_point_index: usize,
program_name: Option<&str>,
) -> CommandResult<MiroFrame> {
let frame_name = match program_name {
Some(pn) => format!("co:{}:{}", pn, entry_point_name),
None => format!("co: {}", entry_point_name),
};
println!("Creating frame in Miro for {}", entry_point_name.green());
let mut miro_frame = MiroFrame::new(&frame_name, MIRO_FRAME_HEIGHT, MIRO_FRAME_WIDTH, 0, 0);
miro_frame.deploy().await.change_context(CommandError)?;
let x_modifier = entry_point_index as i64 % MIRO_BOARD_COLUMNS;
let y_modifier = entry_point_index as i64 / MIRO_BOARD_COLUMNS;
let x_position = MIRO_INITIAL_X + (MIRO_FRAME_WIDTH as i64 + 200) * x_modifier;
let y_position = MIRO_INITIAL_Y + (MIRO_FRAME_HEIGHT as i64 + 400) * y_modifier;
miro_frame
.update_position(x_position, y_position)
.await
.change_context(CommandError)?;
Ok(miro_frame)
}
pub fn get_formatted_path(
name: String,
path: String,
start_line_index: usize,
) -> CommandResult<String> {
Ok(format!(
"{}: {}:{}",
name,
prettify_source_code_path(path.trim_start_matches("../"))
.change_context(CommandError)?,
start_line_index
))
}
pub fn parse_screenshot_name(name: &str, frame_title: &str) -> String {
format!(
"{}::frame={}",
name,
frame_title
.replace([' ', '-'], "_")
.to_screaming_snake_case()
)
}
}
#[test]
fn test_enum_display() {
let bat_package_json_command = MiroCommand::get_bat_package_json_commands("miro".to_string());
for option in bat_package_json_command.clone().command_options {
let combinations_vec = option.get_combinations_vec("miro");
println!("{:#?}", combinations_vec);
}
println!("{:#?}", bat_package_json_command);
}