chiark / gitweb /
Move adhoc.rs out of otter.rs
authorIan Jackson <ijackson@chiark.greenend.org.uk>
Wed, 2 Jun 2021 23:04:02 +0000 (00:04 +0100)
committerIan Jackson <ijackson@chiark.greenend.org.uk>
Wed, 2 Jun 2021 23:04:19 +0000 (00:04 +0100)
Signed-off-by: Ian Jackson <ijackson@chiark.greenend.org.uk>
cli/adhoc.rs [new file with mode: 0644]
cli/otter.rs
cli/uselibs.rs [new file with mode: 0644]

diff --git a/cli/adhoc.rs b/cli/adhoc.rs
new file mode 100644 (file)
index 0000000..38e7e99
--- /dev/null
@@ -0,0 +1,149 @@
+// Copyright 2020-2021 Ian Jackson and contributors to Otter
+// SPDX-License-Identifier: AGPL-3.0-or-later
+// There is NO WARRANTY.
+
+use super::*;
+
+//---------- adhoc json/ron etc. ----------
+
+#[derive(Debug,Copy,Clone)]
+struct AdhocFormat(&'static str);
+
+impl From<&'static Subcommand> for AdhocFormat {
+  fn from(sc: &'static Subcommand) -> Self { AdhocFormat(
+    sc.verb.rsplitn(2,'-').next().unwrap()
+  )}
+}
+
+impl AdhocFormat {
+  pub fn metavar(&self) -> String { self.0.to_uppercase() }
+  pub fn name(&self) -> String { match self.0 {
+    // special cases go here
+    _ => return self.0.to_uppercase(),
+  }/*.into()*/ }
+
+  #[throws(AE)]
+  pub fn parse<T>(&self, input: Vec<String>, what: &str) -> Vec<T>
+  where T: DeserializeOwned
+  {
+    input.into_iter().enumerate().map(|(i,s)| match self.0 {
+      "json" => serde_json::from_str(&s).map_err(AE::from),
+      "ron"  => ron::de::   from_str(&s).map_err(AE::from),
+      _ => panic!(),
+    }
+        .with_context(|| s.clone())
+        .with_context(|| format!("parse {} (#{})", what, i))
+    ).collect::<Result<Vec<T>,AE>>()?
+  }
+
+  #[throws(AE)]
+  pub fn report<T>(&self, out: &mut CookedStdout, resp: T)
+  where T: Serialize
+  {
+    writeln!(out, "{}", match self.0 {
+      "json" => serde_json::to_string(&resp).map_err(AE::from),
+      "ron"  => ron::ser::  to_string(&resp).map_err(AE::from),
+      _ => panic!(),
+    }
+        .context("re-format response")?)?;
+  }
+}
+
+//---------- adhoc command ----------
+
+mod command_adhoc {
+  use super::*;
+
+  #[derive(Default,Debug)]
+  struct Args {
+    cmds: Vec<String>,
+  }
+
+  fn subargs<'ap,'a:'ap,'m:'ap>(
+    sa: &'a mut Args,
+    ahf: AdhocFormat,
+  ) -> ArgumentParser<'ap> {
+    use argparse::*;
+    let mut ap = ArgumentParser::new();
+    ap.refer(&mut sa.cmds).required()
+      .add_argument(format!("{}-COMMAND", ahf.metavar()).leak(),
+                    Collect,
+                    format!("{}-encoded MgmtCommand", ahf.name())
+                    .leak());
+    ap
+  }
+
+  #[throws(AE)]
+  fn call(SCCA{ mut out, ma, args,.. }:SCCA) {
+    let ahf = ma.sc.into();
+
+    let subargs: ApMaker<_> = &|sa| subargs(sa,ahf);
+    let args = parse_args::<Args,_>(args, subargs, &ok_id, None);
+    let mut conn = connect(&ma)?;
+
+    let cmds: Vec<MgmtCommand> = ahf.parse(args.cmds, "cmd")?;
+    for (i, cmd) in cmds.into_iter().enumerate() {
+      let resp = conn.cmd(&cmd).with_context(|| format!("cmd #{}", i))?;
+      ahf.report(&mut out, resp)?;
+    }
+  }
+
+  inventory_subcmd!{
+    "command-json",
+    "run ad-hoc management command(s) (JSON)",
+  }
+  inventory_subcmd!{
+    "command-ron",
+    "run ad-hoc management command(s) (Rusty Object Notation)",
+  }
+}
+
+//---------- alter game ----------
+
+mod alter_game_adhoc {
+  use super::*;
+
+  #[derive(Default,Debug)]
+  struct Args {
+    insns: Vec<String>,
+  }
+
+  fn subargs<'ap,'a:'ap,'m:'ap>(
+    sa: &'a mut Args,
+    ahf: AdhocFormat,
+  ) -> ArgumentParser<'ap> {
+    use argparse::*;
+    let mut ap = ArgumentParser::new();
+    ap.refer(&mut sa.insns).required()
+      .add_argument(format!("{}-INSN", ahf.metavar()).leak(),
+                    Collect,
+                    format!("{}-encoded MgmtGameInstruction", ahf.name())
+                    .leak());
+    ap
+  }
+
+  fn call(SCCA{ mut out, ma, args,.. }:SCCA) -> Result<(),AE> {
+    let ahf = ma.sc.into();
+
+    let subargs: ApMaker<_> = &|sa| subargs(sa,ahf);
+    let args = parse_args::<Args,_>(args, subargs, &ok_id, None);
+    let mut chan = ma.access_game()?;
+
+    let insns: Vec<MgmtGameInstruction> = ahf.parse(args.insns, "insn")?;
+    let resps = chan.alter_game(insns,None)?;
+    for resp in resps {
+      ahf.report(&mut out, resp)?;
+    }
+
+    Ok(())
+  }
+
+  inventory_subcmd!{
+    "alter-game-json",
+    "run an ad-hoc AlterGame command (JSON)",
+  }
+  inventory_subcmd!{
+    "alter-game-ron",
+    "run an ad-hoc AlterGame command (Rusty Object Notation)",
+  }
+}
index 7b8973a8303a58e75c4fef7945e90f8702e81297..e9c637e237ce0766776287e1f86904e68e2e22a9 100644 (file)
@@ -29,7 +29,9 @@ pub use argparse::action::ParseResult::Parsed;
 pub mod clisupport;
 use clisupport::*;
 
+mod adhoc;
 mod forgame;
+mod uselibs;
 
 #[derive(Debug)]
 enum ServerLocation {
@@ -320,501 +322,6 @@ fn main() {
     .unwrap_or_else(|e| e.end_process(12));
 }
 
-//---------- library-list ----------
-
-#[derive(Debug,Default)]
-struct LibGlobArgs {
-  lib: Option<String>,
-  pat: Option<String>,
-}
-
-impl LibGlobArgs {
-  fn add_arguments<'ap, 'tlg: 'ap>(
-    &'tlg mut self,
-    ap: &'_ mut ArgumentParser<'ap>
-  ) {
-    use argparse::*;
-    ap.refer(&mut self.lib).metavar("LIBRARY")
-      .add_option(&["--lib"],StoreOption,"look only in LIBRARY");
-    ap.refer(&mut self.pat)
-      .add_argument("ITEM-GLOB-PATTERN",StoreOption,"item glob pattern");
-  }
-
-  fn lib(&self) -> Option<String> {
-    self.lib.clone()
-  }
-  fn pat(&self) -> String {
-    self.pat.as_ref().map(Deref::deref)
-      .unwrap_or("*")
-      .into()
-  }
-}
-
-mod library_list {
-  use super::*;
-
-  type Args = LibGlobArgs;
-
-  fn subargs(sa: &mut Args) -> ArgumentParser {
-    use argparse::*;
-    let mut ap = ArgumentParser::new();
-    sa.add_arguments(&mut ap);
-    ap
-  }
-
-  #[throws(AE)]
-  fn call(SCCA{ mut out, ma, args,.. }:SCCA) {
-    let args = parse_args::<Args,_>(args, &subargs, &ok_id, None);
-    let mut chan = ma.access_game()?;
-
-    if args.lib.is_none() && args.pat.is_none() {
-      let game = chan.game.clone();
-      let libs = match chan.cmd(&MC::LibraryListLibraries { game })? {
-        MgmtResponse::Libraries(libs) => libs,
-        x => throw!(anyhow!(
-          "unexpected response to LibrarylistLibraries: {:?}", &x)),
-      };
-      for lib in libs {
-        writeln!(out, "{}", lib)?;
-      }
-      return;
-    }
-
-    let items = chan.list_items(args.lib.clone(), args.pat())?;
-    for it in &items {
-      writeln!(out, "{}", it)?;
-    }
-  }
-
-  inventory_subcmd!{
-    "library-list",
-    "List pieces in the shape libraries",
-  }
-}
-
-//---------- library-sdd ----------
-
-mod library_add {
-  use super::*;
-
-  #[derive(Default,Debug)]
-  struct Args {
-    tlg: LibGlobArgs,
-    adjust_markers: Option<bool>,
-    incremental: bool,
-  }
-
-  impl Args {
-    fn adjust_markers(&self) -> bool { self.adjust_markers.unwrap_or(true) }
-  }
-
-  fn subargs(sa: &mut Args) -> ArgumentParser {
-    use argparse::*;
-    let mut ap = ArgumentParser::new();
-    sa.tlg.add_arguments(&mut ap);
-    ap.refer(&mut sa.adjust_markers)
-      .add_option(&["--no-adjust-markers"],StoreConst(Some(false)),
-                  "do not adjust the number of insertion markers, just fail")
-      .add_option(&["--adjust-markers"],StoreConst(Some(true)),"");
-    ap.refer(&mut sa.incremental)
-      .add_option(&["--incremental"],StoreConst(true),
-                  "do not place pieces already on the board; \
-                   if they don't all fit, place as many as possible")
-      .add_option(&["--no-incremental"],StoreConst(false),"");
-    ap
-  }
-
-  fn call(SCCA{ mut out, ma, args,.. }:SCCA) -> Result<(),AE> {
-    const MAGIC: &str = "mgmt-library-load-marker";
-
-    let args = parse_args::<Args,_>(args, &subargs, &ok_id, None);
-    let mut chan = ma.access_game()?;
-    let (pieces, _pcaliases) = chan.list_pieces()?;
-    let markers = pieces.iter().filter(|p| p.itemname.as_str() == MAGIC)
-      .collect::<Vec<_>>();
-
-    let already = if args.incremental { Some(
-      pieces.iter().map(|p| &p.itemname)
-        .collect::<HashSet<_>>()
-    )} else {
-      None
-    };
-
-    if ma.verbose > 2 { dbgc!(&markers, &args, &already); }
-
-    #[derive(Debug)]
-    enum Situation {
-      Poor(Vec<MGI>, &'static str),
-      Good([Pos; 2]),
-    }
-    use Situation::*;
-
-    const WANTED: usize = 2;
-    let situation = if markers.len() < WANTED {
-      let to_add = WANTED - markers.len();
-      let spec = ItemSpec {
-        lib: "wikimedia".to_string(), // todo: make an argument
-        item: MAGIC.to_string(),
-      };
-      let spec = PiecesSpec {
-        pos: None,
-        posd: None,
-        count: Some(to_add as u32),
-        face: None,
-        pinned: Some(false),
-        angle: default(),
-        info: Box::new(spec),
-      };
-      Poor(vec![ MGI::AddPieces(spec) ],
-           "marker(s) created")
-    } else if markers.len() > WANTED {
-      let insns = markers[WANTED..].iter()
-        .map(|p| MGI::DeletePiece(p.piece))
-        .collect();
-      Poor(insns,
-           "surplus marker(s) removed")
-    } else {
-      let mut good: ArrayVec<_,2> = default();
-      for p in &markers {
-        good.push(p.visible.as_ref().ok_or_else(
-          || anyhow!("library marker(s) with hidden position!")
-        )?.pos);
-      }
-      Good(good.into_inner().unwrap())
-    };
-    if ma.verbose > 2 { dbgc!(&situation); }
-
-    #[derive(Debug)]
-    struct Placement {
-      lhs: Coord, top: Coord, rhs: Coord, bot: Coord,
-      clhs: Coord, cbot: Coord, // current line
-    }
-
-    let mut placement = match situation {
-      Poor(insns, msg) => {
-        if !args.adjust_markers() {
-          throw!(anyhow!("only {} markers, wanted {}",
-                         markers.len(), msg));
-        }
-        chan.alter_game(insns, None)?;
-        eprintln!("updated game: {}\n\
-                   please adjust markers as desired and run again",
-                  msg);
-        exit(EXIT_NOTFOUND);
-      }
-      Good([a, b]) => {
-        // todo: take account of the space used by the markers themselves
-        let lhs = min(a.x(), b.x());
-        let rhs = max(a.x(), b.x());
-        let top = min(a.y(), b.y());
-        let bot = max(a.y(), b.y());
-        Placement {
-          lhs, rhs, top, bot,
-          clhs: lhs, cbot: top,
-        }
-      }
-    };
-    if ma.verbose > 3 { dbgc!(&placement); }
-
-    impl Placement {
-      /// If returns None, has already maybe tried to take some space
-      #[throws(AE)]
-      fn place(&mut self, bbox: &Rect,
-               pieces: &Vec<MgmtGamePieceInfo>, ma: &MainOpts)
-               -> Option<Pos> {
-        let PosC{ coords: [w,h] } = (bbox.br() - bbox.tl())?;
-
-        let mut did_newline = false;
-        let (ncbot, tlhs) = 'search: loop {
-          let ncbot = max(self.cbot, self.top + h);
-          if ncbot > self.bot { return None }
-          let mut any_clash_bot = None;
-
-          'within_line: loop {
-            let tlhs = self.clhs;
-            self.clhs += w;
-            if self.clhs > self.rhs { break 'within_line }
-
-            if let Some((nclhs, clash_bot)) = pieces.iter()
-              .filter_map(|p| (|| if_chain! {
-                if let Some(pv) = p.visible.as_ref();
-                let tl = (pv.pos + pv.bbox.tl())?;
-                let br = (pv.pos + pv.bbox.br())?;
-                if !(tl.x() >= self.clhs
-                    || tl.y() >= ncbot
-                    || br.x() <= tlhs
-                    || br.y() <= self.top);
-                then {
-                  if ma.verbose > 2 {
-                    eprintln!(
-                      "at {:?} tlhs={} ncbot={} avoiding {} tl={:?} br={:?}",
-                      &self, tlhs, ncbot, &p.itemname, &tl, &br
-                    )
-                  }
-                  Ok::<_,AE>(Some((br.x(), br.y())))
-                } else {
-                  Ok::<_,AE>(None)
-                }
-              })().transpose())
-              .next().transpose()?
-            {
-              self.clhs = nclhs;
-              any_clash_bot = Some(clash_bot);
-              continue 'within_line;
-            }
-
-            break 'search (ncbot, tlhs);
-          }
-          // line is full
-          self.top = self.cbot;
-          if did_newline {
-            if let Some(top) = any_clash_bot {
-              self.top = top;
-            } else {
-              // if not, will never fit
-              return None;
-            }
-          }
-          did_newline = true;
-          self.clhs = self.lhs;
-          // if we are simply too wide, we'll just loop until off the bottom
-        };
-        self.cbot = ncbot;
-        let ttopleft = PosC::new(tlhs, self.top);
-        let tnominal = (ttopleft - bbox.tl())?;
-
-        if ma.verbose > 3 { dbgc!(&self, &tnominal); }
-        Some(tnominal)
-      }
-    }
-
-    let mut items = chan.list_items(args.tlg.lib(), args.tlg.pat())?;
-
-    fn k(ied: &ItemEnquiryData) -> (&str, &GoodItemName) { (
-      &ied.lib.libname,
-      &ied.itemname,
-    ) }
-    items.sort_by(|a,b| Ord::cmp( &k(a), &k(b) ));
-    items.reverse();
-    items.dedup_by(|a,b| PartialEq::eq( &k(a), &k(b) ));
-    items.reverse();
-
-    let mut exitcode = 0;
-    let mut insns = vec![];
-    for (ix, it) in items.iter().enumerate() {
-      if ma.verbose > 2 { eprintln!(
-        "item {}  {:?}", &it.itemname, &it.f0bbox
-      )};
-      if let Some(already) = &already {
-        if already.contains(&it.itemname) { continue }
-      }
-      let pos = match placement.place(&items[0].f0bbox, &pieces, &ma)? {
-        Some(pos) => pos,
-        None => {
-          let m = format!("out of space after {} at {}",
-                          &ix, &it.itemname);
-          exitcode = EXIT_SPACE;
-          if args.incremental {
-            writeln!(out, "stopping: {}", &m)?;
-            break;
-          } else {
-            eprintln!("error: {}", &m);
-            exit(exitcode);
-          }
-        }
-      };
-      let spec = ItemSpec::from(it);
-      let spec = PiecesSpec {
-        pos: Some(pos),
-        posd: None, count: Some(1), face: None, pinned: Some(false),
-        angle: default(), info: Box::new(spec),
-      };
-      let insn = MGI::AddPieces(spec);
-      insns.push(insn);
-    }
-
-    let count = insns.len();
-    chan.alter_game(insns, None)?;
-    writeln!(out, "added {} pieces", count)?;
-    exit(exitcode);
-  }
-
-  inventory_subcmd!{
-    "library-add",
-    "Add pieces from the shape libraries",
-  }
-}
-
-//---------- list-pieces ----------
-
-mod list_pieces {
-  use super::*;
-
-  type Args = NoArgs;
-
-  #[throws(AE)]
-  fn call(SCCA{ mut out, ma, args,.. }:SCCA) {
-    let _args = parse_args::<Args,_>(args, &noargs, &ok_id, None);
-    let mut chan = ma.access_game()?;
-    let (pieces, pcaliases) = chan.list_pieces()?;
-    for p in pieces {
-      writeln!(out, "{:?}", p)?;
-    }
-    for a in pcaliases {
-      writeln!(out, "{:?}", a)?;
-    }
-  }
-
-  inventory_subcmd!{
-    "list-pieces",
-    "List pieces in the game",
-  }
-}
-
-//---------- adhoc json/ron etc. ----------
-
-#[derive(Debug,Copy,Clone)]
-struct AdhocFormat(&'static str);
-
-impl From<&'static Subcommand> for AdhocFormat {
-  fn from(sc: &'static Subcommand) -> Self { AdhocFormat(
-    sc.verb.rsplitn(2,'-').next().unwrap()
-  )}
-}
-
-impl AdhocFormat {
-  pub fn metavar(&self) -> String { self.0.to_uppercase() }
-  pub fn name(&self) -> String { match self.0 {
-    // special cases go here
-    _ => return self.0.to_uppercase(),
-  }/*.into()*/ }
-
-  #[throws(AE)]
-  pub fn parse<T>(&self, input: Vec<String>, what: &str) -> Vec<T>
-  where T: DeserializeOwned
-  {
-    input.into_iter().enumerate().map(|(i,s)| match self.0 {
-      "json" => serde_json::from_str(&s).map_err(AE::from),
-      "ron"  => ron::de::   from_str(&s).map_err(AE::from),
-      _ => panic!(),
-    }
-        .with_context(|| s.clone())
-        .with_context(|| format!("parse {} (#{})", what, i))
-    ).collect::<Result<Vec<T>,AE>>()?
-  }
-
-  #[throws(AE)]
-  pub fn report<T>(&self, out: &mut CookedStdout, resp: T)
-  where T: Serialize
-  {
-    writeln!(out, "{}", match self.0 {
-      "json" => serde_json::to_string(&resp).map_err(AE::from),
-      "ron"  => ron::ser::  to_string(&resp).map_err(AE::from),
-      _ => panic!(),
-    }
-        .context("re-format response")?)?;
-  }
-}
-
-//---------- adhoc command ----------
-
-mod command_adhoc {
-  use super::*;
-
-  #[derive(Default,Debug)]
-  struct Args {
-    cmds: Vec<String>,
-  }
-
-  fn subargs<'ap,'a:'ap,'m:'ap>(
-    sa: &'a mut Args,
-    ahf: AdhocFormat,
-  ) -> ArgumentParser<'ap> {
-    use argparse::*;
-    let mut ap = ArgumentParser::new();
-    ap.refer(&mut sa.cmds).required()
-      .add_argument(format!("{}-COMMAND", ahf.metavar()).leak(),
-                    Collect,
-                    format!("{}-encoded MgmtCommand", ahf.name())
-                    .leak());
-    ap
-  }
-
-  #[throws(AE)]
-  fn call(SCCA{ mut out, ma, args,.. }:SCCA) {
-    let ahf = ma.sc.into();
-
-    let subargs: ApMaker<_> = &|sa| subargs(sa,ahf);
-    let args = parse_args::<Args,_>(args, subargs, &ok_id, None);
-    let mut conn = connect(&ma)?;
-
-    let cmds: Vec<MgmtCommand> = ahf.parse(args.cmds, "cmd")?;
-    for (i, cmd) in cmds.into_iter().enumerate() {
-      let resp = conn.cmd(&cmd).with_context(|| format!("cmd #{}", i))?;
-      ahf.report(&mut out, resp)?;
-    }
-  }
-
-  inventory_subcmd!{
-    "command-json",
-    "run ad-hoc management command(s) (JSON)",
-  }
-  inventory_subcmd!{
-    "command-ron",
-    "run ad-hoc management command(s) (Rusty Object Notation)",
-  }
-}
-
-//---------- alter game ----------
-
-mod alter_game_adhoc {
-  use super::*;
-
-  #[derive(Default,Debug)]
-  struct Args {
-    insns: Vec<String>,
-  }
-
-  fn subargs<'ap,'a:'ap,'m:'ap>(
-    sa: &'a mut Args,
-    ahf: AdhocFormat,
-  ) -> ArgumentParser<'ap> {
-    use argparse::*;
-    let mut ap = ArgumentParser::new();
-    ap.refer(&mut sa.insns).required()
-      .add_argument(format!("{}-INSN", ahf.metavar()).leak(),
-                    Collect,
-                    format!("{}-encoded MgmtGameInstruction", ahf.name())
-                    .leak());
-    ap
-  }
-
-  fn call(SCCA{ mut out, ma, args,.. }:SCCA) -> Result<(),AE> {
-    let ahf = ma.sc.into();
-
-    let subargs: ApMaker<_> = &|sa| subargs(sa,ahf);
-    let args = parse_args::<Args,_>(args, subargs, &ok_id, None);
-    let mut chan = ma.access_game()?;
-
-    let insns: Vec<MgmtGameInstruction> = ahf.parse(args.insns, "insn")?;
-    let resps = chan.alter_game(insns,None)?;
-    for resp in resps {
-      ahf.report(&mut out, resp)?;
-    }
-
-    Ok(())
-  }
-
-  inventory_subcmd!{
-    "alter-game-json",
-    "run an ad-hoc AlterGame command (JSON)",
-  }
-  inventory_subcmd!{
-    "alter-game-ron",
-    "run an ad-hoc AlterGame command (Rusty Object Notation)",
-  }
-}
-
 //---------- upload-bundle ----------
 
 #[derive(Debug)]
diff --git a/cli/uselibs.rs b/cli/uselibs.rs
new file mode 100644 (file)
index 0000000..c511d1b
--- /dev/null
@@ -0,0 +1,356 @@
+// Copyright 2020-2021 Ian Jackson and contributors to Otter
+// SPDX-License-Identifier: AGPL-3.0-or-later
+// There is NO WARRANTY.
+
+use super::*;
+
+//---------- library-list ----------
+
+#[derive(Debug,Default)]
+struct LibGlobArgs {
+  lib: Option<String>,
+  pat: Option<String>,
+}
+
+impl LibGlobArgs {
+  fn add_arguments<'ap, 'tlg: 'ap>(
+    &'tlg mut self,
+    ap: &'_ mut ArgumentParser<'ap>
+  ) {
+    use argparse::*;
+    ap.refer(&mut self.lib).metavar("LIBRARY")
+      .add_option(&["--lib"],StoreOption,"look only in LIBRARY");
+    ap.refer(&mut self.pat)
+      .add_argument("ITEM-GLOB-PATTERN",StoreOption,"item glob pattern");
+  }
+
+  fn lib(&self) -> Option<String> {
+    self.lib.clone()
+  }
+  fn pat(&self) -> String {
+    self.pat.as_ref().map(Deref::deref)
+      .unwrap_or("*")
+      .into()
+  }
+}
+
+mod library_list {
+  use super::*;
+
+  type Args = LibGlobArgs;
+
+  fn subargs(sa: &mut Args) -> ArgumentParser {
+    use argparse::*;
+    let mut ap = ArgumentParser::new();
+    sa.add_arguments(&mut ap);
+    ap
+  }
+
+  #[throws(AE)]
+  fn call(SCCA{ mut out, ma, args,.. }:SCCA) {
+    let args = parse_args::<Args,_>(args, &subargs, &ok_id, None);
+    let mut chan = ma.access_game()?;
+
+    if args.lib.is_none() && args.pat.is_none() {
+      let game = chan.game.clone();
+      let libs = match chan.cmd(&MC::LibraryListLibraries { game })? {
+        MgmtResponse::Libraries(libs) => libs,
+        x => throw!(anyhow!(
+          "unexpected response to LibrarylistLibraries: {:?}", &x)),
+      };
+      for lib in libs {
+        writeln!(out, "{}", lib)?;
+      }
+      return;
+    }
+
+    let items = chan.list_items(args.lib.clone(), args.pat())?;
+    for it in &items {
+      writeln!(out, "{}", it)?;
+    }
+  }
+
+  inventory_subcmd!{
+    "library-list",
+    "List pieces in the shape libraries",
+  }
+}
+
+//---------- library-sdd ----------
+
+mod library_add {
+  use super::*;
+
+  #[derive(Default,Debug)]
+  struct Args {
+    tlg: LibGlobArgs,
+    adjust_markers: Option<bool>,
+    incremental: bool,
+  }
+
+  impl Args {
+    fn adjust_markers(&self) -> bool { self.adjust_markers.unwrap_or(true) }
+  }
+
+  fn subargs(sa: &mut Args) -> ArgumentParser {
+    use argparse::*;
+    let mut ap = ArgumentParser::new();
+    sa.tlg.add_arguments(&mut ap);
+    ap.refer(&mut sa.adjust_markers)
+      .add_option(&["--no-adjust-markers"],StoreConst(Some(false)),
+                  "do not adjust the number of insertion markers, just fail")
+      .add_option(&["--adjust-markers"],StoreConst(Some(true)),"");
+    ap.refer(&mut sa.incremental)
+      .add_option(&["--incremental"],StoreConst(true),
+                  "do not place pieces already on the board; \
+                   if they don't all fit, place as many as possible")
+      .add_option(&["--no-incremental"],StoreConst(false),"");
+    ap
+  }
+
+  fn call(SCCA{ mut out, ma, args,.. }:SCCA) -> Result<(),AE> {
+    const MAGIC: &str = "mgmt-library-load-marker";
+
+    let args = parse_args::<Args,_>(args, &subargs, &ok_id, None);
+    let mut chan = ma.access_game()?;
+    let (pieces, _pcaliases) = chan.list_pieces()?;
+    let markers = pieces.iter().filter(|p| p.itemname.as_str() == MAGIC)
+      .collect::<Vec<_>>();
+
+    let already = if args.incremental { Some(
+      pieces.iter().map(|p| &p.itemname)
+        .collect::<HashSet<_>>()
+    )} else {
+      None
+    };
+
+    if ma.verbose > 2 { dbgc!(&markers, &args, &already); }
+
+    #[derive(Debug)]
+    enum Situation {
+      Poor(Vec<MGI>, &'static str),
+      Good([Pos; 2]),
+    }
+    use Situation::*;
+
+    const WANTED: usize = 2;
+    let situation = if markers.len() < WANTED {
+      let to_add = WANTED - markers.len();
+      let spec = ItemSpec {
+        lib: "wikimedia".to_string(), // todo: make an argument
+        item: MAGIC.to_string(),
+      };
+      let spec = PiecesSpec {
+        pos: None,
+        posd: None,
+        count: Some(to_add as u32),
+        face: None,
+        pinned: Some(false),
+        angle: default(),
+        info: Box::new(spec),
+      };
+      Poor(vec![ MGI::AddPieces(spec) ],
+           "marker(s) created")
+    } else if markers.len() > WANTED {
+      let insns = markers[WANTED..].iter()
+        .map(|p| MGI::DeletePiece(p.piece))
+        .collect();
+      Poor(insns,
+           "surplus marker(s) removed")
+    } else {
+      let mut good: ArrayVec<_,2> = default();
+      for p in &markers {
+        good.push(p.visible.as_ref().ok_or_else(
+          || anyhow!("library marker(s) with hidden position!")
+        )?.pos);
+      }
+      Good(good.into_inner().unwrap())
+    };
+    if ma.verbose > 2 { dbgc!(&situation); }
+
+    #[derive(Debug)]
+    struct Placement {
+      lhs: Coord, top: Coord, rhs: Coord, bot: Coord,
+      clhs: Coord, cbot: Coord, // current line
+    }
+
+    let mut placement = match situation {
+      Poor(insns, msg) => {
+        if !args.adjust_markers() {
+          throw!(anyhow!("only {} markers, wanted {}",
+                         markers.len(), msg));
+        }
+        chan.alter_game(insns, None)?;
+        eprintln!("updated game: {}\n\
+                   please adjust markers as desired and run again",
+                  msg);
+        exit(EXIT_NOTFOUND);
+      }
+      Good([a, b]) => {
+        // todo: take account of the space used by the markers themselves
+        let lhs = min(a.x(), b.x());
+        let rhs = max(a.x(), b.x());
+        let top = min(a.y(), b.y());
+        let bot = max(a.y(), b.y());
+        Placement {
+          lhs, rhs, top, bot,
+          clhs: lhs, cbot: top,
+        }
+      }
+    };
+    if ma.verbose > 3 { dbgc!(&placement); }
+
+    impl Placement {
+      /// If returns None, has already maybe tried to take some space
+      #[throws(AE)]
+      fn place(&mut self, bbox: &Rect,
+               pieces: &Vec<MgmtGamePieceInfo>, ma: &MainOpts)
+               -> Option<Pos> {
+        let PosC{ coords: [w,h] } = (bbox.br() - bbox.tl())?;
+
+        let mut did_newline = false;
+        let (ncbot, tlhs) = 'search: loop {
+          let ncbot = max(self.cbot, self.top + h);
+          if ncbot > self.bot { return None }
+          let mut any_clash_bot = None;
+
+          'within_line: loop {
+            let tlhs = self.clhs;
+            self.clhs += w;
+            if self.clhs > self.rhs { break 'within_line }
+
+            if let Some((nclhs, clash_bot)) = pieces.iter()
+              .filter_map(|p| (|| if_chain! {
+                if let Some(pv) = p.visible.as_ref();
+                let tl = (pv.pos + pv.bbox.tl())?;
+                let br = (pv.pos + pv.bbox.br())?;
+                if !(tl.x() >= self.clhs
+                    || tl.y() >= ncbot
+                    || br.x() <= tlhs
+                    || br.y() <= self.top);
+                then {
+                  if ma.verbose > 2 {
+                    eprintln!(
+                      "at {:?} tlhs={} ncbot={} avoiding {} tl={:?} br={:?}",
+                      &self, tlhs, ncbot, &p.itemname, &tl, &br
+                    )
+                  }
+                  Ok::<_,AE>(Some((br.x(), br.y())))
+                } else {
+                  Ok::<_,AE>(None)
+                }
+              })().transpose())
+              .next().transpose()?
+            {
+              self.clhs = nclhs;
+              any_clash_bot = Some(clash_bot);
+              continue 'within_line;
+            }
+
+            break 'search (ncbot, tlhs);
+          }
+          // line is full
+          self.top = self.cbot;
+          if did_newline {
+            if let Some(top) = any_clash_bot {
+              self.top = top;
+            } else {
+              // if not, will never fit
+              return None;
+            }
+          }
+          did_newline = true;
+          self.clhs = self.lhs;
+          // if we are simply too wide, we'll just loop until off the bottom
+        };
+        self.cbot = ncbot;
+        let ttopleft = PosC::new(tlhs, self.top);
+        let tnominal = (ttopleft - bbox.tl())?;
+
+        if ma.verbose > 3 { dbgc!(&self, &tnominal); }
+        Some(tnominal)
+      }
+    }
+
+    let mut items = chan.list_items(args.tlg.lib(), args.tlg.pat())?;
+
+    fn k(ied: &ItemEnquiryData) -> (&str, &GoodItemName) { (
+      &ied.lib.libname,
+      &ied.itemname,
+    ) }
+    items.sort_by(|a,b| Ord::cmp( &k(a), &k(b) ));
+    items.reverse();
+    items.dedup_by(|a,b| PartialEq::eq( &k(a), &k(b) ));
+    items.reverse();
+
+    let mut exitcode = 0;
+    let mut insns = vec![];
+    for (ix, it) in items.iter().enumerate() {
+      if ma.verbose > 2 { eprintln!(
+        "item {}  {:?}", &it.itemname, &it.f0bbox
+      )};
+      if let Some(already) = &already {
+        if already.contains(&it.itemname) { continue }
+      }
+      let pos = match placement.place(&items[0].f0bbox, &pieces, &ma)? {
+        Some(pos) => pos,
+        None => {
+          let m = format!("out of space after {} at {}",
+                          &ix, &it.itemname);
+          exitcode = EXIT_SPACE;
+          if args.incremental {
+            writeln!(out, "stopping: {}", &m)?;
+            break;
+          } else {
+            eprintln!("error: {}", &m);
+            exit(exitcode);
+          }
+        }
+      };
+      let spec = ItemSpec::from(it);
+      let spec = PiecesSpec {
+        pos: Some(pos),
+        posd: None, count: Some(1), face: None, pinned: Some(false),
+        angle: default(), info: Box::new(spec),
+      };
+      let insn = MGI::AddPieces(spec);
+      insns.push(insn);
+    }
+
+    let count = insns.len();
+    chan.alter_game(insns, None)?;
+    writeln!(out, "added {} pieces", count)?;
+    exit(exitcode);
+  }
+
+  inventory_subcmd!{
+    "library-add",
+    "Add pieces from the shape libraries",
+  }
+}
+
+//---------- list-pieces ----------
+
+mod list_pieces {
+  use super::*;
+
+  type Args = NoArgs;
+
+  #[throws(AE)]
+  fn call(SCCA{ mut out, ma, args,.. }:SCCA) {
+    let _args = parse_args::<Args,_>(args, &noargs, &ok_id, None);
+    let mut chan = ma.access_game()?;
+    let (pieces, pcaliases) = chan.list_pieces()?;
+    for p in pieces {
+      writeln!(out, "{:?}", p)?;
+    }
+    for a in pcaliases {
+      writeln!(out, "{:?}", a)?;
+    }
+  }
+
+  inventory_subcmd!{
+    "list-pieces",
+    "List pieces in the game",
+  }
+}