Compare commits

..

1 Commits

Author SHA1 Message Date
dc0f2ce9ba Merge old-molehole 2024-07-23 17:48:28 -04:00
40 changed files with 1195 additions and 1485 deletions

794
Cargo.lock generated
View File

@ -1,794 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "addr2line"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb"
dependencies = [
"gimli",
]
[[package]]
name = "adler"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "ahash"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
dependencies = [
"cfg-if",
"once_cell",
"version_check",
"zerocopy",
]
[[package]]
name = "allocator-api2"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5"
[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "backtrace"
version = "0.3.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837"
dependencies = [
"addr2line",
"cc",
"cfg-if",
"libc",
"miniz_oxide",
"object",
"rustc-demangle",
]
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf"
[[package]]
name = "cassowary"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
[[package]]
name = "castaway"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a17ed5635fc8536268e5d4de1e22e81ac34419e5f052d4d51f4e01dcc263fcc"
dependencies = [
"rustversion",
]
[[package]]
name = "cc"
version = "1.0.90"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "color-eyre"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a667583cca8c4f8436db8de46ea8233c42a7d9ae424a82d338f2e4675229204"
dependencies = [
"backtrace",
"color-spantrace",
"eyre",
"indenter",
"once_cell",
"owo-colors",
"tracing-error",
]
[[package]]
name = "color-spantrace"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd6be1b2a7e382e2b98b43b2adcca6bb0e465af0bdd38123873ae61eb17a72c2"
dependencies = [
"once_cell",
"owo-colors",
"tracing-core",
"tracing-error",
]
[[package]]
name = "compact_str"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f"
dependencies = [
"castaway",
"cfg-if",
"itoa",
"ryu",
"static_assertions",
]
[[package]]
name = "crossterm"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df"
dependencies = [
"bitflags 2.4.2",
"crossterm_winapi",
"libc",
"mio",
"parking_lot",
"signal-hook",
"signal-hook-mio",
"winapi",
]
[[package]]
name = "crossterm_winapi"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
dependencies = [
"winapi",
]
[[package]]
name = "either"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a"
[[package]]
name = "eyre"
version = "0.6.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec"
dependencies = [
"indenter",
"once_cell",
]
[[package]]
name = "form_urlencoded"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
dependencies = [
"percent-encoding",
]
[[package]]
name = "gimli"
version = "0.28.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
[[package]]
name = "hashbrown"
version = "0.14.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
dependencies = [
"ahash",
"allocator-api2",
]
[[package]]
name = "heck"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]]
name = "idna"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
dependencies = [
"unicode-bidi",
"unicode-normalization",
]
[[package]]
name = "indenter"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683"
[[package]]
name = "indoc"
version = "2.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8"
[[package]]
name = "itertools"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
[[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.153"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
[[package]]
name = "lock_api"
version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45"
dependencies = [
"autocfg",
"scopeguard",
]
[[package]]
name = "log"
version = "0.4.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
[[package]]
name = "lru"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc"
dependencies = [
"hashbrown",
]
[[package]]
name = "memchr"
version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149"
[[package]]
name = "miniz_oxide"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7"
dependencies = [
"adler",
]
[[package]]
name = "mio"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
dependencies = [
"libc",
"log",
"wasi",
"windows-sys",
]
[[package]]
name = "molehole"
version = "0.1.0"
dependencies = [
"color-eyre",
"crossterm",
"eyre",
"ratatui",
"url",
]
[[package]]
name = "object"
version = "0.32.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441"
dependencies = [
"memchr",
]
[[package]]
name = "once_cell"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "owo-colors"
version = "3.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f"
[[package]]
name = "parking_lot"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
dependencies = [
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.9.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
"windows-targets",
]
[[package]]
name = "paste"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
[[package]]
name = "percent-encoding"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "pin-project-lite"
version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58"
[[package]]
name = "proc-macro2"
version = "1.0.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
dependencies = [
"proc-macro2",
]
[[package]]
name = "ratatui"
version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bcb12f8fbf6c62614b0d56eb352af54f6a22410c3b079eb53ee93c7b97dd31d8"
dependencies = [
"bitflags 2.4.2",
"cassowary",
"compact_str",
"crossterm",
"indoc",
"itertools",
"lru",
"paste",
"stability",
"strum",
"unicode-segmentation",
"unicode-width",
]
[[package]]
name = "redox_syscall"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
dependencies = [
"bitflags 1.3.2",
]
[[package]]
name = "rustc-demangle"
version = "0.1.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
[[package]]
name = "rustversion"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4"
[[package]]
name = "ryu"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "sharded-slab"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
dependencies = [
"lazy_static",
]
[[package]]
name = "signal-hook"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801"
dependencies = [
"libc",
"signal-hook-registry",
]
[[package]]
name = "signal-hook-mio"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af"
dependencies = [
"libc",
"mio",
"signal-hook",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1"
dependencies = [
"libc",
]
[[package]]
name = "smallvec"
version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7"
[[package]]
name = "stability"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebd1b177894da2a2d9120208c3386066af06a488255caabc5de8ddca22dbc3ce"
dependencies = [
"quote",
"syn 1.0.109",
]
[[package]]
name = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "strum"
version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "723b93e8addf9aa965ebe2d11da6d7540fa2283fcea14b3371ff055f7ba13f5f"
dependencies = [
"strum_macros",
]
[[package]]
name = "strum_macros"
version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a3417fc93d76740d974a01654a09777cb500428cc874ca9f45edfe0c4d4cd18"
dependencies = [
"heck",
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.52",
]
[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "2.0.52"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thread_local"
version = "1.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c"
dependencies = [
"cfg-if",
"once_cell",
]
[[package]]
name = "tinyvec"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
dependencies = [
"tinyvec_macros",
]
[[package]]
name = "tinyvec_macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tracing"
version = "0.1.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
dependencies = [
"pin-project-lite",
"tracing-core",
]
[[package]]
name = "tracing-core"
version = "0.1.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
dependencies = [
"once_cell",
"valuable",
]
[[package]]
name = "tracing-error"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d686ec1c0f384b1277f097b2f279a2ecc11afe8c133c1aabf036a27cb4cd206e"
dependencies = [
"tracing",
"tracing-subscriber",
]
[[package]]
name = "tracing-subscriber"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b"
dependencies = [
"sharded-slab",
"thread_local",
"tracing-core",
]
[[package]]
name = "unicode-bidi"
version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75"
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "unicode-normalization"
version = "0.1.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5"
dependencies = [
"tinyvec",
]
[[package]]
name = "unicode-segmentation"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
[[package]]
name = "unicode-width"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85"
[[package]]
name = "url"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633"
dependencies = [
"form_urlencoded",
"idna",
"percent-encoding",
]
[[package]]
name = "valuable"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
[[package]]
name = "version_check"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[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-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.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "zerocopy"
version = "0.7.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.7.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.52",
]

View File

@ -1,13 +0,0 @@
[package]
name = "molehole"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
color-eyre = "0.6.2"
crossterm = "0.27.0"
eyre = "0.6.12"
ratatui = "0.26.1"
url = "2.5.0"

32
Makefile Executable file
View File

@ -0,0 +1,32 @@
NAME=molehole
SRCDIR=src
OBJDIR=obj
BUILDDIR=build
SOURCES=$(shell find $(SRCDIR) -type f -name '*.c')
OBJSOURCES=$(patsubst %.c,$(OBJDIR)/%.o,$(notdir $(SOURCES)))
CC=clang
CFLAGS=-Wall -Wextra -pedantic -O0 -g
LDFLAGS=-lncurses -lssl -lc
INCLUDEFLAGS=-Iinclude
VPATH=$(dir $(SOURCES))
$(BUILDDIR)/$(NAME): $(OBJSOURCES) | $(BUILDDIR)
$(CC) -o $@ $^ $(CFLAGS) $(LDFLAGS)
$(OBJDIR)/%.o: %.c | $(OBJDIR)
$(CC) -c -o $@ $< $(CFLAGS) $(INCLUDEFLAGS)
$(BUILDDIR):
mkdir -p $(BUILDDIR)
$(OBJDIR):
mkdir -p $(OBJDIR)
.PHONY: clean
clean:
rm -rfv $(OBJDIR)
rm -rfv $(BUILDDIR)

15
README Normal file → Executable file
View File

@ -1,10 +1,13 @@
===
Molehole
========
===
Molehole is a (WIP) client for the Molerat protocol [0].
It is implemented in rust using ratatui [1].
Molehole is a WIP client for the Molerat[0] protocol.
It does not currently function, but it is being actively developed.
Contributions are welcome, please send them to molerat@git.trinket.icu
TODO:
- Implement mtxt parser
- Implement display
- Implement response code handler
[0]: https://trkt.in/molerat
[1]: https://ratatui.rs
[0]: http://trkt.in/molerat

10
compile_flags.txt Normal file
View File

@ -0,0 +1,10 @@
-Wall
-Wextra
-pedantic
-Iinclude
-I/nix/store/vgxb7a4zdrqv01wikdjz106v9rd8bnrd-openssl-3.0.13-dev/include
-I/nix/store/y7syj9lr1h9r2b1nx2pk9fdqyc76ag58-ncurses-6.4-dev/include
-lncurses
-lssl

View File

@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1709479366,
"narHash": "sha256-n6F0n8UV6lnTZbYPl1A9q1BS0p4hduAv1mGAP17CVd0=",
"lastModified": 1709237383,
"narHash": "sha256-cy6ArO4k5qTx+l5o+0mL9f5fa86tYUX3ozE1S+Txlds=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "b8697e57f10292a6165a20f03d2f42920dfaf973",
"rev": "1536926ef5621b09bba54035ae2bb6d806d72ac8",
"type": "github"
},
"original": {

View File

@ -10,17 +10,42 @@
system = "aarch64-darwin";
pkgs = nixpkgs.legacyPackages.${system};
in {
packages.${system}.default =
pkgs.stdenv.mkDerivation {
name = "molehole";
src = ./.;
buildInputs = with pkgs; [ncurses openssl];
installPhase = ''
mkdir -p $out/bin
mv build/molehole $out/bin
'';
};
devShells.${system}.default =
pkgs.mkShell {
buildInputs = with pkgs; [
rustc
rustfmt
cargo
rust-analyzer
libiconv
clippy
llvmPackages.clang
neovim
openssl.dev
ncurses.dev
];
shellHook = ''
cat << EOF > compile_flags.txt
-Wall
-Wextra
-pedantic
-Iinclude
-I${pkgs.openssl.dev}/include
-I${pkgs.ncurses.dev}/include
-lncurses
-lssl
EOF
exec zsh
'';
};

18
include/color.h Executable file
View File

@ -0,0 +1,18 @@
#ifndef _COLOR_H_
#define _COLOR_H_
#include <ncurses.h>
/**
* Initialize color pairs for the rest of the application.
*/
void set_colors(void);
enum StatusColorPairs { STATUS_MAIN = 1, STATUS_ERROR, STATUS_PROMPT };
enum StatusColors {
COLOR_GREY = 238,
COLOR_DIM_WHITE = 250,
COLOR_DIM_RED = 160
};
#endif

56
include/config.h Executable file
View File

@ -0,0 +1,56 @@
#ifndef _CONFIG_H_
#define _CONFIG_H_
#include <ncurses.h>
#include <openssl/ssl.h>
#include "response.h"
#include "url.h"
/**
* Molehole internal setting.
*
* Holds important information for the molehole runtime like terminal
* dimensions, and active ncurses windows.
*/
struct config_internal {
int height;
int width;
WINDOW *page_win;
WINDOW *status_win;
};
/**
* Molehole current state information.
*
* Holds information pertaining to the content displayed such as the current
* page url.
*/
struct config_state {
char *url_string;
struct url *url;
struct connection *conn;
struct response *res;
};
/**
* Molehole configuration.
*
* Holds the state of the program including user configuration.
*/
struct config {
struct config_internal i;
struct config_state s;
};
/**
* Initializes a config with default parameters.
*/
void init_config(struct config *conf);
/**
* Cleans up a config state and internal.
*/
void conf_cleanup(struct config *conf);
#endif

44
include/connect.h Executable file
View File

@ -0,0 +1,44 @@
#ifndef _CONNECT_H_
#define _CONNECT_H_
#include <openssl/ssl.h>
#include "config.h"
#include "url.h"
/* Error codes */
enum ConnectError {
ERR_GETADDRINFO = -1,
ERR_SOCKET = -2,
ERR_CONNECT = -3,
ERR_SSL_CTX = -4,
ERR_SSL_SSL = -5,
};
/**
* Holds all needed information to manage an open TLS connection.
*/
struct connection {
SSL *ssl;
int sockfd;
bool used;
};
/**
* Connect to molerat server.
*
* Sets `*ssl` in `struct config *conf` to the current active connection.
*/
int tls_connect(struct config *conf, struct url url);
/**
* Disconnect and free open connection
*/
void tls_cleanup(struct connection *conn);
/**
* Allocate a new instance of a `struct connection`
*/
struct connection *init_connection(void);
#endif

9
include/molerat.h Executable file
View File

@ -0,0 +1,9 @@
#ifndef _MOLERAT_H_
#define _MOLERAT_H_
struct key {
char* key;
char* value;
};
#endif

24
include/net.h Executable file
View File

@ -0,0 +1,24 @@
#ifndef _NET_H_
#define _NET_H_
#include "config.h"
#include "request.h"
#include "response.h"
/**
* Sends a `struct request` to the current `struct connection` contained within
* `struct config *conf`.
*/
int send_request(struct config *conf, struct request *req);
/**
* Reads a response from the current `struct connection` and parses it into
* `struct response *res`
*/
int read_response(struct config *conf, struct response *res);
enum NetError {
SSL_SEND_ERROR = -1,
ALLOC_ERROR = -2,
RESPONSE_PARSE_ERROR = -3,
};
#endif

22
include/request.h Executable file
View File

@ -0,0 +1,22 @@
#ifndef _REQUEST_H_
#define _REQUEST_H_
#include "molerat.h"
#include "url.h"
enum RequestKind { GET, PUT, DEL };
struct request {
enum RequestKind kind;
struct url url;
struct key *keys;
};
/**
* Convert a `struct request` to a string of chars that can be sent over the
* network.
*
* WARNING: Printing the screen won't work because the request ends in two
* \r\n's
*/
char *request_to_string(struct request *request);
#endif

50
include/response.h Executable file
View File

@ -0,0 +1,50 @@
#ifndef _RESPONSE_H_
#define _RESPONSE_H_
enum ResponseStatus {
SUCCESS = 10,
CONTENT_UNCHANGED = 11,
PERMANENT_REDIRECT = 20,
TEMPORARY_REDIRECT = 21,
MALFORMED_REQUEST = 30,
INVALID_REQUEST = 31,
NOT_AVAILABLE = 32,
INTERNAL_ERROR = 40,
NOT_SUPPORTED = 41,
SLOW_DOWN = 42,
CLIENT_CERTIFICATE_REQUIRED = 50
};
struct mime_type {
char *type;
char *subtype;
};
struct response {
enum ResponseStatus status;
char *message;
struct mime_type type;
unsigned int length;
char *hash;
char *content;
};
/**
* Parses a string into a `struct response`.
*/
int parse_response(struct response *res, char *s);
/**
* Deallocates `struct response *res`.
*/
void free_response(struct response *res);
enum ResponseError {
INVALID_STATUS = -1,
UNKNOWN_KEY = -2,
};
#endif

27
include/status.h Executable file
View File

@ -0,0 +1,27 @@
#ifndef _STATUS_H_
#define _STATUS_H_
#include "config.h"
/**
* Allocate a status window at the bottom of the terminal
*
* Saves the status window into the `*conf` parameter.
*/
void init_status(struct config *conf);
/**
* Set the message currently displayed in the status window.
*/
void update_status(struct config *conf, char *s);
/**
* Set the status to prompt for a Molerat URL. The raw string recieved will be.
* placed in `*conf`
*/
void prompt_status_url(struct config *conf);
/**
* Same as `update_status` but signify error.
*/
void error_status(struct config *conf, char *s);
#endif

42
include/url.h Executable file
View File

@ -0,0 +1,42 @@
#ifndef _URL_H_
#define _URL_H_
#define MAX_URL_LENGTH 2048
/**
* Molerat URL.
*
* Holds all relevant for molehole to make requests.
*/
struct url {
char *scheme;
char *host;
int port;
char *path;
char *query;
char *fragment;
};
/**
* Parses a `*s` into `*url` as a molerat URL.
*/
int parse_url(struct url *url, char *s);
/**
* Deallocates `struct url *url`.
*/
void free_url(struct url *url);
/**
* Initializes a `struct url*` with sensible defaults.
*/
struct url *init_url(void);
/**
* Get's the strlen of a `struct url*`
*/
int len_url(struct url *url);
/* Error codes */
enum UrlError { INVALID_CHARACTER = -1, MISSING_HOST = -2, MISSING_PORT = -3 };
#endif

11
include/util.h Executable file
View File

@ -0,0 +1,11 @@
#ifndef _UTIL_H_
#define _UTIL_H_
#include <stdnoreturn.h>
/**
* Kill program by printing message `*s` and errno.
*
* Shuts down ncurses before killing program.
*/
noreturn void die(const char *s);
#endif

View File

@ -1 +0,0 @@
max_width = 80

View File

@ -1,154 +0,0 @@
use crossterm::event::Event;
use eyre::Result;
use ratatui::prelude::*;
use std::time::Duration;
use crate::app_action::AppAction;
use crate::app_event::AppEvent;
use crate::component::Component;
use crate::components;
use crate::keys::key_commands::KeyCommand;
use crate::tui;
pub struct App {
pub tui: tui::Tui,
pub tick_rate: Duration,
pub components: Vec<Box<dyn Component>>,
pub key_commands: Vec<KeyCommand>,
should_quit: bool,
}
impl App {
pub fn new(tick_rate: Duration) -> Result<Self> {
let tui = tui::init()?;
Ok(Self {
tui,
tick_rate,
should_quit: false,
components: vec![],
key_commands: vec![],
})
}
pub fn run(&mut self) -> Result<()> {
let global_keys = components::global_keys::GlobalKeys {
key_commands: self.key_commands.clone(),
..Default::default()
};
let status_bar = components::status::StatusBar {
message: "Press '?' to show the help menu".to_string(),
..Default::default()
};
let url_manager = components::url_manager::UrlManager::default();
self.components = vec![
Box::new(global_keys),
Box::new(status_bar),
Box::new(url_manager),
];
for component in &mut self.components {
component.init()?;
}
loop {
if self.should_quit {
break Ok(());
}
self.draw()?;
}
}
pub fn draw(&mut self) -> Result<()> {
let event: Option<AppEvent> = match tui::get_event(self.tick_rate)? {
Some(event) => match event {
Event::Key(key) => Some(AppEvent::Key(key)),
Event::Mouse(mouse) => Some(AppEvent::Mouse(mouse)),
Event::FocusGained => todo!(),
Event::FocusLost => todo!(),
Event::Paste(_) => todo!(),
Event::Resize(_, _) => todo!(),
},
None => None,
};
if let Some(event) = event {
self.handle_event(event)?;
}
if self.should_quit {
return Ok(());
}
self.tui.draw(|frame| {
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![
Constraint::Percentage(100),
Constraint::Min(1),
])
.split(frame.size());
// status bar
let _ = self.components[1].render(frame, layout[1]);
// global_keys
let _ = self.components[0].render(frame, frame.size());
})?;
self.update()?;
Ok(())
}
pub fn update(&mut self) -> Result<()> {
let mut events: Vec<AppEvent> = vec![];
for component in &mut self.components {
if let Some(event) = component.update() {
events.push(event);
}
}
for event in events {
self.handle_event(event)?;
}
Ok(())
}
pub fn quit(&mut self) -> Result<()> {
tui::restore()?;
self.should_quit = true;
Ok(())
}
fn handle_action(&mut self, action: AppAction) -> Result<()> {
match action {
AppAction::Quit => Ok(self.quit()?),
_ => {
for component in &mut self.components {
component.handle_action(action.clone());
}
Ok(())
}
}
}
fn handle_event(&mut self, event: AppEvent) -> Result<()> {
let mut actions: Vec<AppAction> = vec![];
for component in &mut self.components {
if let Some(action) = component.handle_event(event.clone()) {
actions.push(action);
}
}
for action in actions {
self.handle_action(action)?;
}
Ok(())
}
}

View File

@ -1,24 +0,0 @@
use std::fmt;
#[derive(Default, Clone, Debug)]
pub enum AppAction {
StatusBarSetMessage(String),
StatusBarSetError(String),
OpenUrl,
ScrollUp,
ScrollDown,
ScrollTop,
ScrollBottom,
ShowHelpMenu,
#[default]
Quit,
}
impl fmt::Display for AppAction {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{:?}", self)
}
}

View File

@ -1,10 +0,0 @@
use crossterm::event::{KeyEvent, MouseEvent};
use url::Url;
#[derive(Clone)]
pub enum AppEvent {
Key(KeyEvent),
Mouse(MouseEvent),
OpenUrl(Url),
}

View File

@ -1,43 +0,0 @@
use crossterm::event::{KeyEvent, MouseEvent};
use eyre::Result;
use ratatui::prelude::{Frame, Rect};
use crate::app_action::AppAction;
use crate::app_event::AppEvent;
pub trait Component {
fn init(&mut self) -> Result<()> {
Ok(())
}
#[allow(unused)]
fn handle_action(&mut self, action: AppAction) {}
#[allow(unused)]
fn handle_event(&mut self, event: AppEvent) -> Option<AppAction> {
match event {
AppEvent::Key(key_event) => self.handle_key_event(key_event),
AppEvent::Mouse(mouse_event) => {
self.handle_mouse_event(mouse_event)
}
_ => None,
}
}
#[allow(unused)]
fn handle_key_event(&mut self, key: KeyEvent) -> Option<AppAction> {
None
}
#[allow(unused)]
fn handle_mouse_event(&mut self, mouse: MouseEvent) -> Option<AppAction> {
None
}
#[allow(unused)]
fn update(&mut self) -> Option<AppEvent> {
None
}
fn render(&mut self, frame: &mut Frame, rect: Rect) -> Result<()>;
}

View File

@ -1,131 +0,0 @@
use crossterm::event::{KeyEvent, KeyEventKind};
use ratatui::prelude::{
Alignment, Color, Constraint, Direction, Frame, Layout, Line, Margin, Rect,
Span, Style, Stylize,
};
use ratatui::widgets::block::{Block, BorderType, Title};
use ratatui::widgets::{
Borders, Clear, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState,
Wrap,
};
use crate::app_action::AppAction;
use crate::component::Component;
use crate::keys::key_commands::{serialize_key_event, KeyCommand};
#[derive(Default)]
pub struct GlobalKeys {
pub key_commands: Vec<KeyCommand>,
pub should_show: bool,
pub scroll: usize,
pub scroll_state: ScrollbarState,
}
impl Component for GlobalKeys {
fn init(&mut self) -> eyre::Result<()> {
self.scroll_state =
ScrollbarState::new(self.key_commands.len()).position(self.scroll);
Ok(())
}
fn handle_action(&mut self, action: AppAction) {
match action {
AppAction::ScrollUp => {
if self.scroll > 0 {
self.scroll -= 1;
}
}
AppAction::ScrollDown => {
if self.scroll < self.key_commands.len() - 1 {
self.scroll += 1;
}
}
AppAction::ScrollTop => {
self.scroll = 0;
}
AppAction::ScrollBottom => {
self.scroll = self.key_commands.len() - 1;
}
AppAction::ShowHelpMenu => {
self.should_show = !self.should_show;
self.scroll = 0;
}
_ => {}
}
self.scroll_state = self.scroll_state.position(self.scroll);
}
fn handle_key_event(&mut self, key: KeyEvent) -> Option<AppAction> {
if key.kind == KeyEventKind::Press {
let key_event = serialize_key_event(key);
for key_command in &mut self.key_commands {
if key_command.key_code == key_event {
return Some(key_command.action.clone());
}
}
}
None
}
fn render(&mut self, frame: &mut Frame, rect: Rect) -> eyre::Result<()> {
let vertical_center = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Percentage(50 / 2),
Constraint::Percentage(50),
Constraint::Percentage(50 / 2),
])
.split(rect);
let center = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage(50 / 2),
Constraint::Percentage(50),
Constraint::Percentage(50 / 2),
])
.split(vertical_center[1])[1];
let block = Block::default()
.title(
Title::from("Keyboard shortcuts").alignment(Alignment::Center),
)
.borders(Borders::ALL)
.border_type(BorderType::Thick)
.style(Style::default().bg(Color::DarkGray));
let mut lines: Vec<Line> = vec![];
for key_command in &mut self.key_commands {
let command = Span::from(key_command.key_code.clone());
let description =
Span::from(key_command.description.clone()).italic();
let spacer = Span::from(" ");
let line = Line::from(vec![command, spacer, description]);
lines.push(line);
}
let commands = Paragraph::new(lines)
.block(block)
.wrap(Wrap { trim: true })
.scroll((u16::try_from(self.scroll)?, 0));
if self.should_show {
frame.render_widget(Clear, center);
frame.render_widget(commands, center);
frame.render_stateful_widget(
Scrollbar::new(ScrollbarOrientation::VerticalRight),
center.inner(&Margin {
vertical: 1,
horizontal: 0,
}),
&mut self.scroll_state,
);
}
Ok(())
}
}

View File

@ -1,3 +0,0 @@
pub mod global_keys;
pub mod status;
pub mod url_manager;

View File

@ -1,86 +0,0 @@
use ratatui::prelude::*;
use ratatui::widgets::*;
use crate::app_action::AppAction;
use crate::app_event::AppEvent;
use crate::component::Component;
use crate::keys::key_commands::serialize_key_event;
#[derive(Default, Clone)]
pub struct StatusBar {
pub message: String,
pub current_key: String,
pub error: bool,
pub url_to_open: Option<url::Url>,
}
impl Component for StatusBar {
fn handle_key_event(
&mut self,
key: crossterm::event::KeyEvent,
) -> Option<AppAction> {
let key_str = serialize_key_event(key);
self.current_key = key_str;
None
}
fn handle_action(&mut self, action: crate::app_action::AppAction) {
match action.clone() {
AppAction::StatusBarSetMessage(message) => {
self.error = false;
self.message = message;
}
AppAction::StatusBarSetError(message) => {
self.error = true;
self.message = message;
}
AppAction::OpenUrl => {
self.url_to_open =
Some(url::Url::parse("molerat://example.com").unwrap());
}
_ => {}
}
}
fn update(&mut self) -> Option<AppEvent> {
if let Some(url) = &self.url_to_open {
let event = AppEvent::OpenUrl(url.clone());
self.url_to_open = None;
return Some(event);
}
None
}
fn render(
&mut self,
frame: &mut ratatui::prelude::Frame,
rect: ratatui::prelude::Rect,
) -> eyre::Result<()> {
let block =
Block::default().style(Style::default().bg(if self.error {
Color::Red
} else {
Color::DarkGray
}));
let layout = Layout::default()
.direction(Direction::Horizontal)
.constraints(vec![
Constraint::Percentage(50),
Constraint::Percentage(50),
])
.split(rect);
let message = Paragraph::new(self.message.clone()).block(block.clone());
let current_key = Paragraph::new(self.current_key.clone())
.block(block)
.alignment(Alignment::Right);
frame.render_widget(message, layout[0]);
frame.render_widget(current_key, layout[1]);
Ok(())
}
}

View File

@ -1,34 +0,0 @@
use url::Url;
use crate::app_action::AppAction;
use crate::app_event::AppEvent;
use crate::component::Component;
#[derive(Default)]
pub struct UrlManager {
url: Option<Url>,
}
impl Component for UrlManager {
fn handle_event(&mut self, event: AppEvent) -> Option<AppAction> {
match event {
AppEvent::OpenUrl(url) => {
self.url = Some(url.clone());
return Some(AppAction::StatusBarSetMessage(format!(
"Opening {}",
url.as_str()
)));
}
_ => {}
}
None
}
fn render(
&mut self,
_frame: &mut ratatui::prelude::Frame,
_rect: ratatui::prelude::Rect,
) -> eyre::Result<()> {
Ok(())
}
}

18
src/config.c Executable file
View File

@ -0,0 +1,18 @@
#include "config.h"
#include "connect.h"
#include "response.h"
void init_config(struct config *conf) {
conf->s.url = NULL;
conf->s.conn = NULL;
conf->s.res = NULL;
}
void conf_cleanup(struct config *conf) {
if (conf->s.url != NULL)
free_url(conf->s.url);
if (conf->s.conn != NULL)
tls_cleanup(conf->s.conn);
if (conf->s.res != NULL)
free_response(conf->s.res);
}

View File

@ -1,55 +0,0 @@
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use crate::app_action::AppAction;
#[derive(Default, Clone)]
pub struct KeyCommand {
pub key_code: String,
pub description: String,
pub action: AppAction,
}
impl std::fmt::Display for KeyCommand {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}\t{}", self.key_code, self.description)
}
}
pub fn serialize_key_event(event: KeyEvent) -> String {
let mut modifiers = Vec::with_capacity(3);
if event.modifiers.intersects(KeyModifiers::CONTROL) {
modifiers.push("ctrl");
}
if event.modifiers.intersects(KeyModifiers::SUPER)
|| event.modifiers.intersects(KeyModifiers::HYPER)
|| event.modifiers.intersects(KeyModifiers::META)
{
modifiers.push("super");
}
if event.modifiers.intersects(KeyModifiers::ALT) {
modifiers.push("alt");
}
let char;
let key = match event.code {
KeyCode::Backspace | KeyCode::Delete => "del",
KeyCode::Enter => "enter",
KeyCode::Left => "left",
KeyCode::Right => "right",
KeyCode::Up => "up",
KeyCode::Down => "down",
KeyCode::Tab => "tab",
KeyCode::Char(' ') => "space",
KeyCode::Char(c) => {
char = c.to_string();
&char
}
KeyCode::Esc => "esc",
_ => "",
};
let separator = if modifiers.is_empty() { "" } else { "-" };
let serialized_event =
format!("{}{}{}", modifiers.join("-"), separator, key);
serialized_event
}

View File

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

83
src/main.c Executable file
View File

@ -0,0 +1,83 @@
#include <ncurses.h>
#include <openssl/ssl.h>
#include <stdlib.h>
#include "color.h"
#include "config.h"
#include "connect.h"
#include "net.h"
#include "request.h"
#include "response.h"
#include "status.h"
#include "url.h"
#include "util.h"
void exit_cleanup(void) { endwin(); }
void init_ncurses(void) {
initscr();
cbreak();
noecho();
curs_set(0);
keypad(stdscr, TRUE);
scrollok(stdscr, TRUE);
if (!has_colors())
die("Terminal does not support colors");
start_color();
set_colors();
refresh();
}
int main(void) {
struct config conf;
init_config(&conf);
init_ncurses();
atexit(exit_cleanup);
getmaxyx(stdscr, conf.i.height, conf.i.width);
init_status(&conf);
conf.s.url = init_url();
conf.s.conn = init_connection();
prompt_status_url(&conf);
update_status(&conf, conf.s.url_string);
int rc = parse_url(conf.s.url, conf.s.url_string);
if (rc < 0) {
error_status(&conf, "Invalid URL");
conf_cleanup(&conf);
return 1;
}
rc = tls_connect(&conf, *conf.s.url);
if (rc < 0) {
conf_cleanup(&conf);
return 1;
}
struct request req;
req.kind = GET;
req.url = *conf.s.url;
send_request(&conf, &req);
struct response res;
rc = read_response(&conf, &res);
if (rc < 0) {
conf_cleanup(&conf);
return rc * -1;
}
conf.s.res = &res;
printw("%s\n", conf.s.res->content);
getch();
conf_cleanup(&conf);
return 0;
}

View File

@ -1,59 +0,0 @@
mod app;
mod app_action;
mod app_event;
mod component;
mod components;
mod keys;
mod tui;
use eyre::Result;
use app_action::AppAction;
use keys::key_commands::KeyCommand;
fn main() -> Result<()> {
tui::install_hooks()?;
let mut app = app::App::new(std::time::Duration::from_millis(10))?;
let mut key_commands = vec![
// Status bar
KeyCommand {
key_code: "o".to_string(),
description: "Open new link".to_string(),
action: AppAction::OpenUrl,
},
// Navigation
KeyCommand {
key_code: "g".to_string(),
description: "Scroll to top".to_string(),
action: AppAction::ScrollTop,
},
KeyCommand {
key_code: "G".to_string(),
description: "Scroll to bottom".to_string(),
action: AppAction::ScrollBottom,
},
KeyCommand {
key_code: "k".to_string(),
description: "Scroll up one line".to_string(),
action: AppAction::ScrollUp,
},
KeyCommand {
key_code: "j".to_string(),
description: "Scroll down one line".to_string(),
action: AppAction::ScrollDown,
},
KeyCommand {
key_code: "q".to_string(),
description: "Quit molehole".to_string(),
action: AppAction::Quit,
},
KeyCommand {
key_code: "?".to_string(),
description: "Toggle help menu".to_string(),
action: AppAction::ShowHelpMenu,
},
];
app.key_commands.append(&mut key_commands);
app.run()
}

133
src/molerat/connect.c Executable file
View File

@ -0,0 +1,133 @@
#include <errno.h>
#include <netdb.h>
#include <openssl/ssl.h>
#include <stdbool.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include "connect.h"
#include "status.h"
#include "url.h"
int connect_socket(struct config *conf, struct url url) {
struct addrinfo hints;
struct addrinfo *servinfo;
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE;
char port[5];
sprintf(port, "%d", url.port);
int rc = getaddrinfo(url.host, port, &hints, &servinfo);
if (rc != 0) {
error_status(conf, (char *)gai_strerror(rc));
return ERR_GETADDRINFO;
}
struct addrinfo *p;
int sockfd;
for (p = servinfo; p != NULL; p = p->ai_next) {
sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
if (sockfd == -1) {
continue;
}
rc = connect(sockfd, p->ai_addr, p->ai_addrlen);
if (rc == -1) {
close(sockfd);
continue;
}
break;
}
if (p == NULL) {
error_status(conf, strerror(errno));
return ERR_CONNECT;
}
update_status(conf, "Connected to socket");
freeaddrinfo(servinfo);
return sockfd;
}
int tls_connect(struct config *conf, struct url url) {
char status_msg[strlen(url.host) + 128];
sprintf(status_msg, "Connecting to %s...", url.host);
update_status(conf, status_msg);
struct connection *conn = init_connection();
conf->s.conn = conn;
int sock;
int rc = connect_socket(conf, url);
if (rc < 0) {
return rc;
} else {
sock = rc;
}
const SSL_METHOD *method;
method = TLS_method();
SSL_CTX *ctx;
ctx = SSL_CTX_new(method);
if (ctx == NULL) {
error_status(conf, "Failed to init SSL");
return ERR_SSL_CTX;
}
SSL *ssl;
ssl = SSL_new(ctx);
if (ssl == NULL) {
error_status(conf, "Failed to init SSL");
return ERR_SSL_SSL;
}
rc = SSL_set_fd(ssl, sock);
if (rc == 0) {
error_status(conf, "Failed to wrap socket");
return ERR_SSL_SSL;
}
rc = SSL_connect(ssl);
if (rc <= 0) {
error_status(conf, "Failed to connect SSL");
return ERR_SSL_SSL;
}
conn->ssl = ssl;
conn->sockfd = sock;
conn->used = true;
conf->s.conn = conn;
update_status(conf, "Connected");
return 0;
}
void tls_cleanup(struct connection *conn) {
if (conn->sockfd) {
shutdown(conn->sockfd, SHUT_RDWR);
close(conn->sockfd);
}
if (conn->ssl) {
SSL_shutdown(conn->ssl);
SSL_CTX_free(SSL_get_SSL_CTX(conn->ssl));
SSL_free(conn->ssl);
}
free(conn);
}
struct connection *init_connection(void) {
struct connection *conn = malloc(sizeof(struct connection));
memset(conn, 0, sizeof(struct connection));
return conn;
}

68
src/molerat/net.c Executable file
View File

@ -0,0 +1,68 @@
#include <openssl/ssl.h>
#include <string.h>
#include <time.h>
#include "config.h"
#include "connect.h"
#include "net.h"
#include "request.h"
#include "response.h"
#include "status.h"
int send_request(struct config *conf, struct request *req) {
char *req_string = request_to_string(req);
int rc = SSL_write(conf->s.conn->ssl, req_string, strlen(req_string));
if (rc < 0) {
return SSL_SEND_ERROR;
}
free(req_string);
return 0;
}
int read_response(struct config *conf, struct response *res) {
struct timespec start_time;
clock_gettime(CLOCK_MONOTONIC, &start_time);
int buf_len = 4096;
char *buf = malloc(buf_len);
int bytes_read = 0;
do {
bytes_read = SSL_read(conf->s.conn->ssl, buf, buf_len);
if (bytes_read == buf_len) {
buf_len *= 2;
char *temp_buf = realloc(buf, buf_len);
if (temp_buf == NULL)
return ALLOC_ERROR;
buf = temp_buf;
}
} while (bytes_read > 0);
int rc = parse_response(res, buf);
if (rc < 0) {
return RESPONSE_PARSE_ERROR;
}
free(buf);
struct timespec end_time;
clock_gettime(CLOCK_MONOTONIC, &end_time);
float time_diff = (end_time.tv_sec - start_time.tv_sec) +
1e-9 * (end_time.tv_nsec - start_time.tv_nsec);
int msg_len =
snprintf(NULL, 0, "Received after %0.3f seconds", time_diff) + 1;
char *msg = malloc(msg_len);
snprintf(msg, msg_len, "Received after %.3f seconds", time_diff);
update_status(conf, msg);
free(msg);
return 0;
}

34
src/molerat/request.c Executable file
View File

@ -0,0 +1,34 @@
#include <ncurses.h>
#include <stdlib.h>
#include <string.h>
#include "request.h"
#include "url.h"
char *get_request_kind(enum RequestKind kind) {
switch (kind) {
case GET:
return "get";
case PUT:
return "put";
case DEL:
return "del";
}
}
char *request_to_string(struct request *req) {
int len = sizeof(struct request) +
6; // +1 for null terminator +5 for request whitespace
char *buf = malloc(len);
char *kind = get_request_kind(req->kind);
char *host = req->url.host != NULL ? req->url.host : "";
char *path = req->url.path != NULL ? req->url.path : "";
char *query = req->url.query != NULL ? req->url.query : "";
char *fragment = req->url.fragment != NULL ? req->url.fragment : "";
snprintf(buf, len, "%s %s%s%s%s\r\n\r\n", kind, host, path, query, fragment);
return buf;
}

215
src/molerat/response.c Executable file
View File

@ -0,0 +1,215 @@
#include <stdlib.h>
#include <string.h>
#include "response.h"
#define SET_STR(segment) \
(segment) = malloc(i - start + 1); \
strncpy((segment), s + start, i - start);
#define MOVE(amount) \
i += (amount); \
start = i;
#define CHECK_AT_END() \
if (cur == '\0') { \
state = END; \
}
#define AT_DELIM (cur == '\t' && next == '\r' && next_next == '\n')
enum state {
STATUS,
MESSAGE,
MESSAGE_S,
TYPE_S,
TYPE,
LENGTH_S,
LENGTH,
HASH,
HASH_S,
CONTENT,
END
};
int parse_response(struct response *res, char *s) {
int i = 0;
int start = 0;
char cur;
char next;
char next_next;
int s_len = strlen(s);
enum state state = STATUS;
while (state != END) {
cur = s[i];
if (i < s_len)
next = s[i + 1];
if (i + 1 < s_len)
next_next = s[i + 2];
switch (state) {
case STATUS:
if (cur == '\r' && next == '\n') {
char *status_str;
SET_STR(status_str);
int status = atoi(status_str);
if (status > 0)
res->status = status;
else
return INVALID_STATUS;
free(status_str);
MOVE(2);
state = MESSAGE;
}
break;
case MESSAGE:
if (cur == ':') {
char *message_str;
SET_STR(message_str);
if (strncmp(message_str, "message", 7) != 0) {
MOVE(strlen(message_str) * -1);
state = TYPE;
break;
} else {
MOVE(1); // skip ':'
state = MESSAGE_S;
}
free(message_str);
}
break;
case MESSAGE_S:
if (AT_DELIM) {
char *message_str;
SET_STR(message_str);
res->message = message_str;
MOVE(3);
state = TYPE;
}
break;
case TYPE:
if (cur == ':') {
char *type_str;
SET_STR(type_str);
if (strncmp(type_str, "type", 4) != 0) {
MOVE(strlen(type_str) * -1);
state = LENGTH;
} else {
MOVE(1); // skip ':'
state = TYPE_S;
}
free(type_str);
}
break;
case TYPE_S:
if (cur == '/') {
SET_STR(res->type.type);
MOVE(1);
} else if (AT_DELIM) {
SET_STR(res->type.subtype);
MOVE(3);
state = LENGTH;
}
break;
case LENGTH:
if (cur == ':') {
char *length_str;
SET_STR(length_str);
if (strncmp(length_str, "length", 6) != 0) {
MOVE(strlen(length_str) * -1);
state = HASH;
} else {
MOVE(1); // skip ':'
state = LENGTH_S;
}
free(length_str);
}
case LENGTH_S:
if (AT_DELIM) {
char *length_str;
SET_STR(length_str);
res->length = atoi(length_str);
free(length_str);
MOVE(3);
state = HASH;
}
break;
case HASH:
if (cur == ':') {
char *hash_str;
SET_STR(hash_str);
if (strncmp(hash_str, "hash", 4) != 0) {
MOVE(strlen(hash_str) * -1);
free(hash_str);
return UNKNOWN_KEY;
} else {
MOVE(1); // skip ':'
state = HASH_S;
}
free(hash_str);
}
break;
case HASH_S:
if ((cur == '\r' && next == '\n') || AT_DELIM) {
char *hash_str;
SET_STR(hash_str);
res->hash = hash_str;
MOVE(4);
state = CONTENT;
}
case CONTENT:
if (res->length < 1) {
state = END;
break;
}
if (i == s_len - 1) {
char *content_str;
SET_STR(content_str);
res->content = content_str;
state = END;
}
case END:
break;
}
i++;
if (i == s_len) {
state = END;
}
}
return 0;
}
void free_response(struct response *res) {
free(res->message);
free(res->type.type);
free(res->type.subtype);
free(res->hash);
free(res->content);
}

181
src/molerat/url.c Executable file
View File

@ -0,0 +1,181 @@
#include <stdlib.h>
#include <string.h>
#include "url.h"
#define SET_STR(segment) \
(segment) = malloc(i - start + 1); \
strncpy((segment), s + start, i - start);
#define MOVE(amount) \
i += (amount); \
start = i;
#define SET_AT_END(segment) \
if (cur == '\0') { \
state = END; \
if (i - 1 > start) { \
SET_STR(segment); \
} \
}
enum state { SCHEME, HOST, PORT, PATH, QUERY, FRAGMENT, END };
int parse_url(struct url *url, char *s) {
int i = 0; // index into *s
int start = 0; // index of current mode start
enum state state = SCHEME;
char cur;
while (i < MAX_URL_LENGTH) {
cur = s[i];
if (cur == ' ')
return INVALID_CHARACTER;
switch (state) {
case SCHEME:
if (cur == ':') {
state = HOST;
SET_STR(url->scheme);
MOVE(3); // skip the '://'
}
SET_AT_END(url->scheme);
break;
case HOST:
if (cur == ':') {
state = PORT;
SET_STR(url->host);
MOVE(1);
}
if (cur == '/') {
state = PATH;
SET_STR(url->host);
MOVE(0);
}
SET_AT_END(url->host);
break;
case PORT:
if (cur == '/') {
state = PATH;
char *port;
SET_STR(port);
url->port = atoi(port);
MOVE(0);
}
if (cur == '\0') {
state = END;
char *port;
SET_STR(port);
url->port = atoi(port);
}
break;
case PATH:
if (cur == '?') {
state = QUERY;
SET_STR(url->path);
MOVE(0);
}
if (cur == '#') {
state = FRAGMENT;
SET_STR(url->path);
MOVE(0);
}
SET_AT_END(url->path);
break;
case QUERY:
if (cur == '#') {
state = FRAGMENT;
SET_STR(url->query);
MOVE(0);
}
SET_AT_END(url->query);
break;
case FRAGMENT:
if (cur == '\0') {
state = END;
SET_STR(url->fragment);
MOVE(0);
}
break;
case END:
break;
}
i++;
if (cur == '\0')
break;
if (state == END)
break;
}
if (url->host == NULL)
return MISSING_HOST;
if (url->port == 0)
return MISSING_PORT;
if (url->path == NULL) {
url->path = malloc(2);
strcpy(url->path, "/");
}
return 0;
}
struct url *init_url(void) {
struct url *url = malloc(sizeof(struct url));
url->scheme = NULL;
url->host = NULL;
url->port = 2693;
url->path = NULL;
url->query = NULL;
url->fragment = NULL;
return url;
}
void free_url(struct url *url) {
free(url->scheme);
free(url->host);
free(url->path);
free(url->fragment);
free(url->query);
free(url);
}
int len_url(struct url *url) {
int len = 0;
len += url->scheme != NULL ? strlen(url->scheme) : 0;
len += url->host != NULL ? strlen(url->host) : 0;
len += sizeof(url->port);
len += url->path != NULL ? strlen(url->path) : 0;
len += url->query != NULL ? strlen(url->query) : 0;
len += url->fragment != NULL ? strlen(url->fragment) : 0;
return len;
}

View File

@ -1,62 +0,0 @@
use crossterm::event::{
Event, KeyboardEnhancementFlags, PopKeyboardEnhancementFlags,
PushKeyboardEnhancementFlags,
};
use crossterm::terminal::{
disable_raw_mode, enable_raw_mode, EnterAlternateScreen,
LeaveAlternateScreen,
};
use crossterm::{event, execute};
use ratatui::prelude::{CrosstermBackend, Terminal};
use std::io;
use std::io::{stdout, Stdout};
use std::panic;
pub type Tui = Terminal<CrosstermBackend<Stdout>>;
pub fn init() -> io::Result<Tui> {
execute!(stdout(), EnterAlternateScreen)?;
execute!(
stdout(),
PushKeyboardEnhancementFlags(
KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES,
)
)?;
enable_raw_mode()?;
Terminal::new(CrosstermBackend::new(stdout()))
}
pub fn restore() -> io::Result<()> {
execute!(stdout(), LeaveAlternateScreen)?;
execute!(stdout(), PopKeyboardEnhancementFlags)?;
disable_raw_mode()?;
Ok(())
}
pub fn get_event(tick: std::time::Duration) -> io::Result<Option<Event>> {
if event::poll(tick)? {
return Ok(Some(event::read()?));
}
Ok(None)
}
pub fn install_hooks() -> eyre::Result<()> {
let hook_builder = color_eyre::config::HookBuilder::default();
let (panic_hook, eyre_hook) = hook_builder.into_hooks();
let panic_hook = panic_hook.into_panic_hook();
panic::set_hook(Box::new(move |panic_info| {
restore().unwrap();
panic_hook(panic_info);
}));
let eyre_hook = eyre_hook.into_eyre_hook();
eyre::set_hook(Box::new(move |error| {
restore().unwrap();
eyre_hook(error)
}))?;
Ok(())
}

10
src/ui/color.c Executable file
View File

@ -0,0 +1,10 @@
#include <ncurses.h>
#include "color.h"
void set_colors(void) {
/* fg color, bg color */
init_pair(STATUS_MAIN, COLOR_DIM_WHITE, COLOR_GREY);
init_pair(STATUS_ERROR, COLOR_DIM_WHITE, COLOR_DIM_RED);
init_pair(STATUS_PROMPT, COLOR_GREY, COLOR_DIM_WHITE);
}

54
src/ui/status.c Executable file
View File

@ -0,0 +1,54 @@
#include <ncurses.h>
#include <string.h>
#include "color.h"
#include "config.h"
#include "connect.h"
#include "status.h"
#include "url.h"
void init_status(struct config *conf) {
WINDOW *status = newwin(1, conf->i.width, conf->i.height - 1, 0);
conf->i.status_win = status;
update_status(conf, "");
}
void update_status(struct config *conf, char *s) {
werase(conf->i.status_win);
wbkgd(conf->i.status_win, COLOR_PAIR(STATUS_MAIN));
wprintw(conf->i.status_win, "%s", s);
wrefresh(conf->i.status_win);
}
void prompt_status_url(struct config *conf) {
echo();
curs_set(1);
werase(conf->i.status_win);
char prompt[] = "Enter a molerat URL: molerat://";
werase(conf->i.status_win);
wbkgd(conf->i.status_win, COLOR_PAIR(STATUS_PROMPT));
wprintw(conf->i.status_win, "%s", prompt);
wrefresh(conf->i.status_win);
char url_string[MAX_URL_LENGTH];
wgetstr(conf->i.status_win, url_string);
char url_string_with_scheme[MAX_URL_LENGTH] = "molerat://";
strlcat(url_string_with_scheme, url_string, MAX_URL_LENGTH);
conf->s.url_string = url_string_with_scheme;
curs_set(0);
noecho();
}
void error_status(struct config *conf, char *s) {
werase(conf->i.status_win);
wbkgd(conf->i.status_win, COLOR_PAIR(STATUS_ERROR));
wprintw(conf->i.status_win, "Error: %s [Press RETURN]", s);
wrefresh(conf->i.status_win);
getch();
}

11
src/util.c Executable file
View File

@ -0,0 +1,11 @@
#include <stdio.h>
#include <stdlib.h>
#include <stdnoreturn.h>
#include <ncurses.h>
noreturn void die(const char *s) {
endwin();
perror(s);
exit(EXIT_FAILURE);
}