merge: multi-pod open return
This commit is contained in:
commit
d79b5d5cc4
|
|
@ -44,6 +44,8 @@ use crate::app::App;
|
|||
use crate::picker::PickerOutcome;
|
||||
use crate::spawn::{SpawnOutcome, SpawnReady};
|
||||
|
||||
type FullscreenTerminal = Terminal<CrosstermBackend<io::Stdout>>;
|
||||
|
||||
fn resolve_socket(pod_name: &str, override_path: Option<PathBuf>) -> PathBuf {
|
||||
if let Some(p) = override_path {
|
||||
return p;
|
||||
|
|
@ -289,33 +291,95 @@ async fn run_pod_name(
|
|||
pod_name: String,
|
||||
socket_override: Option<PathBuf>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let preferred_socket = resolve_socket(&pod_name, socket_override.clone());
|
||||
if let Some((_socket_path, client)) =
|
||||
connect_live_pod(&pod_name, preferred_socket, socket_override.is_none()).await
|
||||
{
|
||||
if let Some(client) = try_connect_live_pod(&pod_name, socket_override.clone()).await {
|
||||
let mut terminal = enter_fullscreen()?;
|
||||
let mut app = App::new(pod_name);
|
||||
app.connected = true;
|
||||
return run_loop(&mut terminal, &mut app, client).await;
|
||||
run_connected_pod(&mut terminal, pod_name, client).await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let ready = match spawn::run_pod_name(pod_name).await? {
|
||||
SpawnOutcome::Ready(r) => r,
|
||||
SpawnOutcome::Cancelled => return Ok(()),
|
||||
};
|
||||
let mut terminal = enter_fullscreen()?;
|
||||
terminal.clear()?;
|
||||
let result = run_ready_pod(&mut terminal, ready).await;
|
||||
let _ = leave_fullscreen(&mut terminal);
|
||||
result
|
||||
}
|
||||
|
||||
async fn run_connected_pod(
|
||||
terminal: &mut FullscreenTerminal,
|
||||
pod_name: String,
|
||||
client: PodClient,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut app = App::new(pod_name);
|
||||
app.connected = true;
|
||||
run_loop(terminal, &mut app, client).await
|
||||
}
|
||||
|
||||
async fn run_pod_name_nested(
|
||||
terminal: &mut FullscreenTerminal,
|
||||
request: multi_pod::OpenPodRequest,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let multi_pod::OpenPodRequest {
|
||||
pod_name,
|
||||
socket_override,
|
||||
} = request;
|
||||
|
||||
if let Some(client) = try_connect_live_pod(&pod_name, socket_override).await {
|
||||
return run_connected_pod(terminal, pod_name, client).await;
|
||||
}
|
||||
|
||||
let ready = spawn_pod_name_from_fullscreen(terminal, &pod_name).await?;
|
||||
run_ready_pod(terminal, ready).await
|
||||
}
|
||||
|
||||
async fn spawn_pod_name_from_fullscreen(
|
||||
terminal: &mut FullscreenTerminal,
|
||||
pod_name: &str,
|
||||
) -> Result<SpawnReady, Box<dyn std::error::Error>> {
|
||||
leave_fullscreen(terminal)?;
|
||||
let outcome = spawn::run_pod_name(pod_name.to_string()).await;
|
||||
enter_fullscreen_existing(terminal)?;
|
||||
terminal.clear()?;
|
||||
|
||||
match outcome? {
|
||||
SpawnOutcome::Ready(ready) => Ok(ready),
|
||||
SpawnOutcome::Cancelled => Err(Box::new(NestedOpenCancelled)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn try_connect_live_pod(
|
||||
pod_name: &str,
|
||||
socket_override: Option<PathBuf>,
|
||||
) -> Option<PodClient> {
|
||||
let preferred_socket = resolve_socket(pod_name, socket_override.clone());
|
||||
connect_live_pod(pod_name, preferred_socket, socket_override.is_none())
|
||||
.await
|
||||
.map(|(_, client)| client)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct NestedOpenCancelled;
|
||||
|
||||
impl std::fmt::Display for NestedOpenCancelled {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str("Pod open was cancelled")
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for NestedOpenCancelled {}
|
||||
|
||||
async fn run_ready_pod(
|
||||
terminal: &mut FullscreenTerminal,
|
||||
ready: SpawnReady,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let SpawnReady {
|
||||
pod_name,
|
||||
socket_path,
|
||||
} = ready;
|
||||
|
||||
let mut terminal = enter_fullscreen()?;
|
||||
let result = run(&mut terminal, pod_name, &socket_path).await;
|
||||
let _ = execute!(
|
||||
terminal.backend_mut(),
|
||||
DisableMouseCapture,
|
||||
LeaveAlternateScreen
|
||||
);
|
||||
result
|
||||
run(terminal, pod_name, &socket_path).await
|
||||
}
|
||||
|
||||
async fn connect_live_pod(
|
||||
|
|
@ -354,24 +418,37 @@ async fn run_resume() -> Result<(), Box<dyn std::error::Error>> {
|
|||
}
|
||||
|
||||
async fn run_multi() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut app = multi_pod::load_app().await?;
|
||||
let mut terminal = enter_fullscreen()?;
|
||||
let outcome = multi_pod::run(&mut terminal).await;
|
||||
|
||||
let _ = execute!(
|
||||
terminal.backend_mut(),
|
||||
DisableMouseCapture,
|
||||
LeaveAlternateScreen
|
||||
);
|
||||
|
||||
match outcome? {
|
||||
multi_pod::MultiPodOutcome::Quit => Ok(()),
|
||||
multi_pod::MultiPodOutcome::Open {
|
||||
pod_name,
|
||||
socket_override,
|
||||
} => run_pod_name(pod_name, socket_override).await,
|
||||
loop {
|
||||
match multi_pod::run(&mut terminal, &mut app).await? {
|
||||
multi_pod::MultiPodOutcome::Quit => {
|
||||
let _ = leave_fullscreen(&mut terminal);
|
||||
return Ok(());
|
||||
}
|
||||
multi_pod::MultiPodOutcome::Open(request) => {
|
||||
let pod_name = request.pod_name.clone();
|
||||
match run_pod_name_nested(&mut terminal, request).await {
|
||||
Ok(()) => app.finish_open(&pod_name, Ok(())),
|
||||
Err(error) if is_recoverable_multi_open_error(error.as_ref()) => {
|
||||
app.finish_open(&pod_name, Err(error.as_ref()));
|
||||
}
|
||||
Err(error) => {
|
||||
let _ = leave_fullscreen(&mut terminal);
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
app.reload().await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_recoverable_multi_open_error(error: &(dyn std::error::Error + 'static)) -> bool {
|
||||
error.is::<spawn::SpawnError>() || error.is::<NestedOpenCancelled>()
|
||||
}
|
||||
|
||||
async fn run_spawn(resume_from: Option<SegmentId>) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let ready = match spawn::run(resume_from).await? {
|
||||
SpawnOutcome::Ready(r) => r,
|
||||
|
|
@ -396,16 +473,34 @@ async fn run_spawn(resume_from: Option<SegmentId>) -> Result<(), Box<dyn std::er
|
|||
result
|
||||
}
|
||||
|
||||
fn enter_fullscreen() -> Result<Terminal<CrosstermBackend<io::Stdout>>, Box<dyn std::error::Error>>
|
||||
{
|
||||
fn enter_fullscreen() -> Result<FullscreenTerminal, Box<dyn std::error::Error>> {
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
Ok(Terminal::new(backend)?)
|
||||
}
|
||||
|
||||
fn enter_fullscreen_existing(
|
||||
terminal: &mut FullscreenTerminal,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
EnterAlternateScreen,
|
||||
EnableMouseCapture
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn leave_fullscreen(terminal: &mut FullscreenTerminal) -> io::Result<()> {
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
DisableMouseCapture,
|
||||
LeaveAlternateScreen
|
||||
)
|
||||
}
|
||||
|
||||
async fn run(
|
||||
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
|
||||
terminal: &mut FullscreenTerminal,
|
||||
pod_name: String,
|
||||
socket_path: &std::path::Path,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
|
@ -438,7 +533,7 @@ const POD_EVENT_DRAIN_LIMIT: usize = 32;
|
|||
|
||||
struct TerminalEventReader {
|
||||
stop: Arc<AtomicBool>,
|
||||
_thread: thread::JoinHandle<()>,
|
||||
thread: Option<thread::JoinHandle<()>>,
|
||||
}
|
||||
|
||||
impl TerminalEventReader {
|
||||
|
|
@ -453,7 +548,7 @@ impl TerminalEventReader {
|
|||
Ok((
|
||||
Self {
|
||||
stop,
|
||||
_thread: thread,
|
||||
thread: Some(thread),
|
||||
},
|
||||
rx,
|
||||
))
|
||||
|
|
@ -463,6 +558,9 @@ impl TerminalEventReader {
|
|||
impl Drop for TerminalEventReader {
|
||||
fn drop(&mut self) {
|
||||
self.stop.store(true, Ordering::Relaxed);
|
||||
if let Some(thread) = self.thread.take() {
|
||||
let _ = thread.join();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -62,37 +62,41 @@ impl From<session_store::StoreError> for MultiPodError {
|
|||
|
||||
pub(crate) enum MultiPodOutcome {
|
||||
Quit,
|
||||
Open {
|
||||
pod_name: String,
|
||||
socket_override: Option<PathBuf>,
|
||||
},
|
||||
Open(OpenPodRequest),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct OpenPodRequest {
|
||||
pub(crate) pod_name: String,
|
||||
pub(crate) socket_override: Option<PathBuf>,
|
||||
}
|
||||
|
||||
pub(crate) async fn load_app() -> Result<MultiPodApp, MultiPodError> {
|
||||
MultiPodApp::load(None).await
|
||||
}
|
||||
|
||||
pub(crate) async fn run(
|
||||
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
|
||||
app: &mut MultiPodApp,
|
||||
) -> Result<MultiPodOutcome, MultiPodError> {
|
||||
let mut app = MultiPodApp::load(None).await?;
|
||||
if app.list.entries.is_empty() {
|
||||
return Err(MultiPodError::NoPods);
|
||||
}
|
||||
|
||||
loop {
|
||||
terminal.draw(|f| draw(f, &mut app))?;
|
||||
terminal.draw(|f| draw(f, app))?;
|
||||
match read()? {
|
||||
TermEvent::Key(key) => match app.handle_key(key) {
|
||||
MultiPodAction::None => {}
|
||||
MultiPodAction::Quit => return Ok(MultiPodOutcome::Quit),
|
||||
MultiPodAction::Open => {
|
||||
if let Some(entry) = app.list.selected_entry() {
|
||||
return Ok(MultiPodOutcome::Open {
|
||||
pod_name: entry.name.clone(),
|
||||
socket_override: entry.attach_socket_path().map(PathBuf::from),
|
||||
});
|
||||
if let Some(request) = app.prepare_open() {
|
||||
return Ok(MultiPodOutcome::Open(request));
|
||||
}
|
||||
}
|
||||
MultiPodAction::Refresh => app.reload().await?,
|
||||
MultiPodAction::Send(request) => {
|
||||
terminal.draw(|f| draw(f, &mut app))?;
|
||||
terminal.draw(|f| draw(f, app))?;
|
||||
let result = send_run_and_confirm(&request.socket_path, request.segments).await;
|
||||
app.finish_send(result);
|
||||
let _ = app.reload().await;
|
||||
|
|
@ -147,7 +151,7 @@ impl MultiPodApp {
|
|||
Ok(app)
|
||||
}
|
||||
|
||||
async fn reload(&mut self) -> Result<(), MultiPodError> {
|
||||
pub(crate) async fn reload(&mut self) -> Result<(), MultiPodError> {
|
||||
self.list = load_pod_list(self.list.selected_name.clone()).await?;
|
||||
self.ensure_selection_visible();
|
||||
Ok(())
|
||||
|
|
@ -211,6 +215,40 @@ impl MultiPodApp {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn prepare_open(&mut self) -> Option<OpenPodRequest> {
|
||||
let entry = match self.list.selected_entry() {
|
||||
Some(entry) => entry,
|
||||
None => {
|
||||
self.notice = Some("No Pod is selected.".to_string());
|
||||
return None;
|
||||
}
|
||||
};
|
||||
if !entry.actions.can_open {
|
||||
self.notice = Some("Selected Pod cannot be opened from this view.".to_string());
|
||||
return None;
|
||||
}
|
||||
self.notice = Some(format!("Opening {}…", entry.name));
|
||||
Some(OpenPodRequest {
|
||||
pod_name: entry.name.clone(),
|
||||
socket_override: entry.attach_socket_path().map(PathBuf::from),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn finish_open(
|
||||
&mut self,
|
||||
pod_name: &str,
|
||||
result: Result<(), &dyn std::fmt::Display>,
|
||||
) {
|
||||
match result {
|
||||
Ok(()) => {
|
||||
self.notice = Some(format!("Returned from {pod_name}."));
|
||||
}
|
||||
Err(error) => {
|
||||
self.notice = Some(format!("Open failed for {pod_name}: {error}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn prepare_send(&mut self) -> Option<DirectSendRequest> {
|
||||
let entry = match self.list.selected_entry() {
|
||||
Some(entry) => entry,
|
||||
|
|
@ -1114,6 +1152,53 @@ mod tests {
|
|||
assert!(app.notice.as_deref().unwrap().contains("Delivered"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_open_request_keeps_dashboard_state_for_nested_single_pod() {
|
||||
let mut app = test_app(vec![live_info("alpha", PodStatus::Idle)]);
|
||||
app.input.insert_str("draft survives open");
|
||||
|
||||
let request = app.prepare_open().unwrap();
|
||||
|
||||
assert_eq!(request.pod_name, "alpha");
|
||||
assert_eq!(
|
||||
request.socket_override,
|
||||
Some(PathBuf::from("/tmp/alpha.sock"))
|
||||
);
|
||||
assert_eq!(app.list.selected_entry().unwrap().name, "alpha");
|
||||
assert_eq!(input_text(&app), "draft survives open");
|
||||
assert!(app.notice.as_deref().unwrap().contains("Opening alpha"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_open_failure_keeps_composer_and_sets_notice() {
|
||||
let mut app = test_app(vec![live_info("alpha", PodStatus::Idle)]);
|
||||
app.input.insert_str("keep this draft");
|
||||
let before = input_text(&app);
|
||||
let error = io::Error::other("boom");
|
||||
|
||||
app.finish_open("alpha", Err(&error));
|
||||
|
||||
assert_eq!(input_text(&app), before);
|
||||
assert_eq!(app.list.selected_entry().unwrap().name, "alpha");
|
||||
assert!(
|
||||
app.notice
|
||||
.as_deref()
|
||||
.unwrap()
|
||||
.contains("Open failed for alpha")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_open_disabled_target_stays_in_dashboard() {
|
||||
let mut live = live_info("unreachable", PodStatus::Idle);
|
||||
live.reachable = false;
|
||||
live.status = None;
|
||||
let mut app = test_app(vec![live]);
|
||||
|
||||
assert!(app.prepare_open().is_none());
|
||||
assert!(app.notice.as_deref().unwrap().contains("cannot be opened"));
|
||||
}
|
||||
|
||||
fn test_app(live: Vec<LivePodInfo>) -> MultiPodApp {
|
||||
app_with_list(PodList::from_sources(
|
||||
PodVisibilitySource::ResumePicker,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user