use crate::application::services::InterpolationService;
use crate::domain::action::BookmarkAction;
use crate::domain::bookmark::Bookmark;
use crate::domain::error::{DomainError, DomainResult};
use crossterm::style::Stylize;
use std::sync::Arc;
use tracing::{debug, instrument};
#[derive(Debug)]
pub struct UriAction {
interpolation_service: Arc<dyn InterpolationService>,
}
impl UriAction {
pub fn new(interpolation_service: Arc<dyn InterpolationService>) -> Self {
Self {
interpolation_service,
}
}
#[instrument(skip(self), level = "debug")]
fn execute_custom_opener(&self, opener: &str, url: &str) -> DomainResult<()> {
let expanded_opener = crate::util::path::expand_path(opener);
debug!("Custom opener: {} -> {}", opener, expanded_opener);
debug!("URL argument: {}", url);
let mut child = std::process::Command::new("sh")
.arg("-c")
.arg(format!("{} \"$1\"", expanded_opener))
.arg("--")
.arg(url)
.stdin(std::process::Stdio::inherit())
.stdout(std::process::Stdio::inherit())
.stderr(std::process::Stdio::inherit())
.spawn()
.map_err(|e| {
DomainError::Other(format!(
"Failed to execute custom opener '{}': {}",
expanded_opener, e
))
})?;
let status = child
.wait()
.map_err(|e| DomainError::Other(format!("Failed to wait on custom opener: {}", e)))?;
if !status.success() {
return Err(DomainError::Other(format!(
"Custom opener '{}' exited with status: {}",
expanded_opener,
status.code().unwrap_or(-1)
)));
}
debug!("Custom opener completed successfully");
Ok(())
}
#[instrument(skip(self, url), level = "debug")]
fn open_url(&self, url: &str) -> DomainResult<()> {
debug!("Opening URL: {}", url);
if url.starts_with("shell::") {
let cmd = url.replace("shell::", "");
eprintln!("Executing shell command: {}", cmd);
eprintln!(
"{}",
"'shell::' is deprecated. Use SystemTag '_shell_' instead.".yellow()
);
let mut child = std::process::Command::new("sh")
.arg("-c")
.arg(cmd)
.stdin(std::process::Stdio::inherit())
.stdout(std::process::Stdio::inherit())
.stderr(std::process::Stdio::inherit())
.spawn()
.map_err(|e| {
DomainError::Other(format!("Failed to execute shell command: {}", e))
})?;
let status = child
.wait()
.map_err(|e| DomainError::Other(format!("Failed to wait on command: {}", e)))?;
debug!("Shell command exit status: {:?}", status);
return Ok(());
}
if let Some(path) = crate::util::path::abspath(url) {
debug!("Resolved path: {}", path);
if path.ends_with(".md") {
debug!("Opening markdown file with editor: {}", path);
let editor = std::env::var("EDITOR").unwrap_or_else(|_| "vim".to_string());
debug!("Using editor: {}", editor);
std::process::Command::new(editor)
.arg(&path)
.status()
.map_err(|e| {
DomainError::Other(format!("Failed to open with editor: {}", e))
})?;
} else {
debug!("Opening file with default OS application: {}", path);
open::that(&path)
.map_err(|e| DomainError::Other(format!("Failed to open file: {}", e)))?;
}
} else {
debug!("Opening URL with default OS command: {}", url);
open::that(url)
.map_err(|e| DomainError::Other(format!("Failed to open URL: {}", e)))?;
}
Ok(())
}
}
impl BookmarkAction for UriAction {
#[instrument(skip(self, bookmark), level = "debug")]
fn execute(&self, bookmark: &Bookmark) -> DomainResult<()> {
let rendered_url = self
.interpolation_service
.render_bookmark_url(bookmark)
.map_err(|e| DomainError::Other(format!("Failed to render URL: {}", e)))?;
if let Some(opener) = &bookmark.opener {
return self.execute_custom_opener(opener, &rendered_url);
}
self.open_url(&rendered_url)
}
fn description(&self) -> &'static str {
"Open in browser or application"
}
}