niels vor 2 Jahren
Commit
86a2213612
16 geänderte Dateien mit 1657 neuen und 0 gelöschten Zeilen
  1. 7 0
      .gitignore
  2. 673 0
      Cargo.lock
  3. 19 0
      Cargo.toml
  4. 3 0
      run.zsh
  5. 117 0
      src/aoc_day.rs
  6. 28 0
      src/bin/day1.rs
  7. 192 0
      src/command.rs
  8. 219 0
      src/common.rs
  9. 248 0
      src/exec.rs
  10. 1 0
      src/lib.rs
  11. 24 0
      src/main.rs
  12. 96 0
      src/web.rs
  13. 16 0
      templates/day.rs.tpl
  14. 0 0
      test_inputs/day1.txt
  15. 4 0
      web/lynx.cfg
  16. 10 0
      web/scroll.script

+ 7 - 0
.gitignore

@@ -0,0 +1,7 @@
+/target
+.idea/
+*.iml
+inputs/
+outputs/
+debug_outputs/
+web/cookie

+ 673 - 0
Cargo.lock

@@ -0,0 +1,673 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "aho-corasick"
+version = "0.7.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "ansi_term"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "aoc2022_niels_overkamp"
+version = "0.1.0"
+dependencies = [
+ "ansi_term",
+ "arguments",
+ "chrono",
+ "curl",
+ "hex",
+ "itertools",
+ "petgraph",
+ "regex",
+ "tempfile",
+]
+
+[[package]]
+name = "arguments"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fb117e92724f1317fcf793e49511b864057168a6d7a8ab1533319f409515cde"
+dependencies = [
+ "options",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bumpalo"
+version = "3.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba"
+
+[[package]]
+name = "cc"
+version = "1.0.77"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e9f73505338f7d905b19d18738976aae232eb46b8efc15554ffc56deb5d9ebe4"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "chrono"
+version = "0.4.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f"
+dependencies = [
+ "iana-time-zone",
+ "js-sys",
+ "num-integer",
+ "num-traits",
+ "time",
+ "wasm-bindgen",
+ "winapi",
+]
+
+[[package]]
+name = "codespan-reporting"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e"
+dependencies = [
+ "termcolor",
+ "unicode-width",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
+
+[[package]]
+name = "curl"
+version = "0.4.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "509bd11746c7ac09ebd19f0b17782eae80aadee26237658a6b4808afb5c11a22"
+dependencies = [
+ "curl-sys",
+ "libc",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "socket2",
+ "winapi",
+]
+
+[[package]]
+name = "curl-sys"
+version = "0.4.59+curl-7.86.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6cfce34829f448b08f55b7db6d0009e23e2e86a34e8c2b366269bf5799b4a407"
+dependencies = [
+ "cc",
+ "libc",
+ "libz-sys",
+ "openssl-sys",
+ "pkg-config",
+ "vcpkg",
+ "winapi",
+]
+
+[[package]]
+name = "cxx"
+version = "1.0.82"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4a41a86530d0fe7f5d9ea779916b7cadd2d4f9add748b99c2c029cbbdfaf453"
+dependencies = [
+ "cc",
+ "cxxbridge-flags",
+ "cxxbridge-macro",
+ "link-cplusplus",
+]
+
+[[package]]
+name = "cxx-build"
+version = "1.0.82"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06416d667ff3e3ad2df1cd8cd8afae5da26cf9cec4d0825040f88b5ca659a2f0"
+dependencies = [
+ "cc",
+ "codespan-reporting",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "scratch",
+ "syn",
+]
+
+[[package]]
+name = "cxxbridge-flags"
+version = "1.0.82"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "820a9a2af1669deeef27cb271f476ffd196a2c4b6731336011e0ba63e2c7cf71"
+
+[[package]]
+name = "cxxbridge-macro"
+version = "1.0.82"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a08a6e2fcc370a089ad3b4aaf54db3b1b4cee38ddabce5896b33eb693275f470"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "either"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797"
+
+[[package]]
+name = "fastrand"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499"
+dependencies = [
+ "instant",
+]
+
+[[package]]
+name = "fixedbitset"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
+
+[[package]]
+name = "hashbrown"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.53"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "wasm-bindgen",
+ "winapi",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca"
+dependencies = [
+ "cxx",
+ "cxx-build",
+]
+
+[[package]]
+name = "indexmap"
+version = "1.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399"
+dependencies = [
+ "autocfg",
+ "hashbrown",
+]
+
+[[package]]
+name = "instant"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "itertools"
+version = "0.10.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "js-sys"
+version = "0.3.60"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47"
+dependencies = [
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "libc"
+version = "0.2.137"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89"
+
+[[package]]
+name = "libz-sys"
+version = "1.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9702761c3935f8cc2f101793272e202c72b99da8f4224a19ddcf1279a6450bbf"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "link-cplusplus"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9272ab7b96c9046fbc5bc56c06c117cb639fe2d509df0c421cad82d2915cf369"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "log"
+version = "0.4.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "memchr"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
+
+[[package]]
+name = "num-integer"
+version = "0.1.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
+dependencies = [
+ "autocfg",
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860"
+
+[[package]]
+name = "openssl-probe"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.78"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07d5c8cb6e57b3a3612064d7b18b117912b4ce70955c2504d4b741c9e244b132"
+dependencies = [
+ "autocfg",
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "options"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61a1c9b12c453182754f3126c5689324c7214265de2df79917b59de87547a916"
+
+[[package]]
+name = "petgraph"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6d5014253a1331579ce62aa67443b4a658c5e7dd03d4bc6d302b94474888143"
+dependencies = [
+ "fixedbitset",
+ "indexmap",
+]
+
+[[package]]
+name = "pkg-config"
+version = "0.3.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "regex"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848"
+
+[[package]]
+name = "remove_dir_all"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "schannel"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2"
+dependencies = [
+ "lazy_static",
+ "windows-sys",
+]
+
+[[package]]
+name = "scratch"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8132065adcfd6e02db789d9285a0deb2f3fcb04002865ab67d5fb103533898"
+
+[[package]]
+name = "socket2"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "syn"
+version = "1.0.105"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60b9b43d45702de4c839cb9b51d9f529c5dd26a4aff255b42b1ebc03e88ee908"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4"
+dependencies = [
+ "cfg-if",
+ "fastrand",
+ "libc",
+ "redox_syscall",
+ "remove_dir_all",
+ "winapi",
+]
+
+[[package]]
+name = "termcolor"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "time"
+version = "0.1.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a"
+dependencies = [
+ "libc",
+ "wasi",
+ "winapi",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3"
+
+[[package]]
+name = "unicode-width"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
+
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+[[package]]
+name = "wasi"
+version = "0.10.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142"
+dependencies = [
+ "bumpalo",
+ "log",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f"
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows-sys"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2"
+dependencies = [
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"

+ 19 - 0
Cargo.toml

@@ -0,0 +1,19 @@
+[package]
+name = "aoc2022_niels_overkamp"
+version = "0.1.0"
+authors = ["niels <niels.overkamp@gmail.com>"]
+edition = "2021"
+default-run = "aoc2022_niels_overkamp"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+regex = "1"
+hex = "0.4.2"
+arguments = "0.6.2"
+ansi_term = "0.12.1"
+chrono = "0.4.19"
+curl = "0.4.34"
+tempfile = "3.1.0"
+itertools = "0.10.1"
+petgraph = "0.6.0"

+ 3 - 0
run.zsh

@@ -0,0 +1,3 @@
+#!/bin/zsh
+
+cargo run

+ 117 - 0
src/aoc_day.rs

@@ -0,0 +1,117 @@
+use chrono::{Datelike, Timelike, TimeZone};
+use std::{path, fmt, fs};
+use std::fmt::{Display, Formatter};
+use std::error::Error;
+
+pub struct AocDay {
+    pub day: u8,
+    pub year: u16,
+}
+
+impl AocDay {
+    pub fn new(day: u8, year: u16) -> AocDay {
+        AocDay { day, year }
+    }
+
+    pub fn today() -> AocDay {
+        let now = chrono::Utc::now();
+        if now.month() == 12 {
+            if now.hour() >= 5 {
+                Self::new(now.day().min(25) as u8, now.year() as u16)
+            } else {
+                Self::new((now.day() - 1).min(25).max(1) as u8, now.year() as u16)
+            }
+        } else if now.month() <= 2 {
+            Self::new(25, now.year() as u16 - 1)
+        } else {
+            Self::new(1, now.year() as u16 )
+        }
+    }
+
+    pub fn is_open(&self) -> bool {
+        let start_moment = chrono::Utc
+            .ymd(self.year as i32, 12, self.day as u32)
+            .and_hms(5, 0, 0);
+        return chrono::Utc::now().ge(&start_moment);
+    }
+
+    pub fn source_code_path(&self) -> String {
+        format!("./src/bin/day{}.rs", self.day)
+    }
+
+    pub fn input_file_path(&self) -> String {
+        format!("./inputs/day{}.txt", self.day)
+    }
+
+    pub fn test_input_file_path(&self) -> String {
+        format!("./test_inputs/day{}.txt", self.day)
+    }
+
+    pub fn output_file_path(&self, part: &Part) -> String {
+        format!("outputs/day{}-{}.txt", self.day, part.as_str())
+    }
+
+    pub fn has_source_code(&self) -> bool {
+        path::Path::new(self.source_code_path().as_str()).exists()
+    }
+
+    pub fn has_input_file(&self) -> bool {
+        path::Path::new(self.input_file_path().as_str()).exists()
+    }
+
+    pub fn has_test_input_file(&self) -> bool {
+        path::Path::new(self.test_input_file_path().as_str()).exists()
+    }
+
+    pub fn output_file_age(&self, part: &Part) -> Result<Option<chrono::Duration>, Box<dyn Error>> {
+        let metadata: Option<fs::Metadata> = fs::metadata(self.output_file_path(part).as_str())
+            .map(Some).unwrap_or(None);
+        if let Some(metadata) = metadata {
+            Ok(Some(chrono::Duration::from_std(metadata.modified()?.elapsed()?)?))
+        } else {
+            Ok(None)
+        }
+    }
+
+    pub fn input_file_url(&self) -> String {
+        format!("https://adventofcode.com/{}/day/{}/input", self.year, self.day)
+    }
+
+    pub fn submit_url(&self) -> String {
+        format!("https://adventofcode.com/{}/day/{}/answer", self.year, self.day)
+    }
+
+    pub fn puzzle_url(&self) -> String {
+        format!("https://adventofcode.com/{}/day/{}", self.year, self.day)
+    }
+
+    pub fn leaderboard_url(&self) -> String {
+        format!("https://adventofcode.com/{}/leaderboard/private/view/424060", self.year)
+    }
+}
+
+pub enum Part {
+    ONE, TWO,
+}
+
+impl Display for Part {
+    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+        f.write_str(self.as_str())
+    }
+}
+
+impl Part {
+    pub fn as_str(&self) -> &'static str {
+        match self {
+            Part::ONE => "one",
+            Part::TWO => "two",
+        }
+    }
+    
+    pub fn as_digit(&self) -> u8 {
+        match self {
+            Part::ONE => 1,
+            Part::TWO => 2,
+        }
+    }
+}

+ 28 - 0
src/bin/day1.rs

@@ -0,0 +1,28 @@
+use aoc2022_niels_overkamp::common::{self, AOCResult};
+use std::result::Result::*;
+
+const DAY: &str = "day1";
+
+fn main() -> Result<(), Box<dyn std::error::Error>> {
+    common::run(DAY, &run)
+}
+
+pub fn run(input: &Vec<String>) -> AOCResult {
+    let mut calories = vec![0];
+    for calorie in input.iter() {
+        if let Ok(calorie) = calorie.parse::<u64>() {
+            *calories.last_mut().unwrap() += calorie;
+        } else {
+            calories.push(0);
+        }
+    }
+
+    calories.sort_by(|a,b| a.cmp(b).reverse());
+
+    Ok([Some(calories[0].to_string()), Some(calories[0..3].iter().sum::<u64>().to_string())])
+}
+
+#[test]
+pub fn test_day1() {
+    assert!(common::run_test(DAY, &run))
+}

+ 192 - 0
src/command.rs

@@ -0,0 +1,192 @@
+use core::fmt;
+use std::{error, io};
+use std::io::{stdin, stdout, Write};
+use std::str::FromStr;
+use chrono::Duration;
+
+pub enum CommandType {
+    Test,
+    Run,
+    Submit,
+    NextPart,
+    Puzzle,
+    Leaderboard,
+    Day,
+    Year,
+    Help,
+    Quit,
+}
+
+pub struct Command {
+    pub command_type: CommandType,
+    pub arguments: Vec<String>,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct CommandParseErr(());
+
+impl fmt::Display for CommandParseErr {
+    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
+        fmt.write_str("invalid CommandType syntax")
+    }
+}
+
+impl error::Error for CommandParseErr {}
+
+impl FromStr for CommandType {
+    type Err = CommandParseErr;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        match s.to_lowercase().replace(" ", "").as_str() {
+            "test" | "t" => Ok(CommandType::Test),
+            "run" | "r" => Ok(CommandType::Run),
+            "submit" | "s" => Ok(CommandType::Submit),
+            "nextpart" | "n" => Ok(CommandType::NextPart),
+            "puzzle" | "p" => Ok(CommandType::Puzzle),
+            "leaderboard" | "l" => Ok(CommandType::Leaderboard),
+            "changeday" | "day" | "d" => Ok(CommandType::Day),
+            "changeyear" | "year" | "y" => Ok(CommandType::Year),
+            "help" | "h" | "?" => Ok(CommandType::Help),
+            "quit" | "q" => Ok(CommandType::Quit),
+            _ => Err(CommandParseErr(()))
+        }
+    }
+}
+
+impl FromStr for Command {
+    type Err = CommandParseErr;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        let mut arguments = s.split_whitespace();
+        let command_type = arguments.next().ok_or(CommandParseErr(()))?.parse::<CommandType>()?;
+        let arguments = arguments.map(ToOwned::to_owned).collect();
+        return Ok(Command {
+            command_type,
+            arguments,
+        });
+    }
+}
+
+pub struct Terminal {
+    pub strings: TerminalStrings,
+}
+
+pub struct TerminalStrings {
+    pub emph_style: ansi_term::Style,
+    help: String,
+    y_n_true: String,
+    y_n_false: String,
+    y_n_none: String,
+}
+
+impl Terminal {
+    pub fn new() -> Self {
+        Terminal {
+            strings: TerminalStrings::new(),
+        }
+    }
+
+    pub fn yes_no(&self, default: Option<bool>, question: Option<&str>) -> io::Result<Option<bool>> {
+        print!("{}{}> ", question.unwrap_or(""), self.strings.yes_no_text(default));
+        stdout().flush()?;
+        return Self::read_yes_no(default);
+    }
+
+    pub fn read_yes_no(default: Option<bool>) -> io::Result<Option<bool>> {
+        let mut input = String::new();
+        if stdin().read_line(&mut input)? == 0 {
+            return Ok(None);
+        }
+        return Ok(
+            match input.as_str().strip_suffix("\n").unwrap() {
+                "y" | "Y" => Some(true),
+                "n" | "N" => Some(false),
+                _ => default
+            }
+        );
+    }
+
+    pub fn options(&self, default: Option<&str>, options: &[&str], question: Option<&str>) -> io::Result<Option<String>> {
+        let mut s = String::new();
+        let options: &[String] = &options.iter()
+            .map(|o| {
+                let (i, c) = (*o).char_indices().next().unwrap();
+                let (emph_c, rest) = (self.strings.emph_style.paint(c.to_string().to_uppercase()).to_string(), &o[(i+1)..]);
+                s.push_str(format!("{}{}/", emph_c, rest).as_str());
+                return c.to_lowercase().to_string();
+            })
+            .collect::<Vec<String>>()[..];
+
+        print!("{}({})> ",
+               question.unwrap_or(""),
+               s.trim_end_matches("/"),
+        );
+
+        stdout().flush()?;
+        return Self::read_options(default, options);
+    }
+
+    pub fn read_options(default: Option<&str>, options: &[String]) -> io::Result<Option<String>> {
+        let mut input = String::new();
+        if stdin().read_line(&mut input)? == 0 {
+            return Ok(None);
+        }
+        let input = input.to_lowercase();
+        let mut chars = input.trim_end().chars();
+        return Ok(chars.next().map(|c| c.to_lowercase().to_string()).filter(|c| options.contains(&c)).or(default.map(|d| d.to_owned())));
+    }
+
+    pub fn help(&self) {
+        println!("{}", self.strings.help)
+    }
+}
+
+impl TerminalStrings {
+    fn new() -> Self {
+        let emph_style = ansi_term::Style::new().bold().underline();
+        TerminalStrings {
+            help: format!("Choose action: {}est, {}un, {}ubmit, {}ext Part, open {}uzzle, open {}eaderboard, change {}ay, change {}ear, {}elp, {}uit",
+                          emph_style.paint("T"),
+                          emph_style.paint("R"),
+                          emph_style.paint("S"),
+                          emph_style.paint("N"),
+                          emph_style.paint("P"),
+                          emph_style.paint("L"),
+                          emph_style.paint("D"),
+                          emph_style.paint("Y"),
+                          emph_style.paint("H"),
+                          emph_style.paint("Q")),
+            y_n_true: format!("{}/{} ", emph_style.paint("Y"), emph_style.paint("n")),
+            y_n_false: format!("{}/{} ", emph_style.paint("y"), emph_style.paint("N")),
+            y_n_none: format!("{}/{} ", emph_style.paint("y"), emph_style.paint("n")),
+            emph_style,
+        }
+    }
+
+    pub fn yes_no_text(&self, default: Option<bool>) -> &String {
+        match default {
+            Some(true) => &self.y_n_true,
+            Some(false) => &self.y_n_false,
+            None => &self.y_n_none,
+        }
+    }
+}
+
+pub fn format_duration(duration: Duration) -> String {
+    if duration.num_weeks() != 0 {
+        return format!("{} weeks", duration.num_weeks());
+    }
+    if duration.num_days() != 0 {
+        return format!("{} days", duration.num_days());
+    }
+    if duration.num_hours() != 0 {
+        return format!("{} hours", duration.num_hours());
+    }
+    if duration.num_minutes() != 0 {
+        return format!("{} minutes", duration.num_minutes());
+    }
+    if duration.num_seconds() != 0 {
+        return format!("{} seconds", duration.num_seconds());
+    }
+    return "now".to_owned();
+}

+ 219 - 0
src/common.rs

@@ -0,0 +1,219 @@
+use std::{fs, str};
+use std::error::Error;
+use std::fs::File;
+use std::io::{self, BufRead, Write};
+use std::path::Path;
+
+pub type AOCResult = Result<[Option<String>; 2], Box<dyn Error>>;
+
+pub trait AOCProgram {
+    fn run(&self, input: &Vec<String>) -> AOCResult;
+}
+
+impl<F: Fn(&Vec<String>) -> AOCResult> AOCProgram for F {
+    fn run(&self, input: &Vec<String>) -> AOCResult {
+        self(input)
+    }
+}
+
+fn read_lines<P>(filename: P) -> io::Result<Vec<String>>
+    where P: AsRef<Path> {
+    let file = File::open(filename)?;
+    let lines = io::BufReader::new(file).lines();
+    return lines.collect();
+}
+
+pub fn run<F: AOCProgram>(day: &str, prog: &F) -> Result<(), Box<dyn Error>> {
+    let arguments = std::env::args();
+    let arguments = arguments::parse(arguments)?;
+    let part: Option<usize> = arguments.get("part");
+    let output_file: Option<String> = arguments.get("output-file");
+    // let skip_test = arguments.get::<bool>("skip-test").unwrap_or(false);
+
+    let input = read_lines(format!("./inputs/{}.txt", day))?;
+
+    let result = prog.run(&input);
+
+    let output: Result<String, Box<dyn Error>> = match result {
+        Err(e) => {
+            let res = format!("Failed on input {:?} with error {:?}", &input, e).into();
+            return Err(res);
+        }
+        Ok(result) =>
+            if part.is_none() {
+                match &result {
+                    [Some(o1), Some(o2)] => Ok(format!("1:{}\n2:{}", o1, o2)),
+                    [Some(o1), None] => Ok(format!("1:{}", o1)),
+                    [None, Some(o2)] => Ok(format!("2:{}", o2)),
+                    [None, None] => Ok(format!("No output"))
+                }
+            } else {
+                let part = part.unwrap();
+                let result = &result[part - 1];
+                match result {
+                    None => {
+                        let res = format!("Part {} not implemented", part).into();
+                        return Err(res);
+                    }
+                    Some(s) => Ok(s.to_owned())
+                }
+            }
+    };
+
+    let output = output?;
+
+    return match output_file {
+        Some(output_file) => {
+            let output = output.as_bytes();
+            let mut file = File::create(output_file)?;
+            file.write(output).map(|_| ())
+        }
+        None => Ok(println!("{}", output))
+    }.map_err(|e| e.into());
+}
+
+pub fn run_prog<F: AOCProgram>(day: &str, prog: &F) {
+    let inputs = files_to_vec(day, "./inputs");
+
+    if let Err(e) = inputs {
+        eprintln!("Error occurred while reading input files {:?}", e);
+        return;
+    }
+    let inputs = inputs.unwrap();
+
+    for (path, input) in inputs {
+        let result = prog.run(&input);
+        match result {
+            Err(e) => println!("{}: Failed on input {:?} with error {:?}", path, &input, e),
+            Ok(result) => match &result {
+                [Some(o1), Some(o2)] => println!("{}:\n\t1:{}\n\t2:{}", path, o1, o2),
+                [Some(o1), None] => println!("{}:\n\t1:{}", path, o1),
+                [None, Some(o2)] => println!("{}:\n\t2:{}", path, o2),
+                [None, None] => println!("{}: No output", path)
+            }
+        }
+    }
+}
+
+
+pub fn run_test<F: AOCProgram>(day: &str, prog: &F) -> bool {
+    let expected_re: regex::Regex = regex::Regex::new(r"^([^\$]+)?(?:\$([^\$]+))?$").unwrap();
+
+    let inputs = files_to_vec(day, "./test_inputs");
+
+    if let Err(e) = inputs {
+        eprintln!("Error occurred while reading test input files {:?}", e);
+        return false;
+    }
+    let inputs = inputs.unwrap();
+
+    if inputs.len() == 0 {
+        println!("Tests failed: No valid input files given");
+        return false;
+    }
+
+    let mut succeed = true;
+
+    for (path, mut input) in inputs.clone() {
+        let expected = input.pop();
+        if expected.is_none() {
+            println!("{}: empty", path);
+            continue;
+        }
+        let expected = expected.unwrap();
+
+        let exp_caps = expected_re.captures(&expected);
+        let exp1 = exp_caps.as_ref().and_then(|cap| cap.get(1)).map(|m| m.as_str());
+        let exp2 = exp_caps.as_ref().and_then(|cap| cap.get(2)).map(|m| m.as_str());
+
+        let result = test::<F>(prog, &input, [exp1, exp2], &path);
+        succeed = succeed && result[0].unwrap_or(true) && result[1].unwrap_or(true);
+    }
+
+    return succeed;
+}
+
+fn test<F: AOCProgram>(prog: &F, input: &Vec<String>, expected: [Option<&str>; 2], path: &str) -> [Option<bool>; 2] {
+    let result = prog.run(input);
+    if result.is_err() {
+        println!("{}: Program failed on input {:?} with error {:?}",
+                 path, &input[..10.min(input.len())], result.unwrap_err());
+        return [Some(false), Some(false)];
+    }
+    let result = &result.unwrap();
+
+    let succeeded = [
+        result[0]
+            .as_ref()
+            .ok_or("Program did not produce output")
+            .and_then(|r| expected[0]
+                .ok_or("No expected output")
+                .map(|e| e == r.as_str())
+            ),
+        result[1]
+            .as_ref()
+            .ok_or("Program did not produce output")
+            .and_then(|r| expected[1]
+                .ok_or("No expected output")
+                .map(|e| e == r.as_str())
+            ),
+    ];
+
+    if succeeded[0].is_err() {
+        println!("{}: Skipped testing result 1: {}", path, succeeded[0].unwrap_err())
+    } else {
+        if succeeded[0].unwrap() {
+            println!("{}: Result 1 succeeded", path)
+        } else {
+            eprintln!("{}: Result 1 failed: Wrong output for input {:?}.\n\tExpected \"{}\"\n\tReceived \"{}\"",
+                      path, &input[..10.min(input.len())], expected[0].as_ref().unwrap(), result[0].as_ref().unwrap());
+        }
+    }
+
+    if succeeded[1].is_err() {
+        println!("{}: Skipped testing result 2: {}", path, succeeded[1].unwrap_err())
+    } else {
+        if succeeded[1].unwrap() {
+            println!("{}: Result 2 succeeded", path)
+        } else {
+            eprintln!("{}: Result 2 failed: Wrong output for input {:?}.\n\tExpected {}\n\tReceived {}",
+                      path, &input[..10.min(input.len())], expected[1].as_ref().unwrap(), result[1].as_ref().unwrap());
+        }
+    }
+
+    return [succeeded[0].ok(), succeeded[1].ok()];
+}
+
+fn files_to_vec<'a>(day: &str, input_path: &str) -> Result<Vec<(String, Vec<String>)>, Box<dyn Error>> {
+    let dir = fs::read_dir(input_path)?;
+
+    let file_name_re = regex::Regex::new(&format!(r"{}(?:-.*)?(?:\..*)", day))?;
+
+    let results = dir.map::<io::Result<_>, _>(|f| {
+        let f = f?;
+
+        if !f.metadata()?.is_file() {
+            return Ok(None);
+        }
+
+        let path = f.path();
+
+        let file_name = path.file_name().and_then(|s| s.to_str());
+
+        if !file_name.map(|s| file_name_re.is_match(s)).unwrap_or(false) {
+            return Ok(None);
+        }
+        let file_name = String::from(file_name.unwrap());
+
+        return Ok(Some((file_name, read_lines(f.path())?)));
+    }).fold(vec![], |mut acc, r| {
+        match r {
+            Ok(None) => (),
+            Ok(Some(v)) => acc.push(v),
+            Err(e) => eprintln!("Error while processing file: {:?}", e),
+        }
+        return acc;
+    });
+
+    return Ok(results);
+}

+ 248 - 0
src/exec.rs

@@ -0,0 +1,248 @@
+use std::{fs, io, process};
+use std::error::Error;
+use std::io::{Read, stderr, stdin, stdout, Write};
+use std::str::FromStr;
+
+use crate::aoc_day::{AocDay, Part};
+use crate::command::{Command, CommandType, format_duration, Terminal};
+use crate::web::WebContext;
+
+pub struct Runner<> {
+    pub day: AocDay,
+    pub part: Part,
+    pub terminal: Terminal,
+    pub web_context: WebContext,
+}
+// TODO replace TODO with issues
+
+impl Runner {
+    pub fn new(day: AocDay, part: Part, web_context: WebContext) -> Self {
+        Self {
+            day,
+            part,
+            web_context,
+            terminal: Terminal::new(),
+        }
+    }
+
+    pub fn close(self) -> io::Result<()> {
+        self.web_context.close()
+    }
+
+    pub fn make_missing_files_if_open(&mut self) -> Result<(), Box<dyn Error>> {
+        let Self {
+            ref day,
+            ref mut terminal,
+            ref mut web_context,
+            ..
+        } = self;
+        if day.is_open() {
+            if !day.has_source_code() {
+                let mut child = process::Command::new("gucci")
+                    .args(&[
+                        "-s",
+                        format!("day=day{}", day.day).as_str(),
+                        "templates/day.rs.tpl"
+                    ])
+                    .stdout(process::Stdio::from(fs::File::create(day.source_code_path())?))
+                    .stderr(process::Stdio::inherit())
+                    .spawn()?;
+                if !child.wait()?.success() {
+                    return Err("Error while running gucci".into());
+                }
+
+                process::Command::new("git").args(&["add", day.source_code_path().as_str()]).status()?;
+            }
+            if !day.has_input_file() {
+                let answer = terminal.yes_no(Some(true), Some("Fetch input file from Advent of Code website? "))?;
+                let res: Result<(), Box<dyn Error>> = match answer {
+                    Some(true) => {
+                        println!("Fetching...");
+                        web_context.curl_request_to_named_file(
+                            day.input_file_url().as_str(),
+                            day.input_file_path(),
+                        )?;
+                        return Ok(());
+                    }
+                    Some(false) | None => Ok(())
+                };
+                res?;
+            }
+            if !day.has_test_input_file() {
+                fs::File::create(day.test_input_file_path())?;
+                process::Command::new("git").args(&["add", day.test_input_file_path().as_str()]).status()?;
+            }
+        }
+        return Ok(());
+    }
+
+    pub fn test(&self) -> Result<bool, Box<dyn Error>> {
+        let output = process::Command::new("cargo")
+            .args(&[
+                "test",
+                "-q",
+                format!("test_day{}", self.day.day).as_str(),    // TODO specify which part to test
+                "--bin",
+                format!("day{}", self.day.day).as_str()
+            ])
+            .output()?;
+        eprintln!("{}", String::from_utf8(output.stderr)?);
+        println!("{}", String::from_utf8(output.stdout)?);
+        return Ok(output.status.success());
+    }
+
+    pub fn run(&mut self) -> Result<(), Box<dyn Error>> {
+        let test_succeeded = self.test()?;
+        if test_succeeded || self.terminal.yes_no(Some(false), Some("Test failed, run anyways? "))?.unwrap_or(false) {
+            let output_file = format!("outputs/day{}-{}.txt", self.day.day, self.part.as_str());
+            let output = process::Command::new("cargo")
+                .args(&[
+                    "run",
+                    "-q",
+                    "--bin", format!("day{}", self.day.day).as_str(),
+                    "--",
+                    "--part", self.part.as_digit().to_string().as_str(),
+                    "--output-file", self.day.output_file_path(&self.part).as_str()
+                ])
+                .output()?; // TODO Spawn and do fancy terminal things
+            eprintln!("{}", String::from_utf8(output.stderr)?);
+            println!("{}", String::from_utf8(output.stdout)?);
+            if output.status.success() {
+                let mut output_file = fs::File::open(output_file)?;
+                let mut output = String::new();
+                output_file.read_to_string(&mut output)?;
+                println!("For day {} part {} got result {}",
+                         self.terminal.strings.emph_style.paint(self.day.day.to_string()),
+                         self.terminal.strings.emph_style.paint(&self.part.to_string()),
+                         self.terminal.strings.emph_style.paint(&output)
+                );
+                // TODO Query puzzle status from AOC before submitting
+                if self.terminal.yes_no(Some(true), Some("Submit to Advent of Code? "))?
+                    .unwrap_or(false) {
+                    self.do_submit(output)?;
+                }
+            }
+        }
+        return Ok(());
+    }
+
+    fn submit(&mut self) -> Result<(), Box<dyn Error>> {
+        let output_age = self.day.output_file_age(&self.part)?;
+        if let Some(output_age) = output_age {
+            let mut output = String::new();
+            fs::File::open(self.day.output_file_path(&self.part))?.read_to_string(&mut output)?;
+            let answer = self.terminal.options(Some("r"), &["Rerun", "Submit", "Cancel"],
+                                               Some(format!("Found output {} with age {} ",
+                                                            self.terminal.strings.emph_style.paint(&output),
+                                                            format_duration(output_age)).as_str()))?;
+
+            println!("{:?}", &answer);
+            if let Some(s) = answer {
+                match s.as_str() {
+                    "r" => self.run()?,
+                    "s" => self.do_submit(output)?,
+                    _ => (),
+                }
+            }
+        } else {
+            self.run()?
+        }
+        Ok(())
+    }
+
+    fn do_submit(&mut self, submission: String) -> Result<(), Box<dyn Error>> {
+        // TODO get tls session from curl and wrap around lynx, OR parse output file into text or minimal html
+        let data = format!("level={}&answer={}", self.part.as_digit(), submission);
+        self.web_context.curl_post_to_lynx(self.day.submit_url().as_str(), data.as_bytes())
+    }
+
+    pub fn switch_part(&mut self) {
+        match self.part {
+            Part::ONE => self.part = Part::TWO,
+            Part::TWO => self.part = Part::ONE,
+        }
+    }
+
+    pub fn change_day(&mut self, args: &[&str]) -> Result<(), String> {
+        let day = args.first()
+            .ok_or("Expected 1 argument got 0")?
+            .parse::<u8>()
+            .map_err(|e| format!("Invalid argument 1: {}", e))?;
+        if day <= 25 {
+            self.day.day = day;
+            Ok(())
+        } else {
+            Err(format!("AoC is played on 1-25 December, {} December is not a valid day.", day))
+        }
+    }
+
+    pub fn change_year(&mut self, args: &[&str]) -> Result<(), String> {
+        let year = args.first()
+            .ok_or("Expected 1 argument got 0")?
+            .parse::<u16>()
+            .map_err(|e| format!("Invalid argument 1: {}", e))?;
+        if year >= 2015 {
+            self.day.year = year;
+            Ok(())
+        } else {
+            Err(format!("AoC started in 2015, {} is not a valid year.", year))
+        }
+    }
+
+    pub fn print_day(&self) {
+        let ordinal_suffix = if self.day.day >= 10 && self.day.day <= 20 {
+            "th"
+        } else {
+            match self.day.day % 10 {
+                1 => "st",
+                2 => "nd",
+                3 => "rd",
+                _ => "th"
+            }
+        };
+        println!("Current Advent of Code Day is the {}{} of {}", self.day.day, ordinal_suffix, self.day.year); // TODO make impl Display for AOCDay
+    }
+
+    pub fn start_runner(&mut self) -> Result<(), Box<dyn Error>> {
+        self.print_day();
+        self.terminal.help();
+        self.make_missing_files_if_open()?;
+
+        loop {
+            stderr().flush()?;
+            print!("> ");
+            stdout().flush()?;
+            let mut line = String::new();
+            stdin().read_line(&mut line)?;
+            let args: Vec<&str> = line.trim().split_whitespace().collect();
+            match args.first().map(|s| Command::from_str(*s).map_err(|e| (s, e))) {
+                None => (),
+                Some(Err((s, _))) => eprintln!("Unknown command \"{}\"", *s),
+                Some(Ok(command)) => {
+                    let args = &args[1..];
+                    match command.command_type {
+                        CommandType::Test => self.test().map(|_| ()),
+                        CommandType::Run => self.run(),
+                        CommandType::Submit => self.submit(),
+                        CommandType::NextPart => Ok(self.switch_part()),
+                        CommandType::Puzzle => Ok(self.web_context.lynx(self.day.puzzle_url())?),
+                        CommandType::Day => Ok({
+                            self.change_day(args)?;
+                            self.print_day();
+                            self.make_missing_files_if_open()?;
+                        }),
+                        CommandType::Year => Ok({
+                            self.change_year(args)?;
+                            self.print_day();
+                        }),
+                        CommandType::Leaderboard => Ok(self.web_context.lynx(self.day.leaderboard_url())?),
+                        CommandType::Help => Ok(self.terminal.help()),
+                        CommandType::Quit => break,
+                    }?;
+                }
+            }
+        }
+
+        return Ok(());
+    }
+}

+ 1 - 0
src/lib.rs

@@ -0,0 +1 @@
+pub mod common;

+ 24 - 0
src/main.rs

@@ -0,0 +1,24 @@
+use std::error::Error;
+
+use crate::aoc_day::{AocDay, Part};
+use crate::exec::Runner;
+
+mod command;
+mod web;
+mod exec;
+mod aoc_day;
+
+fn main() -> Result<(), Box<dyn Error>> {
+    let mut runner = Runner::new(
+        AocDay::today(),
+        Part::ONE,
+        web::WebContext::new("web/cookie".to_owned(), "web/lynx.cfg".to_owned())?,
+    );
+
+    runner.start_runner()?;
+    runner.close()?;
+
+    return Ok(());
+}
+
+

+ 96 - 0
src/web.rs

@@ -0,0 +1,96 @@
+use std::{error, fs, io, path, process};
+use std::io::{Read, Write};
+
+use curl::easy::{Easy, WriteError};
+use curl::easy;
+
+pub struct WebContext {
+    pub cookie_file_path: String,
+    pub lynx_config_path: String,
+    pub tmpdir: tempfile::TempDir,
+    // TODO implement drop? Should not be necessary
+}
+
+impl WebContext {
+    pub fn new(cookie_file_path: String, lynx_config_path: String) -> io::Result<WebContext> {
+        return Ok(WebContext {
+            cookie_file_path,
+            lynx_config_path,
+            tmpdir: tempfile::Builder::new().suffix("aoc").rand_bytes(6).tempdir()?,
+        });
+    }
+
+    pub fn close(self) -> io::Result<()> {
+        self.tmpdir.close()
+    }
+
+    pub fn lynx(&mut self, address: String) -> io::Result<()> {
+        let args = &[
+            format!("-cfg={}", self.lynx_config_path),
+            format!("-cookie_file={}", self.cookie_file_path),
+            // TODO cmd-file
+            address,
+        ];
+
+        process::Command::new("lynx")
+            .args(args)
+            .status()?;
+        
+        Ok(())
+    }
+
+    pub fn curl_base(&mut self) -> Result<Easy, Box<dyn error::Error>> {
+        let mut cookie_store = String::new();
+        fs::File::open(&self.cookie_file_path)?.read_to_string(&mut cookie_store)?;
+
+        let mut request = Easy::new();
+        request.follow_location(true).and_then(|_|
+            request.cookie_list(&cookie_store)
+        )?;
+        return Ok(request);
+    }
+
+    pub fn curl_request<F>(&mut self, url: &str, f: F) -> Result<(), Box<dyn error::Error>>
+        where F: FnMut(&[u8]) -> Result<usize, WriteError> + Send + 'static {
+        let mut request = self.curl_base()?;
+        request.url(url)?;
+        request.write_function(f)?;
+        request.perform()?;
+        return Ok(());
+    }
+
+    pub fn curl_post<F>(&mut self, url: &str, data: &[u8], f: F) -> Result<(), Box<dyn error::Error>>
+        where F: FnMut(&[u8]) -> Result<usize, WriteError> + Send + 'static {
+        let mut request = self.curl_base()?;
+        request.url(url)?;
+        request.write_function(f)?;
+        request.post(true)?;
+        request.post_fields_copy(data)?;
+        let mut headers = easy::List::new();
+        headers.append("Content-Type: application/x-www-form-urlencoded")?;
+        request.http_headers(headers)?;
+        request.perform()?;
+        return Ok(());
+    }
+
+    pub fn curl_request_to_file(&mut self, url: &str, mut file: fs::File) -> Result<(), Box<dyn error::Error>> {
+        self.curl_request(url, move |data| file.write(data).map_err(|_| easy::WriteError::Pause)).map_err(Into::into)
+    }
+
+    pub fn curl_request_to_named_file<P: AsRef<path::Path>>(&mut self, url: &str, path: P) -> Result<(), Box<dyn error::Error>> {
+        self.curl_request_to_file(url, fs::File::create(path)?)
+    }
+
+    pub fn curl_post_to_lynx(&mut self, url: &str, data: &[u8]) -> Result<(), Box<dyn error::Error>> {
+        let mut path = path::PathBuf::from(self.tmpdir.path());
+        path.push("result.html");
+        {
+            let mut file = fs::File::create(&path)?;
+            self.curl_post(url, data, move |data: &[u8]| {
+                file.write(data).map_err(|_| WriteError::Pause)
+            })?;
+        }
+        self.lynx(path.to_str().unwrap().to_owned())?;
+        return Ok(());
+    }
+}

+ 16 - 0
templates/day.rs.tpl

@@ -0,0 +1,16 @@
+use aoc2022_niels_overkamp::common::{self, AOCResult};
+
+const DAY: &str = "{{.day}}";
+
+fn main() -> Result<(), Box<dyn std::error::Error>> {
+    common::run(DAY, &run)
+}
+
+pub fn run(input: &Vec<String>) -> AOCResult {
+    Ok([None, None])
+}
+
+#[test]
+pub fn test_{{.day}}() {
+    assert!(common::run_test(DAY, &run))
+}

+ 0 - 0
test_inputs/day1.txt


+ 4 - 0
web/lynx.cfg

@@ -0,0 +1,4 @@
+INCLUDE:/etc/lynx/lynx.cfg
+SET_COOKIES:TRUE
+ACCEPT_ALL_COOKIES:TRUE
+PERSISTENT_COOKIES:TRUE

+ 10 - 0
web/scroll.script

@@ -0,0 +1,10 @@
+key ^N
+key ^N
+key ^N
+key ^N
+key ^N
+key ^N
+key ^N
+key ^N
+key ^N
+key ^N