mirror of
https://gitlab.redox-os.org/redox-os/redox.git
synced 2026-11-04 19:56:36 +08:00
Implement new repo_editor
This commit is contained in:
parent
af5c2fb695
commit
ff9a548795
@ -23,6 +23,8 @@ cargo-test:
|
||||
image: "rust:trixie"
|
||||
stage: lint
|
||||
script:
|
||||
# warnings are annoying for user
|
||||
- export RUSTFLAGS="-Dwarnings"
|
||||
- cargo test --locked
|
||||
|
||||
fetch-changed:
|
||||
|
||||
@ -19,6 +19,10 @@ path = "src/bin/repo.rs"
|
||||
name = "repo_builder"
|
||||
path = "src/bin/repo_builder.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "repo_editor"
|
||||
path = "src/bin/repo_editor/main.rs"
|
||||
|
||||
[lib]
|
||||
name = "cookbook"
|
||||
path = "src/lib.rs"
|
||||
|
||||
@ -49,6 +49,13 @@ else
|
||||
touch $@
|
||||
endif
|
||||
|
||||
repo_editor: $(FSTOOLS_TAG)
|
||||
ifeq ($(PODMAN_BUILD),1)
|
||||
$(PODMAN_RUN) make $@
|
||||
else
|
||||
CONFIG=$(FILESYSTEM_CONFIG) $(REPO_BIN)_editor
|
||||
endif
|
||||
|
||||
fstools_clean: FORCE
|
||||
rm -rf target
|
||||
rm -rf $(FSTOOLS)
|
||||
|
||||
180
src/bin/repo.rs
180
src/bin/repo.rs
@ -1,15 +1,14 @@
|
||||
use ansi_to_tui::IntoText;
|
||||
use cookbook::config::{CookConfig, CookLockOpt, get_config, init_config};
|
||||
use cookbook::cook::cook_build::{build, get_stage_dirs, remove_stage_dir};
|
||||
use cookbook::cook::fetch::{FetchResult, fetch, fetch_offline};
|
||||
use cookbook::cook::fs::{
|
||||
create_dir, create_target_dir, get_git_commit_date, get_git_head_rev, get_git_rev_before_date,
|
||||
read_toml, remove_all, run_command,
|
||||
remove_all, run_command,
|
||||
};
|
||||
use cookbook::cook::package::{package, package_handle_push};
|
||||
use cookbook::cook::pty::{PtyOut, UnixSlavePty, flush_pty, setup_pty, write_to_pty};
|
||||
use cookbook::cook::script::KILL_ALL_PID;
|
||||
use cookbook::cook::tree::{self, WalkTreeEntry};
|
||||
use cookbook::cook::tui::{drain_buffer_to_lines, join_logs, kill_everything, render_build_log};
|
||||
use cookbook::cook::{fetch_repo, ident};
|
||||
use cookbook::recipe::{
|
||||
CookRecipe, SourceRecipe, recipes_flatten_package_names, recipes_mark_as_deps,
|
||||
@ -20,11 +19,11 @@ use ratatui::Terminal;
|
||||
use ratatui::layout::{Constraint, Direction, Layout, Rect};
|
||||
use ratatui::prelude::TermionBackend;
|
||||
use ratatui::style::{Color, Style};
|
||||
use ratatui::text::{Line, Span, Text};
|
||||
use ratatui::text::{Line, Span};
|
||||
use ratatui::widgets::{Block, Borders, Clear, List, ListItem, ListState, Paragraph, Wrap};
|
||||
use redox_installer::PackageConfig;
|
||||
use std::borrow::Cow;
|
||||
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
|
||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||
use std::io::{Read, Write, stderr, stdin, stdout};
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
@ -32,7 +31,7 @@ use std::str::FromStr;
|
||||
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
|
||||
use std::sync::{Arc, OnceLock, mpsc};
|
||||
use std::time::{Duration, Instant};
|
||||
use std::{cmp, env, fs};
|
||||
use std::{env, fs};
|
||||
use std::{process, thread};
|
||||
use termion::event::{Event, Key};
|
||||
use termion::input::TermRead;
|
||||
@ -151,6 +150,8 @@ impl FromStr for CliCommand {
|
||||
match s {
|
||||
"fetch" => Ok(CliCommand::Fetch),
|
||||
"cook" => Ok(CliCommand::Cook),
|
||||
// alias for repo_editor
|
||||
"rebuild" => Ok(CliCommand::Cook),
|
||||
"unfetch" => Ok(CliCommand::Unfetch),
|
||||
"clean" => Ok(CliCommand::Clean),
|
||||
"clean-target" => Ok(CliCommand::CleanTarget),
|
||||
@ -519,7 +520,16 @@ fn parse_args(args: Vec<String>) -> Result<(CliConfig, CliCommand, Vec<CookRecip
|
||||
let Some(command) = command else {
|
||||
bail_options_err!("Error: No command specified");
|
||||
};
|
||||
let command: CliCommand = str::parse(&command)?;
|
||||
let command =
|
||||
if command.starts_with("change-rule ") || command.starts_with("change-rule-local ") {
|
||||
// repo_editor hack
|
||||
let mut split = command.split(' ');
|
||||
let cmd: CliCommand = str::parse(split.next().unwrap())?;
|
||||
config.set_rule = split.next().map(|s| s.to_string());
|
||||
cmd
|
||||
} else {
|
||||
str::parse(&command)?
|
||||
};
|
||||
if command.is_informational() {
|
||||
// avoid extra data that clobber stdout
|
||||
config.cook.verbose = false;
|
||||
@ -532,23 +542,11 @@ fn parse_args(args: Vec<String>) -> Result<(CliConfig, CliCommand, Vec<CookRecip
|
||||
let all_recipes_path = match command.is_cleaning() || config.category.is_some() {
|
||||
true => {
|
||||
// some wip recipes have broken toml
|
||||
staged_pkg::list("")
|
||||
staged_pkg::list()
|
||||
}
|
||||
false => {
|
||||
// get the list from repo/TARGET/repo.toml
|
||||
let repo_toml_path = config.repo_dir.join(redoxer::target()).join("repo.toml");
|
||||
if !repo_toml_path.is_file() {
|
||||
bail_options_err!(
|
||||
"The repository is not found: {}",
|
||||
repo_toml_path.display()
|
||||
);
|
||||
};
|
||||
let repos: pkg::Repository = read_toml(&repo_toml_path)?;
|
||||
repos
|
||||
.packages
|
||||
.keys()
|
||||
.filter_map(|n| staged_pkg::find(n).map(|s| s.to_path_buf()))
|
||||
.collect::<BTreeSet<_>>()
|
||||
staged_pkg::list_repo(&config.repo_dir)?
|
||||
}
|
||||
};
|
||||
|
||||
@ -906,6 +904,12 @@ fn handle_clean(recipe: &CookRecipe, config: &CliConfig, command: &CliCommand) -
|
||||
|
||||
static PUSH_CONFIG: OnceLock<CliConfig> = OnceLock::new();
|
||||
fn handle_push(recipes: &Vec<CookRecipe>, config: &CliConfig) -> Result<()> {
|
||||
if !config.sysroot_dir.is_dir() {
|
||||
return Err(Error::Other(format!(
|
||||
"{} is not exist. Please run `make mount` first.",
|
||||
config.sysroot_dir.display()
|
||||
)));
|
||||
}
|
||||
let recipe_map: HashMap<&PackageName, &CookRecipe> =
|
||||
recipes.iter().map(|r| (&r.name, r)).collect();
|
||||
let mut total_size: u64 = 0;
|
||||
@ -1366,14 +1370,7 @@ impl TuiApp {
|
||||
let _ = std::io::stdout().write_all(&chunk);
|
||||
}
|
||||
let log_list = self.logs.entry(name.clone()).or_default();
|
||||
// TODO: multibyte-aware line split?
|
||||
while let Some(newline_pos) = buffer.iter().position(|&b| b == b'\n') {
|
||||
let line_bytes = buffer.drain(..=newline_pos);
|
||||
let line_str = String::from_utf8_lossy(&line_bytes.as_slice());
|
||||
let line_str_pos = line_str.trim_end();
|
||||
let line_str = line_str_pos.rsplit('\r').next().unwrap_or(&line_str_pos);
|
||||
log_list.push(line_str.to_owned());
|
||||
}
|
||||
drain_buffer_to_lines(buffer, log_list);
|
||||
return;
|
||||
}
|
||||
StatusUpdate::FlushLog(name, path) => {
|
||||
@ -1734,84 +1731,24 @@ fn run_tui_cook(config: CliConfig, recipes: Vec<CookRecipe>) -> Result<TuiApp> {
|
||||
format!(" {} Log ", app.log_view_job.to_string())
|
||||
};
|
||||
|
||||
let mut enable_auto_scroll = false;
|
||||
let mut intended_scroll_pos = 0usize;
|
||||
let mut enable_auto_scroll = app.auto_scroll;
|
||||
let mut intended_scroll_pos = app.log_scroll;
|
||||
|
||||
let mut log_lines: Vec<Line> = if let Some(log_text) = log_text
|
||||
&& !log_text.is_empty()
|
||||
{
|
||||
let total_log_lines = log_text.len() as usize;
|
||||
|
||||
let start = if app.auto_scroll {
|
||||
if total_log_lines > panel_height {
|
||||
intended_scroll_pos = total_log_lines - panel_height;
|
||||
total_log_lines - panel_height
|
||||
} else {
|
||||
0
|
||||
}
|
||||
let log_lines = render_build_log(
|
||||
panel_height,
|
||||
log_text,
|
||||
log_line,
|
||||
app.prompt.is_none() || config.cook.nonstop,
|
||||
&mut enable_auto_scroll,
|
||||
&mut intended_scroll_pos,
|
||||
if let Some(search_results) = app.search_results.as_ref()
|
||||
&& app.is_inspecting
|
||||
{
|
||||
Some((app.search_idx, search_results))
|
||||
} else {
|
||||
if total_log_lines > panel_height {
|
||||
let limit = 2; // arbitrary number
|
||||
if app.log_scroll >= total_log_lines - limit {
|
||||
if app.prompt.is_none() || config.cook.nonstop {
|
||||
enable_auto_scroll = true;
|
||||
}
|
||||
intended_scroll_pos = total_log_lines - limit;
|
||||
total_log_lines - limit
|
||||
} else {
|
||||
app.log_scroll
|
||||
}
|
||||
} else {
|
||||
0
|
||||
}
|
||||
};
|
||||
|
||||
let end = cmp::min(panel_height + start, total_log_lines - 1);
|
||||
|
||||
log_text[start..end]
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, s)| {
|
||||
if app.is_inspecting {
|
||||
if let Some(search_results) = app.search_results.as_ref() {
|
||||
let absolute_i = start + i;
|
||||
if absolute_i == search_results[app.search_idx] {
|
||||
let s = strip_ansi_escapes::strip_str(s);
|
||||
return Line::from(s).style(
|
||||
Style::default().bg(Color::Yellow).fg(Color::Black),
|
||||
);
|
||||
} else if search_results.binary_search(&absolute_i).is_ok() {
|
||||
let s = strip_ansi_escapes::strip_str(s);
|
||||
return Line::from(s)
|
||||
.style(Style::default().bg(Color::Black).fg(Color::White));
|
||||
}
|
||||
}
|
||||
}
|
||||
let text_with_colors = s
|
||||
.into_text()
|
||||
.unwrap_or_else(|_| Text::raw("--unrenderable line--"));
|
||||
text_with_colors
|
||||
.lines
|
||||
.into_iter()
|
||||
.next()
|
||||
.unwrap_or_else(|| Line::raw("--unrenderable line--"))
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
vec![Line::from("No logs yet")]
|
||||
};
|
||||
|
||||
if let Some(buffer) = log_line
|
||||
&& !buffer.is_empty()
|
||||
{
|
||||
let text_with_colors = handle_cr(&buffer)
|
||||
.into_text()
|
||||
.unwrap_or_else(|_| Text::raw("--unrenderable line--"));
|
||||
|
||||
if let Some(line) = text_with_colors.lines.into_iter().next() {
|
||||
log_lines.push(line);
|
||||
}
|
||||
}
|
||||
None
|
||||
},
|
||||
);
|
||||
|
||||
let instruct = if app.is_inspecting {
|
||||
let line_info = if let Some(search_results) = app.search_results.as_ref() {
|
||||
@ -1876,10 +1813,10 @@ fn run_tui_cook(config: CliConfig, recipes: Vec<CookRecipe>) -> Result<TuiApp> {
|
||||
draw_prompt(f, prompt, config.cook.nonstop);
|
||||
}
|
||||
}
|
||||
if enable_auto_scroll {
|
||||
if !app.auto_scroll && enable_auto_scroll {
|
||||
app.auto_scroll = true;
|
||||
}
|
||||
if intended_scroll_pos > 0 {
|
||||
if intended_scroll_pos != app.log_scroll {
|
||||
app.log_scroll = intended_scroll_pos;
|
||||
}
|
||||
|
||||
@ -1928,7 +1865,7 @@ fn run_tui_cook(config: CliConfig, recipes: Vec<CookRecipe>) -> Result<TuiApp> {
|
||||
let _ = stdout().flush();
|
||||
|
||||
if config.cook.nonstop && app.dump_logs_on_exit.is_some() {
|
||||
kill_everything();
|
||||
kill_everything(None);
|
||||
}
|
||||
|
||||
let _ = fetcher_handle.join();
|
||||
@ -1937,20 +1874,6 @@ fn run_tui_cook(config: CliConfig, recipes: Vec<CookRecipe>) -> Result<TuiApp> {
|
||||
Ok(app)
|
||||
}
|
||||
|
||||
fn join_logs(log: &Vec<String>, line: Option<Cow<'_, str>>) -> String {
|
||||
let mut logs = log.join("\n");
|
||||
if let Some(line) = line {
|
||||
logs.push_str("\n");
|
||||
logs.push_str(handle_cr(&line));
|
||||
}
|
||||
logs
|
||||
}
|
||||
|
||||
fn handle_cr<'a>(buffer: &'a Cow<'_, str>) -> &'a str {
|
||||
let st = buffer.trim_end();
|
||||
st.rsplit('\r').next().unwrap_or(&st)
|
||||
}
|
||||
|
||||
fn handle_main_event(app: &mut TuiApp, event: &Event) {
|
||||
match event {
|
||||
Event::Key(key) => match key {
|
||||
@ -1962,7 +1885,7 @@ fn handle_main_event(app: &mut TuiApp, event: &Event) {
|
||||
}
|
||||
Key::Char('c') => {
|
||||
// as compilers still running, we use this way to stop it
|
||||
kill_everything();
|
||||
kill_everything(None);
|
||||
}
|
||||
Key::Up => {
|
||||
app.auto_scroll = false;
|
||||
@ -2118,17 +2041,6 @@ fn handle_inspect_event(event: &Event, app: &mut TuiApp) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn kill_everything() {
|
||||
let pid = std::process::id();
|
||||
Command::new("bash")
|
||||
.arg("-c")
|
||||
.arg(KILL_ALL_PID.replace("$PID", &pid.to_string()))
|
||||
.stdout(process::Stdio::null())
|
||||
.stderr(process::Stdio::null())
|
||||
.spawn()
|
||||
.expect("unable to spawn kill");
|
||||
}
|
||||
|
||||
fn handle_prompt_input<'a>(
|
||||
event: &Event,
|
||||
app: &'a mut TuiApp,
|
||||
|
||||
289
src/bin/repo_editor/app.rs
Normal file
289
src/bin/repo_editor/app.rs
Normal file
@ -0,0 +1,289 @@
|
||||
// Contains code related to TUI model
|
||||
|
||||
use crate::*;
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub enum RightPanelTab {
|
||||
MainActions,
|
||||
Terminal(usize),
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Copy)]
|
||||
pub enum FilterSource {
|
||||
Config,
|
||||
Compiled,
|
||||
Pinned,
|
||||
All,
|
||||
}
|
||||
|
||||
impl FilterSource {
|
||||
pub fn labels() -> Vec<&'static str> {
|
||||
vec!["[F2] Selected", "[F3] Config", "[F4] Compiled", "[F5] All"]
|
||||
}
|
||||
|
||||
pub fn index(&self) -> usize {
|
||||
match self {
|
||||
FilterSource::Pinned => 0,
|
||||
FilterSource::Config => 1,
|
||||
FilterSource::Compiled => 2,
|
||||
FilterSource::All => 3,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn try_from_index(i: usize) -> Option<FilterSource> {
|
||||
Some(match i {
|
||||
0 => FilterSource::Pinned,
|
||||
1 => FilterSource::Config,
|
||||
2 => FilterSource::Compiled,
|
||||
3 => FilterSource::All,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub enum Focus {
|
||||
SearchAndList,
|
||||
MainPanel,
|
||||
}
|
||||
|
||||
pub struct ActionButton {
|
||||
pub key: char,
|
||||
pub label: &'static str,
|
||||
pub cmd: &'static str,
|
||||
pub area: Rect,
|
||||
}
|
||||
|
||||
pub struct App {
|
||||
pub search_query: String,
|
||||
pub all_recipes: Vec<CookRecipe>,
|
||||
pub all_compiled_recipes: Vec<CookRecipe>,
|
||||
pub all_config_recipes: Vec<CookRecipe>,
|
||||
pub filtered_recipes: Vec<PackageName>,
|
||||
pub pinned_recipes: BTreeSet<PackageName>,
|
||||
pub filter_source: FilterSource,
|
||||
pub list_state: ListState,
|
||||
pub focus: Focus,
|
||||
pub buttons: Vec<ActionButton>,
|
||||
|
||||
pub last_click_time: Option<Instant>,
|
||||
pub last_click_idx: Option<usize>,
|
||||
|
||||
pub filter_tabs_area: Rect,
|
||||
pub right_tabs_area: Rect,
|
||||
pub right_tabs_str: Vec<String>,
|
||||
pub right_tab_active: RightPanelTab,
|
||||
pub exec: ExecutionManager,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new() -> Self {
|
||||
let mut app = Self {
|
||||
search_query: String::new(),
|
||||
all_recipes: Vec::new(),
|
||||
all_compiled_recipes: Vec::new(),
|
||||
all_config_recipes: Vec::new(),
|
||||
filtered_recipes: Vec::new(),
|
||||
pinned_recipes: BTreeSet::new(),
|
||||
filter_source: FilterSource::Config,
|
||||
list_state: ListState::default(),
|
||||
right_tabs_area: Rect::default(),
|
||||
right_tabs_str: Vec::new(),
|
||||
focus: Focus::SearchAndList,
|
||||
buttons: vec![
|
||||
ActionButton {
|
||||
key: 'f',
|
||||
label: "[F]etch",
|
||||
cmd: "fetch",
|
||||
area: Rect::default(),
|
||||
},
|
||||
ActionButton {
|
||||
key: 'u',
|
||||
label: "[U]nfetch",
|
||||
cmd: "unfetch",
|
||||
area: Rect::default(),
|
||||
},
|
||||
ActionButton {
|
||||
key: 'c',
|
||||
label: "[C]lean",
|
||||
cmd: "clean",
|
||||
area: Rect::default(),
|
||||
},
|
||||
ActionButton {
|
||||
key: 'r',
|
||||
label: "[R]ebuild",
|
||||
cmd: "rebuild",
|
||||
area: Rect::default(),
|
||||
},
|
||||
ActionButton {
|
||||
key: 'p',
|
||||
label: "[P]ush",
|
||||
cmd: "push",
|
||||
area: Rect::default(),
|
||||
},
|
||||
],
|
||||
last_click_time: None,
|
||||
last_click_idx: None,
|
||||
filter_tabs_area: Rect::default(),
|
||||
right_tab_active: RightPanelTab::MainActions,
|
||||
exec: ExecutionManager::new(),
|
||||
};
|
||||
app.read_recipes();
|
||||
app.update_filter();
|
||||
app
|
||||
}
|
||||
|
||||
pub fn read_recipes(&mut self) {
|
||||
self.all_recipes = staged_pkg::list()
|
||||
.iter()
|
||||
.filter_map(|p| CookRecipe::from_path(p, false, false).ok())
|
||||
.collect();
|
||||
self.all_compiled_recipes = staged_pkg::list_repo("repo")
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter_map(|p| CookRecipe::from_path(p, false, false).ok())
|
||||
.collect();
|
||||
self.all_config_recipes = std::env::var("CONFIG")
|
||||
.ok()
|
||||
.and_then(|s| redox_installer::Config::from_file(Path::new(&s)).ok())
|
||||
.map(|s| {
|
||||
s.packages
|
||||
.keys()
|
||||
.filter_map(|p| PackageName::new(p).ok())
|
||||
.filter_map(|p| CookRecipe::from_name(p).ok())
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or(vec![]);
|
||||
}
|
||||
|
||||
pub fn source_filter(&self) -> &Vec<CookRecipe> {
|
||||
match self.filter_source {
|
||||
FilterSource::Config => &self.all_config_recipes,
|
||||
FilterSource::Compiled => &self.all_compiled_recipes,
|
||||
FilterSource::All => &self.all_recipes,
|
||||
_ => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_filter(&mut self) {
|
||||
let query = self.search_query.to_lowercase();
|
||||
|
||||
if matches!(self.filter_source, FilterSource::Pinned) {
|
||||
self.filtered_recipes = self.pinned_recipes.iter().cloned().collect();
|
||||
} else {
|
||||
self.filtered_recipes = self
|
||||
.source_filter()
|
||||
.iter()
|
||||
.filter(|p| p.name.as_str().contains(&query))
|
||||
.map(|r| r.name.clone())
|
||||
.collect();
|
||||
}
|
||||
|
||||
self.filtered_recipes.sort();
|
||||
|
||||
if self.filtered_recipes.is_empty() {
|
||||
self.list_state.select(None);
|
||||
} else if self.list_state.selected().is_none() {
|
||||
self.list_state.select(Some(0));
|
||||
} else if let Some(idx) = self.list_state.selected() {
|
||||
if idx >= self.filtered_recipes.len() {
|
||||
self.list_state
|
||||
.select(Some(self.filtered_recipes.len() - 1));
|
||||
}
|
||||
}
|
||||
|
||||
self.right_tab_active = RightPanelTab::MainActions;
|
||||
}
|
||||
|
||||
pub fn highlighted_recipe(&self) -> Option<&PackageName> {
|
||||
self.list_state
|
||||
.selected()
|
||||
.and_then(|idx| self.filtered_recipes.get(idx))
|
||||
}
|
||||
|
||||
pub fn selected_recipes(&self) -> Vec<PackageName> {
|
||||
if !self.pinned_recipes.is_empty() {
|
||||
let mut targets: Vec<_> = self.pinned_recipes.iter().cloned().collect();
|
||||
targets.sort();
|
||||
targets
|
||||
} else if let Some(single) = self.highlighted_recipe() {
|
||||
vec![single.clone()]
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_pinned_highlighted(&mut self) {
|
||||
if let Some(recipe) = self.highlighted_recipe().cloned() {
|
||||
if self.pinned_recipes.contains(&recipe) {
|
||||
self.pinned_recipes.remove(&recipe);
|
||||
} else {
|
||||
self.pinned_recipes.insert(recipe);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pin_all_filtered(&mut self) {
|
||||
let mut nothing_pinned = true;
|
||||
for recipe in &self.filtered_recipes {
|
||||
nothing_pinned &= !self.pinned_recipes.insert(recipe.clone());
|
||||
}
|
||||
if nothing_pinned {
|
||||
for recipe in &self.filtered_recipes {
|
||||
self.pinned_recipes.remove(recipe);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn box_style<'a, 'b>(&'b self, title: &'a str, hint: &'a str, focus: Focus) -> Block<'a> {
|
||||
let style = if self.focus == focus {
|
||||
Style::default().fg(Color::Cyan)
|
||||
} else {
|
||||
Style::default()
|
||||
};
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title(title)
|
||||
.title_bottom(hint)
|
||||
.border_style(style)
|
||||
}
|
||||
|
||||
pub fn select_delta(&mut self, delta: isize, wrap: bool) {
|
||||
let len = self.filtered_recipes.len();
|
||||
if len == 0 {
|
||||
return;
|
||||
}
|
||||
let current = self.list_state.selected().unwrap_or(0);
|
||||
let next = current.saturating_add_signed(delta);
|
||||
let next = if wrap {
|
||||
next % len
|
||||
} else {
|
||||
next.clamp(0, len - 1)
|
||||
};
|
||||
|
||||
self.list_state.select(Some(next));
|
||||
self.focus = Focus::SearchAndList;
|
||||
self.right_tab_active = RightPanelTab::MainActions;
|
||||
}
|
||||
|
||||
pub fn current_job_id(&self) -> Option<JobId> {
|
||||
let RightPanelTab::Terminal(i) = self.right_tab_active else {
|
||||
return None;
|
||||
};
|
||||
self.exec.active_job_order.get(i).map(|s| *s)
|
||||
}
|
||||
|
||||
pub fn current_job(&self) -> Option<&PackageJob> {
|
||||
let Some(i) = self.current_job_id() else {
|
||||
return None;
|
||||
};
|
||||
self.exec.jobs.get(&i)
|
||||
}
|
||||
|
||||
pub fn current_job_mut(&mut self) -> Option<&mut PackageJob> {
|
||||
let Some(i) = self.current_job_id() else {
|
||||
return None;
|
||||
};
|
||||
self.exec.jobs.get_mut(&i)
|
||||
}
|
||||
}
|
||||
211
src/bin/repo_editor/jobs.rs
Normal file
211
src/bin/repo_editor/jobs.rs
Normal file
@ -0,0 +1,211 @@
|
||||
// Contains code related to background tasks
|
||||
|
||||
use std::fmt::{Display as FmtDisplay, Write as FmtWrite};
|
||||
use std::io::Read;
|
||||
use std::sync::mpsc;
|
||||
use std::thread;
|
||||
use std::{collections::HashMap, io::Write};
|
||||
|
||||
use cookbook::cook::pty::{setup_pty_with_stdin, spawn_to_pipe};
|
||||
use cookbook::cook::tui::drain_buffer_to_lines;
|
||||
use pkg::PackageName;
|
||||
|
||||
pub type JobId = u64;
|
||||
|
||||
pub enum StatusUpdate {
|
||||
PushLog(JobId, Vec<u8>),
|
||||
JobFinished(JobId, i32),
|
||||
}
|
||||
|
||||
pub struct PackageJob {
|
||||
pub id: JobId,
|
||||
pub command: String,
|
||||
pub targets: Vec<PackageName>,
|
||||
pub logs: Vec<String>,
|
||||
pub buffer: Vec<u8>,
|
||||
pub scroll: usize,
|
||||
pub auto_scroll: bool,
|
||||
pub exit_code: Option<i32>,
|
||||
pub pid: Option<u32>,
|
||||
pub pty_writer: Box<dyn Write + Send>,
|
||||
}
|
||||
|
||||
impl PackageJob {
|
||||
pub fn write_input(&mut self, bytes: &[u8]) {
|
||||
let _ = self.pty_writer.write_all(bytes);
|
||||
let _ = self.pty_writer.flush();
|
||||
}
|
||||
}
|
||||
|
||||
impl FmtDisplay for PackageJob {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(&self.command)?;
|
||||
f.write_char(' ')?;
|
||||
match self.targets.len() {
|
||||
0 => unreachable!(),
|
||||
1 => f.write_str(self.targets[0].as_str())?,
|
||||
2 => write!(
|
||||
f,
|
||||
"{} & {}",
|
||||
self.targets[0].as_str(),
|
||||
self.targets[0].as_str()
|
||||
)?,
|
||||
_ => write!(
|
||||
f,
|
||||
"{} ({} more)",
|
||||
self.targets[0].as_str(),
|
||||
self.targets.len() - 1
|
||||
)?,
|
||||
}
|
||||
match self.exit_code {
|
||||
Some(0) => f.write_str(" ✅")?,
|
||||
Some(_) => f.write_str(" ❌")?,
|
||||
None => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
pub struct ExecutionManager {
|
||||
next_job_id: JobId,
|
||||
pub jobs: HashMap<JobId, PackageJob>,
|
||||
pub active_job_order: Vec<JobId>,
|
||||
}
|
||||
|
||||
impl ExecutionManager {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
next_job_id: 1,
|
||||
jobs: HashMap::new(),
|
||||
active_job_order: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn spawn_job(
|
||||
&mut self,
|
||||
command: &str,
|
||||
targets: Vec<PackageName>,
|
||||
status_tx: mpsc::Sender<StatusUpdate>,
|
||||
) {
|
||||
if targets.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Scan anything not failed to auto close
|
||||
if self.active_job_order.len() > 0 {
|
||||
for id in self.active_job_order.clone() {
|
||||
if self
|
||||
.jobs
|
||||
.get(&id)
|
||||
.is_some_and(|s| s.exit_code.is_some_and(|s| s == 0))
|
||||
{
|
||||
self.close_job(&id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let job_id = self.next_job_id;
|
||||
self.next_job_id += 1;
|
||||
let (pty_reader, log_reader, pty_writer, (mut slave_pty, mut pipe_writer)) =
|
||||
setup_pty_with_stdin();
|
||||
|
||||
let mut job = PackageJob {
|
||||
id: job_id,
|
||||
command: command.to_string(),
|
||||
targets: targets.clone(),
|
||||
logs: Vec::new(),
|
||||
buffer: Vec::new(),
|
||||
scroll: 0,
|
||||
auto_scroll: true,
|
||||
exit_code: None,
|
||||
pid: None,
|
||||
pty_writer,
|
||||
};
|
||||
|
||||
spawn_log_reader(pty_reader, job_id, status_tx.clone());
|
||||
spawn_log_reader(log_reader, job_id, status_tx.clone());
|
||||
|
||||
let mut cmd = std::process::Command::new(
|
||||
std::env::current_exe()
|
||||
.unwrap()
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join("repo"),
|
||||
);
|
||||
cmd.arg(command);
|
||||
for target in &targets {
|
||||
cmd.arg(target.as_str());
|
||||
}
|
||||
|
||||
// TODO: How to handle more ANSI code?
|
||||
cmd.env("CI", "1");
|
||||
|
||||
self.active_job_order.push(job_id);
|
||||
|
||||
let pipe = (&mut slave_pty, &mut pipe_writer);
|
||||
let mut handle = spawn_to_pipe(&mut cmd, &Some(pipe)).unwrap();
|
||||
job.pid = Some(handle.id());
|
||||
self.jobs.insert(job_id, job);
|
||||
|
||||
let _ = write!(pipe_writer, "{:?}\n", cmd);
|
||||
thread::spawn(move || {
|
||||
let code = handle.wait().unwrap().code().unwrap_or(-1);
|
||||
status_tx
|
||||
.send(StatusUpdate::JobFinished(job_id, code))
|
||||
.unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
pub fn handle_status_update(&mut self, update: StatusUpdate) {
|
||||
match update {
|
||||
StatusUpdate::PushLog(job_id, bytes) => {
|
||||
if let Some(job) = self.jobs.get_mut(&job_id) {
|
||||
job.buffer.extend_from_slice(&bytes);
|
||||
let rel = drain_buffer_to_lines(&mut job.buffer, &mut job.logs);
|
||||
if job.auto_scroll {
|
||||
job.scroll += rel;
|
||||
}
|
||||
}
|
||||
}
|
||||
StatusUpdate::JobFinished(job_id, code) => {
|
||||
if let Some(job) = self.jobs.get_mut(&job_id) {
|
||||
job.pid.take();
|
||||
job.exit_code = Some(code);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn close_job(&mut self, id: &JobId) {
|
||||
self.jobs.remove(id);
|
||||
if let Some(id) = self.active_job_order.iter().position(|s| *s == *id) {
|
||||
self.active_job_order.remove(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_log_reader<R>(mut reader: R, job_id: JobId, status_tx: mpsc::Sender<StatusUpdate>)
|
||||
where
|
||||
R: Read + Send + 'static,
|
||||
{
|
||||
thread::spawn(move || {
|
||||
let mut buffer = [0; 1024];
|
||||
loop {
|
||||
match reader.read(&mut buffer) {
|
||||
Ok(0) => break,
|
||||
Ok(n) => {
|
||||
if status_tx
|
||||
.send(StatusUpdate::PushLog(job_id, buffer[..n].to_vec()))
|
||||
.is_err()
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
let err_msg = format!("[IO Error] {}", e).into_bytes();
|
||||
let _ = status_tx.send(StatusUpdate::PushLog(job_id, err_msg));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
584
src/bin/repo_editor/main.rs
Normal file
584
src/bin/repo_editor/main.rs
Normal file
@ -0,0 +1,584 @@
|
||||
// Contains code related to TUI layout
|
||||
|
||||
use cookbook::{
|
||||
Error, Result,
|
||||
cook::tui::{get_clicked_tab_index, kill_everything, render_build_log},
|
||||
recipe::CookRecipe,
|
||||
staged_pkg,
|
||||
};
|
||||
use pkg::PackageName;
|
||||
use ratatui::{
|
||||
Terminal,
|
||||
backend::TermionBackend,
|
||||
layout::{Constraint, Direction, Layout, Position, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Line, Span},
|
||||
widgets::{Block, Borders, List, ListItem, ListState, Paragraph, Tabs, Wrap},
|
||||
};
|
||||
use std::collections::BTreeSet;
|
||||
use std::time::{Duration, Instant};
|
||||
use std::{
|
||||
io::{self},
|
||||
thread,
|
||||
};
|
||||
use std::{ops::ControlFlow, path::Path};
|
||||
use termion::{
|
||||
event::{Event, Key, MouseButton, MouseEvent},
|
||||
input::{MouseTerminal, TermRead},
|
||||
raw::IntoRawMode,
|
||||
screen::IntoAlternateScreen,
|
||||
};
|
||||
|
||||
use std::sync::mpsc;
|
||||
|
||||
pub mod app;
|
||||
pub use app::*;
|
||||
|
||||
pub mod jobs;
|
||||
pub use jobs::*;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let mut app = App::new();
|
||||
let (status_tx, status_rx) = mpsc::channel::<StatusUpdate>();
|
||||
let stdout = io::stdout().into_raw_mode().unwrap();
|
||||
let stdout = MouseTerminal::from(stdout);
|
||||
let stdout = stdout.into_alternate_screen().unwrap();
|
||||
let backend = TermionBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
let stdin = io::stdin();
|
||||
let events = stdin.events();
|
||||
|
||||
let (input_tx, input_rx) = mpsc::channel::<Event>();
|
||||
let _input_handle = thread::spawn(move || {
|
||||
for evt in events {
|
||||
if let Ok(evt) = evt {
|
||||
if input_tx.send(evt).is_err() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
loop {
|
||||
let r = terminal.draw(|f| {
|
||||
let main_chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Length(30), Constraint::Min(0)])
|
||||
.split(f.area());
|
||||
render_left_recipes(&mut app, f, &main_chunks);
|
||||
|
||||
let right_chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Length(3), Constraint::Min(0)])
|
||||
.split(main_chunks[1]);
|
||||
render_right_tabs(&mut app, f, &right_chunks);
|
||||
|
||||
match app.right_tab_active {
|
||||
RightPanelTab::MainActions => {
|
||||
let right_style =
|
||||
app.box_style(" Main Actions ", "[Ctrl+C] Exit", Focus::MainPanel);
|
||||
let right_inner = right_style.inner(right_chunks[1]);
|
||||
f.render_widget(right_style, right_chunks[1]);
|
||||
let right_chunks_2 = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Length(25), Constraint::Min(0)])
|
||||
.split(right_inner);
|
||||
render_right_actions(&mut app, f, right_chunks_2[0]);
|
||||
}
|
||||
RightPanelTab::Terminal(_) => {
|
||||
render_right_terminal(f, &mut app, right_chunks[1]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
r.map_err(|e| Error::from_io_error(e, "Drawing to terminal pty"))?;
|
||||
|
||||
while let Ok(event) = input_rx.try_recv() {
|
||||
if let ControlFlow::Break(_) = handle_input(&mut app, &mut terminal, event, &status_tx)
|
||||
{
|
||||
kill_everything(None);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
while let Ok(update) = status_rx.try_recv() {
|
||||
app.exec.handle_status_update(update);
|
||||
}
|
||||
|
||||
std::thread::sleep(Duration::from_millis(16));
|
||||
}
|
||||
}
|
||||
|
||||
fn render_left_recipes(
|
||||
app: &mut App,
|
||||
f: &mut ratatui::prelude::Frame<'_>,
|
||||
main_chunks: &std::rc::Rc<[Rect]>,
|
||||
) {
|
||||
let left_chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(2),
|
||||
Constraint::Min(0),
|
||||
])
|
||||
.split(main_chunks[0]);
|
||||
|
||||
let search_title = if app.pinned_recipes.len() == 0 {
|
||||
format!(" Search ")
|
||||
} else {
|
||||
format!(" Search [Selected {}] ", app.pinned_recipes.len())
|
||||
};
|
||||
let search_style = app.box_style(&search_title, "", Focus::SearchAndList);
|
||||
let search_box = Paragraph::new(app.search_query.as_str()).block(search_style);
|
||||
f.render_widget(search_box, left_chunks[0]);
|
||||
|
||||
app.filter_tabs_area = left_chunks[1];
|
||||
{
|
||||
let split = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Length(1), Constraint::Min(0)])
|
||||
.split(left_chunks[1]);
|
||||
let labels = FilterSource::labels();
|
||||
let tabs = Tabs::new(vec![labels[0], labels[1]])
|
||||
.select(app.filter_source.index())
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
);
|
||||
f.render_widget(tabs, split[0]);
|
||||
let tabs = Tabs::new(vec![labels[2], labels[3]])
|
||||
.select(app.filter_source.index() - 2)
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
);
|
||||
f.render_widget(tabs, split[1]);
|
||||
}
|
||||
|
||||
let list_style = app.box_style(" Recipes ", "[Ctrl+A] Select All", Focus::SearchAndList);
|
||||
|
||||
let items: Vec<ListItem> = app
|
||||
.filtered_recipes
|
||||
.iter()
|
||||
.map(|name| {
|
||||
let is_pinned = app.pinned_recipes.contains(name);
|
||||
let prefix = if is_pinned { "* " } else { " " };
|
||||
let style = if is_pinned {
|
||||
Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
Style::default()
|
||||
};
|
||||
ListItem::new(Line::from(vec![
|
||||
Span::styled(prefix, Style::default().fg(Color::Green)),
|
||||
Span::styled(name.as_str(), style),
|
||||
]))
|
||||
})
|
||||
.collect();
|
||||
|
||||
let list = List::new(items)
|
||||
.block(list_style)
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.bg(Color::DarkGray)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
)
|
||||
.highlight_symbol(">");
|
||||
f.render_stateful_widget(list, left_chunks[2], &mut app.list_state);
|
||||
}
|
||||
|
||||
fn render_right_tabs(
|
||||
app: &mut App,
|
||||
f: &mut ratatui::prelude::Frame<'_>,
|
||||
right_chunks: &std::rc::Rc<[Rect]>,
|
||||
) {
|
||||
let mut right_tab_titles = vec![" [F6] Build ".to_string()];
|
||||
let mut fbtn = 6;
|
||||
for job in &app.exec.active_job_order {
|
||||
let Some(job) = app.exec.jobs.get(&job) else {
|
||||
continue;
|
||||
};
|
||||
let fstr = if fbtn > 12 {
|
||||
"".into()
|
||||
} else {
|
||||
fbtn += 1;
|
||||
format!("[F{}] ", fbtn)
|
||||
};
|
||||
right_tab_titles.push(format!("{}{}", fstr, job));
|
||||
}
|
||||
let selected_tab_idx = match &app.right_tab_active {
|
||||
RightPanelTab::MainActions => 0,
|
||||
RightPanelTab::Terminal(i) => *i + 1,
|
||||
};
|
||||
app.right_tabs_area = right_chunks[0];
|
||||
let top_right_tabs = Tabs::new(right_tab_titles.clone())
|
||||
.select(selected_tab_idx)
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.block(app.box_style("", "", Focus::MainPanel))
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
);
|
||||
app.right_tabs_str = right_tab_titles
|
||||
.into_iter()
|
||||
.map(|s| s.to_string())
|
||||
.collect();
|
||||
f.render_widget(top_right_tabs, right_chunks[0]);
|
||||
}
|
||||
|
||||
fn render_right_actions(app: &mut App, f: &mut ratatui::prelude::Frame<'_>, right_layout: Rect) {
|
||||
let active_targets = app.selected_recipes();
|
||||
if !active_targets.is_empty() {
|
||||
let btn_count = app.buttons.len();
|
||||
let info_chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Length(3),
|
||||
Constraint::Length((btn_count * 3) as _),
|
||||
])
|
||||
.split(right_layout);
|
||||
|
||||
let target_str = if active_targets.len() == 1 {
|
||||
active_targets[0].to_string()
|
||||
} else {
|
||||
format!("{} recipes", active_targets.len())
|
||||
};
|
||||
|
||||
let info_text = vec![Line::from(vec![
|
||||
Span::raw("Selected: "),
|
||||
Span::styled(
|
||||
target_str,
|
||||
Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
])];
|
||||
f.render_widget(Paragraph::new(info_text), info_chunks[0]);
|
||||
|
||||
let btn_constraints = vec![Constraint::Percentage(100 / btn_count as u16); btn_count];
|
||||
let btn_chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(btn_constraints)
|
||||
.split(info_chunks[1]);
|
||||
|
||||
for (idx, btn) in app.buttons.iter_mut().enumerate() {
|
||||
btn.area = btn_chunks[idx];
|
||||
let btn_widget = Paragraph::new(btn.label)
|
||||
.alignment(ratatui::layout::Alignment::Center)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(Color::Gray)),
|
||||
);
|
||||
f.render_widget(btn_widget, btn.area);
|
||||
}
|
||||
} else {
|
||||
let no_select = Paragraph::new("No recipe selected");
|
||||
f.render_widget(no_select, right_layout);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_right_terminal(f: &mut ratatui::Frame, app: &mut App, area: Rect) {
|
||||
let Some(id) = app.current_job_id() else {
|
||||
return;
|
||||
};
|
||||
let is_focus = app.focus == Focus::MainPanel;
|
||||
let Some(job) = &mut app.exec.jobs.get_mut(&id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let title = format!(" Job #{}: repo {}", job.id, job);
|
||||
let hint = if job.exit_code.is_some() {
|
||||
"[X] Close [Ctrl+C] Exit"
|
||||
} else {
|
||||
"[Ctrl+C] Stop"
|
||||
};
|
||||
|
||||
let auto_scroll = job.auto_scroll;
|
||||
let lines = render_build_log(
|
||||
area.height as _,
|
||||
Some(&job.logs),
|
||||
Some(String::from_utf8_lossy(&job.buffer)),
|
||||
true,
|
||||
&mut job.auto_scroll,
|
||||
&mut job.scroll,
|
||||
None,
|
||||
);
|
||||
|
||||
let line_block = {
|
||||
// TODO: Copied from app.box_style (borrowing issue)
|
||||
let style = if is_focus {
|
||||
Style::default().fg(Color::Cyan)
|
||||
} else {
|
||||
Style::default()
|
||||
};
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title(title)
|
||||
.title_bottom(hint)
|
||||
.border_style(style)
|
||||
};
|
||||
let mut paragraph = Paragraph::new(lines).block(line_block);
|
||||
|
||||
if !auto_scroll {
|
||||
paragraph = paragraph.wrap(Wrap { trim: false });
|
||||
}
|
||||
|
||||
f.render_widget(paragraph, area);
|
||||
}
|
||||
|
||||
fn handle_input<B: ratatui::backend::Backend>(
|
||||
app: &mut App,
|
||||
terminal: &mut Terminal<B>,
|
||||
event: Event,
|
||||
status_tx: &mpsc::Sender<StatusUpdate>,
|
||||
) -> ControlFlow<()> {
|
||||
match event {
|
||||
Event::Key(Key::Ctrl('c')) => {
|
||||
if let Some(job) = app.current_job()
|
||||
&& job.pid.is_some()
|
||||
{
|
||||
kill_everything(job.pid);
|
||||
} else {
|
||||
return ControlFlow::Break(());
|
||||
}
|
||||
}
|
||||
|
||||
Event::Key(Key::F(n)) => {
|
||||
if let Some(fi) = FilterSource::try_from_index((n - 2) as _) {
|
||||
if app.filter_source != fi {
|
||||
app.filter_source = fi;
|
||||
app.update_filter();
|
||||
}
|
||||
app.focus = Focus::SearchAndList;
|
||||
} else if n == 6 {
|
||||
app.right_tab_active = RightPanelTab::MainActions;
|
||||
app.focus = Focus::MainPanel;
|
||||
} else if n != 0 {
|
||||
app.right_tab_active = RightPanelTab::Terminal((n - 7) as _);
|
||||
app.focus = Focus::MainPanel;
|
||||
}
|
||||
}
|
||||
|
||||
Event::Key(Key::Ctrl('a')) => {
|
||||
app.pin_all_filtered();
|
||||
}
|
||||
|
||||
Event::Key(Key::Ctrl('r')) => {
|
||||
app.read_recipes();
|
||||
}
|
||||
|
||||
Event::Key(Key::Char('x')) => {
|
||||
if let Some(id) = app.current_job_id() {
|
||||
app.exec.close_job(&id);
|
||||
app.right_tab_active = RightPanelTab::MainActions;
|
||||
}
|
||||
}
|
||||
|
||||
Event::Key(key) => match app.focus {
|
||||
Focus::SearchAndList => match key {
|
||||
Key::Char('\t') => {
|
||||
app.toggle_pinned_highlighted();
|
||||
}
|
||||
Key::Char('\n') => {
|
||||
app.focus = Focus::MainPanel;
|
||||
}
|
||||
Key::Char(c) => {
|
||||
app.search_query.push(c);
|
||||
app.update_filter();
|
||||
}
|
||||
Key::Backspace => {
|
||||
app.search_query.pop();
|
||||
app.update_filter();
|
||||
}
|
||||
Key::Up => app.select_delta(-1, true),
|
||||
Key::Down => app.select_delta(1, true),
|
||||
_ => {}
|
||||
},
|
||||
Focus::MainPanel => match key {
|
||||
Key::Char('\n') | Key::Esc => {
|
||||
app.focus = Focus::SearchAndList;
|
||||
}
|
||||
Key::Char('\t') => {
|
||||
app.toggle_pinned_highlighted();
|
||||
}
|
||||
Key::Down => app.select_delta(1, true),
|
||||
Key::Char(c) => {
|
||||
let matched_action = app.buttons.iter().find(|b| b.key == c).map(|b| b.cmd);
|
||||
let targets = app.selected_recipes();
|
||||
if let (Some(cmd), false, true) = (
|
||||
matched_action,
|
||||
targets.is_empty(),
|
||||
app.current_job_id().is_none(),
|
||||
) {
|
||||
app.exec.spawn_job(cmd, targets, status_tx.clone());
|
||||
app.right_tab_active =
|
||||
RightPanelTab::Terminal(app.exec.active_job_order.len() - 1);
|
||||
}
|
||||
}
|
||||
key => {
|
||||
if let Some(job) = app.current_job_mut() {
|
||||
match key {
|
||||
Key::Up => {
|
||||
job.auto_scroll = false;
|
||||
job.scroll = job.scroll.saturating_add_signed(-1);
|
||||
}
|
||||
Key::Down => {
|
||||
job.auto_scroll = false;
|
||||
job.scroll = job.scroll.saturating_add_signed(1);
|
||||
}
|
||||
Key::PageUp => {
|
||||
job.auto_scroll = false;
|
||||
job.scroll = job.scroll.saturating_add_signed(-20);
|
||||
}
|
||||
Key::PageDown => {
|
||||
job.auto_scroll = false;
|
||||
job.scroll = job.scroll.saturating_add_signed(20);
|
||||
}
|
||||
Key::Home => {
|
||||
job.auto_scroll = false;
|
||||
job.scroll = 0;
|
||||
}
|
||||
Key::End => {
|
||||
job.auto_scroll = true;
|
||||
job.scroll = usize::MAX;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
Event::Mouse(mouse_event) => {
|
||||
let (x, y) = match mouse_event {
|
||||
MouseEvent::Press(_, x, y) => (x, y),
|
||||
MouseEvent::Hold(x, y) => (x, y),
|
||||
_ => (0, 0),
|
||||
};
|
||||
let mx = x.saturating_sub(1);
|
||||
let my = y.saturating_sub(1);
|
||||
let mp = Position::new(mx, my);
|
||||
|
||||
let size = terminal.size().unwrap();
|
||||
let viewport = Rect::new(0, 0, size.width, size.height);
|
||||
|
||||
let main_chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Length(30), Constraint::Min(0)])
|
||||
.split(viewport);
|
||||
|
||||
let left_chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(2),
|
||||
Constraint::Min(0),
|
||||
])
|
||||
.split(main_chunks[0]);
|
||||
|
||||
match mouse_event {
|
||||
MouseEvent::Press(
|
||||
btn @ MouseButton::WheelUp | btn @ MouseButton::WheelDown,
|
||||
_,
|
||||
_,
|
||||
) => {
|
||||
let delta = if btn == MouseButton::WheelUp { -1 } else { 1 };
|
||||
if main_chunks[0].contains(mp) {
|
||||
app.focus = Focus::SearchAndList;
|
||||
app.select_delta(delta, false);
|
||||
} else {
|
||||
app.focus = Focus::MainPanel;
|
||||
if let Some(i) = app.current_job_mut() {
|
||||
i.scroll = i.scroll.saturating_add_signed(delta);
|
||||
i.auto_scroll = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
MouseEvent::Press(MouseButton::Left, _, _) | MouseEvent::Hold(_, _) => {
|
||||
if app.filter_tabs_area.contains(mp) {
|
||||
let tabw = app.filter_tabs_area.width / 2;
|
||||
let tabh = app.filter_tabs_area.height / 2;
|
||||
let relative_x = mx.saturating_sub(app.filter_tabs_area.x);
|
||||
let relative_y = my.saturating_sub(app.filter_tabs_area.y);
|
||||
let index = (relative_x / tabw) + (relative_y / tabh * 2);
|
||||
app.filter_source =
|
||||
FilterSource::try_from_index(index as _).unwrap_or(FilterSource::All);
|
||||
app.update_filter();
|
||||
} else if app.right_tabs_area.contains(mp) {
|
||||
app.focus = Focus::MainPanel;
|
||||
|
||||
if let Some(i) =
|
||||
get_clicked_tab_index(app.right_tabs_area, mp, &app.right_tabs_str, 3)
|
||||
{
|
||||
if i == 0 {
|
||||
app.right_tab_active = RightPanelTab::MainActions;
|
||||
} else {
|
||||
app.right_tab_active = RightPanelTab::Terminal(i - 1);
|
||||
}
|
||||
}
|
||||
} else if left_chunks[2].contains(mp) {
|
||||
app.focus = Focus::SearchAndList;
|
||||
|
||||
let list_top = left_chunks[2].y + 1;
|
||||
if my >= list_top && my < left_chunks[2].y + left_chunks[2].height - 1 {
|
||||
let clicked_visible_row = (my - list_top) as usize;
|
||||
let offset = app.list_state.offset();
|
||||
let target_idx = offset + clicked_visible_row;
|
||||
|
||||
if target_idx < app.filtered_recipes.len() {
|
||||
app.list_state.select(Some(target_idx));
|
||||
app.right_tab_active = RightPanelTab::MainActions;
|
||||
|
||||
let now = Instant::now();
|
||||
if let (Some(last_time), Some(last_idx)) =
|
||||
(app.last_click_time, app.last_click_idx)
|
||||
{
|
||||
if last_idx == target_idx
|
||||
&& now.duration_since(last_time)
|
||||
< Duration::from_millis(300)
|
||||
{
|
||||
app.toggle_pinned_highlighted();
|
||||
}
|
||||
}
|
||||
app.last_click_time = Some(now);
|
||||
app.last_click_idx = Some(target_idx);
|
||||
}
|
||||
}
|
||||
} else if main_chunks[1].contains(mp) {
|
||||
app.focus = Focus::MainPanel;
|
||||
|
||||
if app.current_job_id().is_none() {}
|
||||
let mut clicked_command = None;
|
||||
for btn in &app.buttons {
|
||||
if btn.area.contains(mp) {
|
||||
clicked_command = Some(btn.cmd);
|
||||
break;
|
||||
}
|
||||
}
|
||||
let targets = app.selected_recipes();
|
||||
if let Some(cmd) = clicked_command
|
||||
&& !targets.is_empty()
|
||||
&& app.current_job_id().is_none()
|
||||
{
|
||||
app.exec.spawn_job(cmd, targets, status_tx.clone());
|
||||
app.right_tab_active =
|
||||
RightPanelTab::Terminal(app.exec.active_job_order.len() - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
ControlFlow::Continue(())
|
||||
}
|
||||
@ -8,3 +8,4 @@ pub mod package;
|
||||
pub mod pty;
|
||||
pub mod script;
|
||||
pub mod tree;
|
||||
pub mod tui;
|
||||
|
||||
130
src/cook/pty.rs
130
src/cook/pty.rs
@ -24,6 +24,8 @@ pub(crate) use log_to_pty;
|
||||
|
||||
pub type PtyOut<'a> = Option<(&'a mut UnixSlavePty, &'a mut PipeWriter)>;
|
||||
|
||||
/// Setup pty without stdin.
|
||||
/// Returns (pty stdout, log out, pty+log writer)
|
||||
pub fn setup_pty() -> (
|
||||
Box<dyn Read + Send>,
|
||||
PipeReader,
|
||||
@ -38,7 +40,6 @@ pub fn setup_pty() -> (
|
||||
})
|
||||
.expect("Unable to open pty");
|
||||
|
||||
// TODO: There's no way to handle stdin
|
||||
let pty_reader = pair
|
||||
.master
|
||||
.try_clone_reader()
|
||||
@ -49,6 +50,41 @@ pub fn setup_pty() -> (
|
||||
(pty_reader, log_reader, pipes)
|
||||
}
|
||||
|
||||
/// Setup pty with stdin.
|
||||
/// Returns (pty stdout, log out, pty stdin, pty+log writer)
|
||||
pub fn setup_pty_with_stdin() -> (
|
||||
Box<dyn Read + Send>,
|
||||
PipeReader,
|
||||
Box<dyn Write + Send>,
|
||||
(UnixSlavePty, PipeWriter),
|
||||
) {
|
||||
let pty_system = UnixPtySystem::default();
|
||||
let pair = pty_system
|
||||
.openpty(PtySize {
|
||||
rows: 24,
|
||||
cols: 80,
|
||||
pixel_width: 0,
|
||||
pixel_height: 0,
|
||||
})
|
||||
.expect("Unable to open pty");
|
||||
|
||||
let pty_reader = pair
|
||||
.master
|
||||
.try_clone_reader()
|
||||
.expect("Unable to clone pty reader");
|
||||
|
||||
let pty_writer = pair
|
||||
.master
|
||||
.try_clone_writer()
|
||||
.expect("Unable to clone pty writer");
|
||||
|
||||
let (log_reader, log_writer) = std::io::pipe().expect("Failed to create log pipe");
|
||||
|
||||
let pipes = (pair.slave, log_writer);
|
||||
|
||||
(pty_reader, log_reader, pty_writer, pipes)
|
||||
}
|
||||
|
||||
pub fn flush_pty(logger: &mut PtyOut) {
|
||||
let Some((pty, file)) = logger else {
|
||||
return;
|
||||
@ -66,6 +102,13 @@ pub fn spawn_to_pipe(command: &mut Command, stdout_pipe: &PtyOut) -> Result<Chil
|
||||
}
|
||||
}
|
||||
|
||||
pub fn spawn_to_pipe_with_stdin(command: &mut Command, stdout_pipe: &PtyOut) -> Result<Child> {
|
||||
match stdout_pipe {
|
||||
Some(stdout) => stdout.0.spawn_command_stdin(command.into()),
|
||||
None => Ok(command.spawn().map_err(wrap_io_err!("Spawning"))?),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_to_pty(pty: &PtyOut, text: &str) {
|
||||
log_to_pty!(pty, "{}", text);
|
||||
}
|
||||
@ -188,6 +231,16 @@ impl Read for PtyFd {
|
||||
}
|
||||
}
|
||||
|
||||
impl Write for PtyFd {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
self.0.write(buf)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.0.flush()
|
||||
}
|
||||
}
|
||||
|
||||
impl PtyFd {
|
||||
fn resize(&self, size: PtySize) -> Result<()> {
|
||||
let ws_size = winsize {
|
||||
@ -231,37 +284,40 @@ impl PtyFd {
|
||||
})
|
||||
}
|
||||
|
||||
fn spawn_command(&self, cmd: &mut Command) -> Result<std::process::Child> {
|
||||
fn spawn_command(&self, cmd: &mut Command, stdin: bool) -> Result<std::process::Child> {
|
||||
cmd.stdout(self.try_clone().map_err(wrap_io_err!("Cloning pty"))?)
|
||||
.stderr(self.try_clone().map_err(wrap_io_err!("Cloning pty"))?);
|
||||
|
||||
if stdin {
|
||||
cmd.stdin(self.try_clone().map_err(wrap_io_err!("Cloning pty"))?);
|
||||
}
|
||||
|
||||
unsafe {
|
||||
cmd
|
||||
// .stdin(self.as_stdio()?)
|
||||
.stdout(self.try_clone().map_err(wrap_io_err!("Cloning pty"))?)
|
||||
.stderr(self.try_clone().map_err(wrap_io_err!("Cloning pty"))?)
|
||||
.pre_exec(move || {
|
||||
// Clean up a few things before we exec the program
|
||||
// Clear out any potentially problematic signal
|
||||
// dispositions that we might have inherited
|
||||
for signo in &[
|
||||
libc::SIGCHLD,
|
||||
libc::SIGHUP,
|
||||
libc::SIGINT,
|
||||
libc::SIGQUIT,
|
||||
libc::SIGTERM,
|
||||
libc::SIGALRM,
|
||||
] {
|
||||
libc::signal(*signo, libc::SIG_DFL);
|
||||
}
|
||||
cmd.pre_exec(move || {
|
||||
// Clean up a few things before we exec the program
|
||||
// Clear out any potentially problematic signal
|
||||
// dispositions that we might have inherited
|
||||
for signo in &[
|
||||
libc::SIGCHLD,
|
||||
libc::SIGHUP,
|
||||
libc::SIGINT,
|
||||
libc::SIGQUIT,
|
||||
libc::SIGTERM,
|
||||
libc::SIGALRM,
|
||||
] {
|
||||
libc::signal(*signo, libc::SIG_DFL);
|
||||
}
|
||||
|
||||
let empty_set: libc::sigset_t = std::mem::zeroed();
|
||||
libc::sigprocmask(libc::SIG_SETMASK, &empty_set, std::ptr::null_mut());
|
||||
let empty_set: libc::sigset_t = std::mem::zeroed();
|
||||
libc::sigprocmask(libc::SIG_SETMASK, &empty_set, std::ptr::null_mut());
|
||||
|
||||
// Establish ourselves as a session leader.
|
||||
if libc::setsid() == -1 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
// Establish ourselves as a session leader.
|
||||
if libc::setsid() == -1 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
Ok(())
|
||||
})
|
||||
};
|
||||
|
||||
let mut child = cmd.spawn().map_err(wrap_io_err!("Spawning cmd"))?;
|
||||
@ -271,7 +327,9 @@ impl PtyFd {
|
||||
// them) and won't work in the usual way anyway.
|
||||
// In practice these are None, but it seems best to be move them
|
||||
// out in case the behavior of Command changes in the future.
|
||||
// child.stdin.take();
|
||||
if stdin {
|
||||
child.stdin.take();
|
||||
}
|
||||
child.stdout.take();
|
||||
child.stderr.take();
|
||||
|
||||
@ -310,7 +368,10 @@ fn cloexec(fd: RawFd) -> Result<()> {
|
||||
|
||||
impl UnixSlavePty {
|
||||
fn spawn_command(&self, builder: &mut Command) -> Result<std::process::Child> {
|
||||
Ok(self.fd.spawn_command(builder)?)
|
||||
Ok(self.fd.spawn_command(builder, false)?)
|
||||
}
|
||||
fn spawn_command_stdin(&self, builder: &mut Command) -> Result<std::process::Child> {
|
||||
Ok(self.fd.spawn_command(builder, true)?)
|
||||
}
|
||||
fn flush(&mut self) -> Result<()> {
|
||||
self.fd.flush()
|
||||
@ -336,4 +397,13 @@ impl UnixMasterPty {
|
||||
);
|
||||
Ok(Box::new(fd))
|
||||
}
|
||||
|
||||
pub fn try_clone_writer(&self) -> Result<Box<dyn Write + Send>> {
|
||||
let fd = PtyFd(
|
||||
self.fd
|
||||
.try_clone()
|
||||
.map_err(wrap_io_err!("Cloning pty fd"))?,
|
||||
);
|
||||
Ok(Box::new(fd))
|
||||
}
|
||||
}
|
||||
|
||||
174
src/cook/tui.rs
Normal file
174
src/cook/tui.rs
Normal file
@ -0,0 +1,174 @@
|
||||
// TUI utilities, shared between repo and repo_editor
|
||||
|
||||
use ansi_to_tui::IntoText;
|
||||
use std::{borrow::Cow, cmp, process};
|
||||
|
||||
use ratatui::{
|
||||
layout::{Position, Rect},
|
||||
style::{Color, Style},
|
||||
text::{Line, Text},
|
||||
};
|
||||
|
||||
use crate::cook::script::KILL_ALL_PID;
|
||||
|
||||
/// Drain `buffer` to `lines` when new lines occurred. Does `"\r"` trimming.
|
||||
pub fn drain_buffer_to_lines(buffer: &mut Vec<u8>, lines: &mut Vec<String>) -> usize {
|
||||
let mut addition = 0;
|
||||
// TODO: multibyte-aware line split?
|
||||
while let Some(newline_pos) = buffer.iter().position(|&b| b == b'\n') {
|
||||
let line_bytes = buffer.drain(..=newline_pos);
|
||||
let line_str = String::from_utf8_lossy(&line_bytes.as_slice());
|
||||
let line_str_pos = line_str.trim_end();
|
||||
let line_str = line_str_pos.rsplit('\r').next().unwrap_or(&line_str_pos);
|
||||
lines.push(line_str.to_owned());
|
||||
addition += 1;
|
||||
}
|
||||
addition
|
||||
}
|
||||
|
||||
/// Render build log lines, stateful
|
||||
pub fn render_build_log<'a>(
|
||||
panel_height: usize,
|
||||
log_text: Option<&'a Vec<String>>,
|
||||
log_line: Option<Cow<'a, str>>,
|
||||
may_autoscroll: bool,
|
||||
enable_auto_scroll: &'a mut bool,
|
||||
intended_scroll_pos: &'a mut usize,
|
||||
search_result: Option<(usize, &'a Vec<usize>)>,
|
||||
) -> Vec<Line<'a>> {
|
||||
let mut log_lines: Vec<Line> = if let Some(log_text) = log_text
|
||||
&& !log_text.is_empty()
|
||||
{
|
||||
let total_log_lines = log_text.len() as usize;
|
||||
|
||||
let start = if *enable_auto_scroll {
|
||||
if total_log_lines > panel_height {
|
||||
*intended_scroll_pos = total_log_lines - panel_height;
|
||||
total_log_lines - panel_height
|
||||
} else {
|
||||
0
|
||||
}
|
||||
} else {
|
||||
if total_log_lines > panel_height {
|
||||
let limit = 2; // arbitrary number
|
||||
if *intended_scroll_pos >= total_log_lines - limit {
|
||||
if may_autoscroll {
|
||||
*enable_auto_scroll = true;
|
||||
}
|
||||
*intended_scroll_pos = total_log_lines - limit;
|
||||
total_log_lines - limit
|
||||
} else {
|
||||
*intended_scroll_pos
|
||||
}
|
||||
} else {
|
||||
0
|
||||
}
|
||||
};
|
||||
|
||||
let end = cmp::min(panel_height + start, total_log_lines - 1);
|
||||
|
||||
log_text[start..end]
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, s)| {
|
||||
if let Some((search_idx, search_results)) = search_result {
|
||||
let absolute_i = start + i;
|
||||
if absolute_i == search_results[search_idx] {
|
||||
let s = strip_ansi_escapes::strip_str(s);
|
||||
return Line::from(s)
|
||||
.style(Style::default().bg(Color::Yellow).fg(Color::Black));
|
||||
} else if search_results.binary_search(&absolute_i).is_ok() {
|
||||
let s = strip_ansi_escapes::strip_str(s);
|
||||
return Line::from(s)
|
||||
.style(Style::default().bg(Color::Black).fg(Color::White));
|
||||
}
|
||||
}
|
||||
let text_with_colors = s
|
||||
.into_text()
|
||||
.unwrap_or_else(|_| Text::raw("--unrenderable line--"));
|
||||
text_with_colors
|
||||
.lines
|
||||
.into_iter()
|
||||
.next()
|
||||
.unwrap_or_else(|| Line::raw("--unrenderable line--"))
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
if let Some(buffer) = log_line
|
||||
&& !buffer.is_empty()
|
||||
{
|
||||
let text_with_colors = handle_cr(&buffer)
|
||||
.into_text()
|
||||
.unwrap_or_else(|_| Text::raw("--unrenderable line--"));
|
||||
|
||||
if let Some(line) = text_with_colors.lines.into_iter().next() {
|
||||
log_lines.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
if log_lines.is_empty() {
|
||||
log_lines.push(Line::from("No logs yet"));
|
||||
}
|
||||
log_lines
|
||||
}
|
||||
|
||||
/// Join logs for logging
|
||||
pub fn join_logs(log: &Vec<String>, line: Option<Cow<'_, str>>) -> String {
|
||||
let mut logs = log.join("\n");
|
||||
if let Some(line) = line {
|
||||
logs.push_str("\n");
|
||||
logs.push_str(handle_cr(&line));
|
||||
}
|
||||
logs
|
||||
}
|
||||
|
||||
fn handle_cr<'a>(buffer: &'a Cow<'_, str>) -> &'a str {
|
||||
let st = buffer.trim_end();
|
||||
st.rsplit('\r').next().unwrap_or(&st)
|
||||
}
|
||||
|
||||
/// Check if point is in rect
|
||||
pub fn get_clicked_tab_index(
|
||||
area: Rect,
|
||||
mp: Position,
|
||||
titles: &Vec<String>,
|
||||
divider_len: u16,
|
||||
) -> Option<usize> {
|
||||
if !area.contains(mp) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut current_x = area.x;
|
||||
let mx = mp.x;
|
||||
|
||||
for (idx, title) in titles.iter().enumerate() {
|
||||
let title_width = title.chars().count() as u16;
|
||||
let tab_end_x = current_x + title_width;
|
||||
|
||||
if mx >= current_x && mx < tab_end_x {
|
||||
return Some(idx);
|
||||
}
|
||||
|
||||
current_x = tab_end_x + divider_len;
|
||||
|
||||
if current_x >= area.x + area.width {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Kill everything under given PID or this process PID.
|
||||
pub fn kill_everything(pid: Option<u32>) {
|
||||
process::Command::new("bash")
|
||||
.arg("-c")
|
||||
.arg(KILL_ALL_PID.replace("$PID", &pid.unwrap_or_else(|| process::id()).to_string()))
|
||||
.stdout(process::Stdio::null())
|
||||
.stderr(process::Stdio::null())
|
||||
.spawn()
|
||||
.expect("unable to spawn kill");
|
||||
}
|
||||
@ -6,6 +6,9 @@ use std::sync::LazyLock;
|
||||
|
||||
use pkg::{Package, PackageError, PackageName};
|
||||
|
||||
use crate::bail_other_err;
|
||||
use crate::cook::fs;
|
||||
|
||||
// This file contains code that caches recipe paths.
|
||||
|
||||
// TODO: This file is previously resides in `pkg` crate,
|
||||
@ -45,12 +48,22 @@ pub fn find(recipe: &str) -> Option<&'static Path> {
|
||||
RECIPE_PATHS.get(recipe).map(PathBuf::as_path)
|
||||
}
|
||||
|
||||
pub fn list(prefix: impl AsRef<Path>) -> BTreeSet<PathBuf> {
|
||||
let prefix = prefix.as_ref();
|
||||
RECIPE_PATHS
|
||||
.values()
|
||||
.map(|path| prefix.join(path))
|
||||
.collect()
|
||||
pub fn list() -> BTreeSet<PathBuf> {
|
||||
RECIPE_PATHS.values().map(|p| p.to_path_buf()).collect()
|
||||
}
|
||||
|
||||
pub fn list_repo(repo_dir: impl AsRef<Path>) -> crate::Result<BTreeSet<PathBuf>> {
|
||||
let repo_dir = repo_dir.as_ref();
|
||||
let repo_toml_path = repo_dir.join(redoxer::target()).join("repo.toml");
|
||||
if !repo_toml_path.is_file() {
|
||||
bail_other_err!("The repository is not found: {}", repo_toml_path.display());
|
||||
};
|
||||
let repos: pkg::Repository = fs::read_toml(&repo_toml_path)?;
|
||||
Ok(repos
|
||||
.packages
|
||||
.keys()
|
||||
.filter_map(|n| find(n).map(|s| s.to_path_buf()))
|
||||
.collect::<BTreeSet<_>>())
|
||||
}
|
||||
|
||||
pub fn new(name: &PackageName) -> Result<Package, PackageError> {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user