Skip to content

Commit

Permalink
feat(CLI): Add close command
Browse files Browse the repository at this point in the history
  • Loading branch information
nokome committed Jun 13, 2021
1 parent b6e6819 commit 1aa4365
Show file tree
Hide file tree
Showing 3 changed files with 90 additions and 50 deletions.
65 changes: 51 additions & 14 deletions cli/main.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#![recursion_limit = "256"]

use std::{collections::HashMap, env, path::Path};
use std::{collections::HashMap, env, path::PathBuf};
use stencila::{
cli::display,
config, documents,
Expand Down Expand Up @@ -74,8 +74,8 @@ pub enum Command {
// or the `documents` module (depending upon if path is a folder or file),
// or combine results from both in the case of `List`.
List(ListCommand),
//Open(OpenCommand),
//Close(CloseCommand),
Open(OpenCommand),
Close(CloseCommand),
//Show(ShowCommand),
#[structopt(aliases = &["project"])]
Projects(projects::cli::Command),
Expand Down Expand Up @@ -104,8 +104,8 @@ pub async fn run_command(
) -> Result<()> {
let result = match command {
Command::List(command) => command.run(projects, documents).await,
//Command::Open(command) => command.run(projects, documents, config).await,
//Command::Close(command) => command.run(projects, documents, config).await,
Command::Open(command) => command.run(projects, documents, config).await,
Command::Close(command) => command.run(projects, documents).await,
//Command::Show(command) => command.run(projects, documents, config).await,
Command::Documents(command) => command.run(documents).await,
Command::Projects(command) => command.run(projects, &config.projects),
Expand Down Expand Up @@ -137,11 +137,11 @@ impl ListCommand {
}
}

/// Open a project or document using Stencila Desktop or a web browser
/// Open a project or document using a web browser
///
/// If the path is a directory, then Stencila will attempt to
/// open it's main document. If the path a file, then Stencila
/// will open it as an orphan document (i.e. not associated with any project).
/// If the path a file, it will be opened as a document.
/// If the path a folder, it will be opened as a project and it's main file
/// (if any) opened.
///
/// In the future, this command will open the project/document
/// in the Stencila Desktop if that is available.
Expand All @@ -151,9 +151,9 @@ impl ListCommand {
setting = structopt::clap::AppSettings::ColoredHelp,
)]
pub struct OpenCommand {
/// The file or directory to open
/// The file or folder to open
#[structopt(default_value = ".")]
path: String,
path: PathBuf,
}

impl OpenCommand {
Expand All @@ -162,10 +162,10 @@ impl OpenCommand {
projects: &mut projects::Projects,
documents: &mut documents::Documents,
config: &config::Config,
) -> Result<()> {
) -> display::Result {
let Self { path } = self;

let doc_path = if Path::new(&path.clone()).is_dir() {
let doc_path = if path.is_dir() {
let project = projects.open(&path, &config.projects, true)?;
project.main_path
} else {
Expand All @@ -190,7 +190,44 @@ impl OpenCommand {

// Open browser at the login page and start serving
webbrowser::open(login_url.as_str())?;
serve::serve(documents, None, Some(key)).await
serve::serve(documents, None, Some(key)).await?;

display::nothing()
}
}

/// Close a project or document
///
/// If the path a file, the associated open document (if any) will be closed.
/// If the path a folder, the associated project (if any) will be closed.
/// Closing a document or project just means that it is unloaded from memory
/// and the file or folder is not longer watched for changes.
#[derive(Debug, StructOpt)]
#[structopt(
setting = structopt::clap::AppSettings::NoBinaryName,
setting = structopt::clap::AppSettings::ColoredHelp,
)]
pub struct CloseCommand {
/// The file or folder to close
#[structopt(default_value = ".")]
path: PathBuf,
}

impl CloseCommand {
pub async fn run(
self,
projects: &mut projects::Projects,
documents: &mut documents::Documents,
) -> display::Result {
let Self { path } = self;

if path.is_dir() {
projects.close(&path)?;
} else {
documents.close(&path).await?;
}

display::nothing()
}
}

Expand Down
8 changes: 6 additions & 2 deletions rust/src/files.rs
Original file line number Diff line number Diff line change
Expand Up @@ -588,8 +588,12 @@ pub struct Files {

impl Files {
/// Load files from a folder
pub fn load(folder: &str, watch: bool, watch_exclude_patterns: Vec<String>) -> Result<Files> {
let path = Path::new(folder).canonicalize()?;
pub fn load<P: AsRef<Path>>(
folder: P,
watch: bool,
watch_exclude_patterns: Vec<String>,
) -> Result<Files> {
let path = folder.as_ref().canonicalize()?;

// Create a registry of the files
let registry = Arc::new(Mutex::new(FileRegistry::new(&path)));
Expand Down
67 changes: 33 additions & 34 deletions rust/src/projects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,35 +92,37 @@ impl Project {
const FILE_NAME: &'static str = "project.json";

/// Get the path to a projects' manifest file
fn file(folder: &str) -> PathBuf {
Path::new(folder).join(Project::FILE_NAME)
fn file<P: AsRef<Path>>(folder: P) -> PathBuf {
folder.as_ref().join(Project::FILE_NAME)
}

/// Read a project's manifest file
///
/// If there is no manifest file, then return a default project
fn read(folder: &str) -> Result<Project> {
if !Path::new(folder).exists() {
bail!("Project folder does not exist: {}", folder)
fn read<P: AsRef<Path>>(folder: P) -> Result<Project> {
let folder = folder.as_ref().canonicalize()?;

if !folder.exists() {
bail!("Project folder does not exist: {}", folder.display())
}

let file = Project::file(folder);
let file = Project::file(&folder);
let mut project = if file.exists() {
let json = fs::read_to_string(file)?;
serde_json::from_str(&json)?
} else {
Project::default()
};
project.path = Path::new(folder).canonicalize()?;
project.path = folder;

Ok(project)
}

/// Write a project's manifest file
///
/// If the project folder does not exist yet then it will be created
pub fn write(folder: &str, project: &Project) -> Result<()> {
fs::create_dir_all(folder)?;
pub fn write<P: AsRef<Path>>(folder: P, project: &Project) -> Result<()> {
fs::create_dir_all(&folder)?;

let file = Project::file(folder);
let json = serde_json::to_string_pretty(project)?;
Expand All @@ -133,8 +135,8 @@ impl Project {
///
/// If the project has already been initialized (i.e. has a manifest file)
/// then this function will do nothing.
pub fn init(folder: &str) -> Result<()> {
if Project::file(folder).exists() {
pub fn init<P: AsRef<Path>>(folder: P) -> Result<()> {
if Project::file(&folder).exists() {
return Ok(());
}

Expand All @@ -146,7 +148,12 @@ impl Project {

/// Load a project including creating default values for properties
/// where necessary
pub fn open(folder: &str, config: &config::ProjectsConfig, watch: bool) -> Result<Project> {
pub fn open<P: AsRef<Path>>(
folder: P,
config: &config::ProjectsConfig,
watch: bool,
) -> Result<Project> {
let folder = folder.as_ref();
let mut project = Project::read(folder)?;

// Watch exclude patterns default to the configured defaults
Expand All @@ -163,12 +170,10 @@ impl Project {
project.main_path = project.resolve_main_path(&config.main_patterns);

// Name defaults to the name of the folder
project.name = project
.name
.or_else(|| match Path::new(folder).components().last() {
Some(last) => Some(last.as_os_str().to_string_lossy().to_string()),
None => Some("Unnamed".to_string()),
});
project.name = project.name.or_else(|| match folder.components().last() {
Some(last) => Some(last.as_os_str().to_string_lossy().to_string()),
None => Some("Unnamed".to_string()),
});

// Theme defaults to the configured default
project.theme = project.theme.or_else(|| Some(config.theme.clone()));
Expand Down Expand Up @@ -249,19 +254,14 @@ impl Project {
#[serde(default)]
pub struct Projects {
/// The projects, stored by absolute path
pub registry: HashMap<String, Project>,
pub registry: HashMap<PathBuf, Project>,
}

impl Projects {
pub fn new() -> Self {
Self::default()
}

/// Get the canonical absolute path of a project folder
fn path(folder: &str) -> Result<String> {
Ok(Path::new(folder).canonicalize()?.display().to_string())
}

/// List documents that are currently open
///
/// Returns a vector of document paths (relative to the current working directory)
Expand All @@ -285,14 +285,13 @@ impl Projects {
/// This function `loads` a project, stores it, optionally watches the project folder,
/// updates the project on changes and publishes the updates on the "project"
/// pubsub topic channel.
pub fn open(
pub fn open<P: AsRef<Path>>(
&mut self,
folder: &str,
folder: P,
config: &config::ProjectsConfig,
watch: bool,
) -> Result<Project> {
let path = Projects::path(folder)?;

let path = folder.as_ref().canonicalize()?;
let project = match self.registry.entry(path) {
Entry::Occupied(entry) => entry.into_mut(),
Entry::Vacant(entry) => match Project::open(folder, config, watch) {
Expand All @@ -304,8 +303,8 @@ impl Projects {
}

/// Close a project
pub fn close(&mut self, folder: &str) -> Result<()> {
let path = Projects::path(folder)?;
pub fn close<P: AsRef<Path>>(&mut self, folder: P) -> Result<()> {
let path = folder.as_ref().canonicalize()?;
self.registry.remove(&path);
Ok(())
}
Expand Down Expand Up @@ -420,7 +419,7 @@ pub mod cli {
/// If no `project.json` file exists in the folder then a new one
/// will be created.
#[structopt(default_value = ".")]
pub folder: String,
pub folder: PathBuf,
}

impl Init {
Expand Down Expand Up @@ -454,7 +453,7 @@ pub mod cli {
pub struct Open {
/// The path of the project folder
#[structopt(default_value = ".")]
pub folder: String,
pub folder: PathBuf,
}

impl Open {
Expand All @@ -478,7 +477,7 @@ pub mod cli {
pub struct Close {
/// The path of the project folder
#[structopt(default_value = ".")]
pub folder: String,
pub folder: PathBuf,
}

impl Close {
Expand All @@ -498,7 +497,7 @@ pub mod cli {
pub struct Show {
/// The path of the project folder
#[structopt(default_value = ".")]
pub folder: String,
pub folder: PathBuf,
}

impl Show {
Expand Down

0 comments on commit 1aa4365

Please sign in to comment.