aboutsummaryrefslogtreecommitdiff
path: root/src/main.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/main.rs')
-rw-r--r--src/main.rs360
1 files changed, 360 insertions, 0 deletions
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..91eba62
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,360 @@
+use std::ffi::OsString;
+use std::io::Read;
+use std::io::Seek;
+use std::io::Write;
+use std::os::unix::prelude::OsStrExt;
+use std::path::PathBuf;
+use std::process::ExitCode;
+
+use clap::{Parser, Subcommand};
+
+mod shared;
+use shared::{Project, DbCtx};
+
+#[derive(Parser)]
+#[command(version, about, long_about = None)] // , multicall(true))]
+struct Args {
+ #[arg(long)]
+ db: Option<PathBuf>,
+
+ #[command(subcommand)]
+ command: Command,
+}
+
+#[derive(Subcommand)]
+enum Command {
+ /// initialize a qroject database. you should run this first, and shouldn't need to again.
+ Init,
+
+ /// add a project to the qroject database. projects are unique by their name.
+ Add {
+ /// the name of the project.
+ project: String,
+ /// the directory at which the project lives.
+ dir: OsString,
+ /// if the project is on some other computer, the hostname or address of that computer.
+ ///
+ /// with this set, `qg <proj>` will ssh to that computer and switch into that directory.
+ #[arg(long)]
+ host: Option<String>,
+ /// misc additional information about the project. not used by qroject itself.
+ info: Option<String>,
+ /// where the project can be found on the wider internet, if anywhere.
+ ///
+ /// this is mostly useful as notes for homepages, git remotes, etc.
+ upstream: Option<String>,
+ },
+
+ /// forget a project of the given name.
+ ///
+ /// this deletes the database row. this does not make changes to any other files. still, be
+ /// careful.
+ Forget {
+ project: String,
+ },
+
+ /// print the directory for the given project.
+ ///
+ /// this is mostly useful as, for example, `$(q dir foo)` as a stand-in for the actual
+ /// directory of foo.
+ Dir {
+ project: String,
+ },
+
+ /// print information about the given project.
+ Info {
+ project: String,
+ },
+
+ /// print the project's upstream, if any.
+ Upstream {
+ project: String,
+ },
+
+ /// edit `project`'s record.
+ ///
+ /// this opens the project's information in vim (not $EDITOR yet sorry). edits to the project
+ /// are written back to the database (purusant to the unique name restriction). optional fields
+ /// are omitted when null, but can be set by adding a corresponding line in the file.
+ Edit {
+ project: String,
+ },
+
+ /// list all known projects (optionally, with details)
+ List {
+ #[arg(short, default_value_t = false)]
+ verbose: bool,
+ },
+}
+
+enum CliResult {
+ Ok,
+ /// a project-specifying command selected a project that doesn't exist
+ NotAProject,
+ /// a project-specifying command wanted a field that was None
+ NoField,
+ /// a command that involved disk i/o failed to either i or o.
+ IoError,
+ /// something happened related to the database
+ DbError,
+}
+
+impl std::process::Termination for CliResult {
+ fn report(self) -> ExitCode {
+ match self {
+ CliResult::Ok => ExitCode::SUCCESS,
+ CliResult::NotAProject => ExitCode::from(1),
+ CliResult::NoField => ExitCode::from(2),
+ CliResult::IoError => ExitCode::from(3),
+ CliResult::DbError => ExitCode::from(4),
+ }
+ }
+}
+
+unsafe extern "C" {
+ // from libc. no rustix binding..
+ fn mkstemp(template: *mut std::ffi::c_char) -> std::ffi::c_int;
+ fn perror(msg: *const std::ffi::c_char);
+}
+
+fn print_project_name(p: Project) {
+ println!("{}", p.name);
+}
+
+fn print_project(p: Project) {
+ println!("{}", p.name);
+ if p.is_here() {
+ println!(" {: <8}: {}", "path", p.dir.display());
+ } else if let Some(host) = p.host.as_ref() {
+ println!(" {: <8}: {}:{}", "path", host, p.dir.display());
+ } else {
+ println!(" {: <8}: {}:{}", "path", "<unknown>", p.dir.display());
+ }
+ if let Some(info) = p.info.as_ref() {
+ println!(" {: <8}: {}", "info", info);
+ }
+ if let Some(upstream) = p.upstream.as_ref() {
+ println!(" {: <8}: {}", "upstream", upstream);
+ }
+}
+
+fn main() -> CliResult {
+ let args = Args::parse();
+
+ let dbpath = args.db.clone().unwrap_or_else(|| {
+ let mut path = std::env::home_dir().expect("have a homedir");
+ path.push(".config");
+ path.push("qroject");
+ path.push("projects.db");
+ path
+ });
+
+ match process(args, dbpath) {
+ Ok(()) => CliResult::Ok,
+ Err(e) => e,
+ }
+}
+
+fn process(args: Args, dbpath: PathBuf) -> Result<(), CliResult> {
+
+ if let Command::Init = args.command {
+ return DbCtx::init(&dbpath).map_err(|e| {
+ eprintln!("{e}");
+ CliResult::IoError
+ });
+ }
+
+ let conn = DbCtx::new(&dbpath).map_err(|e| {
+ eprintln!("{e}");
+ CliResult::IoError
+ })?;
+
+ match args.command {
+ Command::Init => { Ok(()) /* unreachable */ },
+ Command::Add { project, dir, info, upstream, host } => {
+ conn.project_add(
+ Project {
+ id: 0,
+ name: project,
+ dir,
+ info,
+ upstream,
+ host,
+ }
+ ).map_err(|e| {
+ eprintln!("could not add project: {e}");
+ CliResult::DbError
+ })
+ },
+
+ Command::Forget { project } => {
+ conn.project_forget(
+ &project,
+ ).map_err(|e| {
+ eprintln!("could not forget project: {e}");
+ CliResult::DbError
+ })
+ }
+
+ Command::Dir { project } => {
+ let proj = conn.project_by_name(&project).map_err(|e| {
+ eprintln!("could not look up project: {e}");
+ CliResult::DbError
+ })?;
+ let proj = proj.ok_or(CliResult::NotAProject)?;
+
+ println!("{}", proj.dir.display());
+ Ok(())
+ }
+
+ Command::Info { project } => {
+ let proj = conn.project_by_name(&project).map_err(|e| {
+ eprintln!("could not look up project: {e}");
+ CliResult::DbError
+ })?;
+ let proj = proj.ok_or(CliResult::NotAProject)?;
+
+ print_project(proj);
+ Ok(())
+ }
+
+ Command::Upstream { project } => {
+ let proj = conn.project_by_name(&project).map_err(|e| {
+ eprintln!("could not look up project: {e}");
+ CliResult::DbError
+ })?;
+ let proj = proj.ok_or(CliResult::NotAProject)?;
+ let upstream = proj.upstream.as_ref().ok_or(CliResult::NoField)?;
+
+ println!("{}", upstream);
+ Ok(())
+ }
+
+ Command::Edit { project } => {
+ let proj = conn.project_by_name(&project).map_err(|e| {
+ eprintln!("could not look up project: {e}");
+ CliResult::DbError
+ })?;
+ let mut p = proj.ok_or(CliResult::NotAProject)?;
+
+ let mut existing = String::new();
+ use std::fmt::{Write as FmtWrite};
+ use std::os::fd::FromRawFd;
+ writeln!(existing, "name:{}", p.name)
+ .expect("can write to a string...");
+ writeln!(existing, "dir:{}", p.dir.display())
+ .expect("can write to a string...");
+ if let Some(host) = p.host.as_ref() {
+ writeln!(existing, "host:{}", host)
+ .expect("can write to a string...");
+ }
+ if let Some(info) = p.info.as_ref() {
+ writeln!(existing, "info:{}", info)
+ .expect("can write to a string...");
+ }
+ if let Some(upstream) = p.upstream.as_ref() {
+ writeln!(existing, "upstream:{}", upstream)
+ .expect("can write to a string...");
+ }
+
+ let mut template: [u8; 18] = *b"/tmp/qrojectXXXXXX";
+
+ let tmpfd = unsafe { mkstemp(template.as_mut_ptr() as *mut std::ffi::c_char) };
+
+ if tmpfd == -1 {
+ unsafe { perror(b"mkstemp\0".as_ptr() as *const _); }
+ return Err(CliResult::IoError);
+ }
+
+ let mut file = unsafe { std::fs::File::from_raw_fd(tmpfd) };
+
+ let res = file.write_all(existing.as_bytes());
+ if let Err(e) = res {
+ eprintln!("could not write: {:?}", e);
+ return Err(CliResult::IoError);
+ }
+
+ let res = match std::process::Command::new("vim")
+ .arg(str::from_utf8(&template[..]).expect("valid"))
+ .status() {
+ Ok(res) => res,
+ Err(e) => {
+ eprintln!("failed to exec editor: {:?}", e);
+ return Err(CliResult::IoError);
+ }
+ };
+
+ if !res.success() {
+ return Err(CliResult::IoError);
+ }
+
+ let mut edited = String::new();
+ if let Err(_e) = file.rewind() {
+ return Err(CliResult::IoError);
+ }
+ if let Err(_e) = file.read_to_string(&mut edited) {
+ return Err(CliResult::IoError);
+ }
+
+ let lines = edited.split("\n");
+
+ let mut new_name = None;
+ let mut new_dir = None;
+ p.host = None;
+ p.info = None;
+ p.upstream = None;
+
+ for line in lines {
+ if line.trim().is_empty() {
+ continue;
+ }
+ let Some((field, value)) = line.split_once(":") else {
+ eprintln!("bogus line: {line}");
+ return Err(CliResult::IoError);
+ };
+ let value = value.to_owned();
+
+ if field == "name" {
+ new_name = Some(value);
+ } else if field == "dir" {
+ new_dir = Some(value);
+ } else if field == "host" {
+ p.host = Some(value);
+ } else if field == "info" {
+ p.info = Some(value);
+ } else if field == "upstream" {
+ p.upstream = Some(value);
+ }
+ }
+
+ let (Some(name), Some(dir)) = (new_name, new_dir) else {
+ eprintln!("missing name or dir");
+ return Err(CliResult::IoError);
+ };
+
+ p.name = name;
+ let dir: &[u8] = dir.as_bytes();
+ let dir: &std::ffi::OsStr = OsStrExt::from_bytes(dir);
+ p.dir = dir.to_os_string();
+
+ conn.project_update(&p)
+ .map_err(|e| {
+ eprintln!("failed to update project: {}", e);
+ CliResult::IoError
+ })
+ }
+
+ Command::List { verbose: details } => {
+ let op = if details {
+ print_project
+ } else {
+ print_project_name
+ };
+
+ conn.for_each_project(op).map_err(|e| {
+ eprintln!("could iterate projects: {e}");
+ CliResult::DbError
+ })
+ }
+ }
+}