diff --git a/.better-commits.json b/.better-commits.json new file mode 100644 index 0000000..880a06e --- /dev/null +++ b/.better-commits.json @@ -0,0 +1,175 @@ +{ + "check_status": true, + "commit_type": { + "enable": true, + "initial_value": "feat", + "infer_type_from_branch": true, + "append_emoji_to_label": true, + "append_emoji_to_commit": true, + "options": [ + { + "value": "feat", + "label": "feat", + "hint": "A new feature", + "emoji": "✨", + "trailer": "Changelog: feature" + }, + { + "value": "fix", + "label": "fix", + "hint": "A bug fix", + "emoji": "🐛", + "trailer": "Changelog: fix" + }, + { + "value": "docs", + "label": "docs", + "hint": "Documentation only changes", + "emoji": "📚", + "trailer": "Changelog: documentation" + }, + { + "value": "refactor", + "label": "refactor", + "hint": "A code change that neither fixes a bug nor adds a feature", + "emoji": "🔨", + "trailer": "Changelog: refactor" + }, + { + "value": "perf", + "label": "perf", + "hint": "A code change that improves performance", + "emoji": "🚀", + "trailer": "Changelog: performance" + }, + { + "value": "test", + "label": "test", + "hint": "Adding missing tests or correcting existing tests", + "emoji": "🚨", + "trailer": "Changelog: test" + }, + { + "value": "build", + "label": "build", + "hint": "Changes that affect the build system or external dependencies", + "emoji": "🚧", + "trailer": "Changelog: build" + }, + { + "value": "ci", + "label": "ci", + "hint": "Changes to our CI configuration files and scripts", + "emoji": "🤖", + "trailer": "Changelog: ci" + }, + { + "value": "chore", + "label": "chore", + "hint": "Other changes that do not modify src or test files", + "emoji": "🧹", + "trailer": "Changelog: chore" + }, + { + "value": "", + "label": "none" + } + ] + }, + "commit_scope": { + "enable": false, + "custom_scope": false, + "initial_value": "app", + "options": [ + { + "value": "app", + "label": "app" + }, + { + "value": "shared", + "label": "shared" + }, + { + "value": "server", + "label": "server" + }, + { + "value": "tools", + "label": "tools" + }, + { + "value": "", + "label": "none" + } + ] + }, + "check_ticket": { + "infer_ticket": true, + "confirm_ticket": true, + "add_to_title": true, + "append_hashtag": false, + "surround": "", + "title_position": "start" + }, + "commit_title": { + "max_size": 70 + }, + "commit_body": { + "enable": true, + "required": false + }, + "commit_footer": { + "enable": false, + "initial_value": [], + "options": [ + "closes", + "trailer", + "breaking-change", + "deprecated", + "custom" + ] + }, + "breaking_change": { + "add_exclamation_to_title": true + }, + "confirm_with_editor": false, + "confirm_commit": true, + "print_commit_output": true, + "branch_pre_commands": [], + "branch_post_commands": [], + "worktree_pre_commands": [], + "worktree_post_commands": [], + "branch_user": { + "enable": true, + "required": false, + "separator": "/" + }, + "branch_type": { + "enable": true, + "separator": "/" + }, + "branch_version": { + "enable": false, + "required": false, + "separator": "/" + }, + "branch_ticket": { + "enable": true, + "required": false, + "separator": "-" + }, + "branch_description": { + "max_length": 70, + "separator": "" + }, + "branch_action_default": "branch", + "branch_order": [ + "user", + "version", + "type", + "ticket", + "description" + ], + "enable_worktrees": true, + "overrides": {} +} diff --git a/.gitignore b/.gitignore index 9185e48..c8a34ef 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .DS_Store src/img/ src/prototype/ +generated_images/ # Generated by Cargo # will have compiled files and executables diff --git a/Cargo.lock b/Cargo.lock index 8523807..ba57d01 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,18 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "ahash" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d713b3834d76b85304d4d525563c1276e2e30dc97cc67bfb4585a4a29fc2c89f" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.2" @@ -26,6 +38,40 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[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-to-tui" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a39d5e575d0d1de1a8e008740ee9ab672c149f513e75a2b4f4810ecc73b226a" +dependencies = [ + "nom", + "ratatui", + "simdutf8", + "smallvec", + "thiserror", +] + [[package]] name = "anstream" version = "0.6.12" @@ -167,6 +213,21 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +[[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.83" @@ -182,6 +243,20 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bc015644b92d5890fab7489e49d21f879d5c990186827d42ec511919404f38b" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets 0.52.0", +] + [[package]] name = "clap" version = "4.5.1" @@ -222,7 +297,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.50", ] [[package]] @@ -263,6 +338,19 @@ dependencies = [ "windows-sys 0.48.0", ] +[[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 = "core-foundation" version = "0.9.4" @@ -314,31 +402,48 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" [[package]] -name = "crunchy" -version = "0.2.2" +name = "crossterm" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" +dependencies = [ + "bitflags 2.4.2", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] [[package]] -name = "dgen" -version = "0.1.0" +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" dependencies = [ - "anyhow", - "artem", - "clap", - "dotenv", - "image", - "reqwest", - "serde", - "serde_json", + "winapi", ] +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "dotenv" version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" +[[package]] +name = "dyn-clone" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "545b22097d44f8a9581187cdf93de7a71e4722bf51200cfaba810865b49a495d" + [[package]] name = "either" version = "1.10.0" @@ -582,6 +687,10 @@ 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" @@ -672,6 +781,35 @@ dependencies = [ "tokio-native-tls", ] +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icy_sixel" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dc4d30216c3fc247730a4c6c74db2bd217a5454361ce24d70e504bda0cd345e" + [[package]] name = "idna" version = "0.5.0" @@ -710,12 +848,27 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "indoc" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8" + [[package]] name = "ipnet" version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +[[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" @@ -780,6 +933,15 @@ version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +[[package]] +name = "lru" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2c024b41519440580066ba82aab04092b333e09066a5eb86c7c4890df31f22" +dependencies = [ + "hashbrown", +] + [[package]] name = "memchr" version = "2.7.1" @@ -792,6 +954,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.7.2" @@ -809,6 +977,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" dependencies = [ "libc", + "log", "wasi", "windows-sys 0.48.0", ] @@ -831,6 +1000,16 @@ dependencies = [ "tempfile", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "num-traits" version = "0.2.18" @@ -888,7 +1067,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.50", ] [[package]] @@ -909,6 +1088,35 @@ dependencies = [ "vcpkg", ] +[[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 0.48.5", +] + +[[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" @@ -946,6 +1154,12 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + [[package]] name = "proc-macro2" version = "1.0.78" @@ -973,6 +1187,71 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[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 = "ratatui-image" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b2b2c9623c63916694d56b7f27358ef81fd6232ffa4858444787ecbcda9f791" +dependencies = [ + "base64", + "dyn-clone", + "icy_sixel", + "image", + "rand", + "ratatui", + "rustix", +] + [[package]] name = "rayon" version = "1.8.1" @@ -993,6 +1272,15 @@ dependencies = [ "crossbeam-utils", ] +[[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 = "regex" version = "1.10.3" @@ -1142,6 +1430,12 @@ dependencies = [ "untrusted", ] +[[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" @@ -1203,7 +1497,7 @@ checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.50", ] [[package]] @@ -1217,6 +1511,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -1229,12 +1532,48 @@ dependencies = [ "serde", ] +[[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 = "simd-adler32" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +[[package]] +name = "simdutf8" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" + [[package]] name = "slab" version = "0.4.9" @@ -1269,18 +1608,67 @@ dependencies = [ "lock_api", ] +[[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 = "strsim" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" +[[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.50", +] + [[package]] name = "subtle" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" +[[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.50" @@ -1341,6 +1729,26 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "thiserror" +version = "1.0.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.50", +] + [[package]] name = "tiff" version = "0.9.1" @@ -1367,6 +1775,29 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "tnap" +version = "0.1.0" +dependencies = [ + "ansi-to-tui", + "anyhow", + "artem", + "chrono", + "clap", + "crossterm", + "dotenv", + "env_logger", + "image", + "log", + "once_cell", + "ratatui", + "ratatui-image", + "reqwest", + "serde", + "serde_json", + "toml", +] + [[package]] name = "tokio" version = "1.36.0" @@ -1407,6 +1838,40 @@ dependencies = [ "tracing", ] +[[package]] +name = "toml" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a9aad4a3066010876e8dcf5a8a06e70a558751117a145c6ce2b82c2e2054290" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c1b5fd4128cc8d3e0cb74d4ed9a9cc7c7284becd4df68f5f940e1ad123606f6" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "tower-service" version = "0.3.2" @@ -1459,6 +1924,18 @@ 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 = "untrusted" version = "0.9.0" @@ -1505,6 +1982,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + [[package]] name = "want" version = "0.3.1" @@ -1541,7 +2024,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn", + "syn 2.0.50", "wasm-bindgen-shared", ] @@ -1575,7 +2058,7 @@ checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.50", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -1611,6 +2094,37 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" +[[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-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.0", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -1743,6 +2257,15 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" +[[package]] +name = "winnow" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a4191c47f15cc3ec71fcb4913cb83d58def65dd3787610213c649283b5ce178" +dependencies = [ + "memchr", +] + [[package]] name = "winreg" version = "0.50.0" @@ -1753,6 +2276,26 @@ dependencies = [ "windows-sys 0.48.0", ] +[[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.50", +] + [[package]] name = "zeroize" version = "1.7.0" diff --git a/Cargo.toml b/Cargo.toml index 569a21c..24a4cb6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "dgen" +name = "tnap" version = "0.1.0" edition = "2021" @@ -9,8 +9,17 @@ edition = "2021" anyhow = "1.0.79" artem = "2.0.6" clap = { version = "4.5.1", features = ["derive"] } +crossterm = "0.27.0" dotenv = "0.15.0" image = "0.24.8" reqwest = { version = "0.11.10", features = ["blocking", "json"] } -serde = "1.0.130" +serde = { version = "1.0.130", features = ["derive"] } serde_json = "1.0.72" +toml = "0.8.10" +ratatui = "0.26.1" +ansi-to-tui = "4.0.0" +ratatui-image = "0.8.1" +log = "0.4.20" +env_logger = "0.11.2" +once_cell = "1.19.0" +chrono = "0.4.34" diff --git a/README.md b/README.md index 71a8c6f..9498fc2 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,71 @@ -# dgen -## Usage +
-``` -Generate an image with DALL·E and display it on the terminal +# tnap - Let's take a nap 💤 -Usage: dgen [OPTIONS] +![Demo](./examples/demo.gif) -Arguments: - Prompt to pass to DALL·E +`tnap` is a screen saver for the terminal. +You can rest the terminal in a secure. -Options: - -a, --ascii Convert an image to ASCII art - -h, --help Print help - -V, --version Print version -``` +[![Lang](https://img.shields.io/badge/Rust-1.26+-blue.svg?logo=rust)](https://www.rust-lang.org/) +[![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) +![Release](https://img.shields.io/badge/Release-v0.1.0-blue.svg) + +
+ +## Features -```sh -$ cargo build -$ ./target/debug/dgen "prompt" +- [x] Display images in the terminal and use it as a screen saver +- [x] Convert images to **_ASCII art_** +- [x] Use images generated using **_[DALL-E 3](https://openai.com/dall-e-3)_** +- [x] Generate by simply specifying a key in `config.toml` without thinking a prompt +- [x] Of course, you can also generate images by specifying a prompt + +## Screenshots + +
+ girl_with_headphone + girl_with_headphone_ascii +
+ +## Usage -or +| Option | Description | Type | Required? | +| ------------------- | ---------------------------------------------------- | ------ | --------- | +| `--theme ` | Use the sample theme without generating images | String | No | +| `--key ` | Generate images by a default prompt in `config.toml` | String | No | +| `--prompt ` | Generate images with a user's prompt | String | No | +| `--ascii` | Convert an image to ASCII art | bool | No | -$ cargo run -- "prompt" +## Installation + +You can install tnap by following these steps: + +```bash +gh repo clone pigeon-sable/tnap +cd tnap +cargo install --path . +export TNAP_ROOT=`pwd` ``` +## Acknowledgements + +- [sheepla/pingu](https://github.com/sheepla/pingu) - 🐧ping command but with pingu +- [mtoyoda/sl](https://github.com/mtoyoda/sl) - SL(1): Cure your bad habit of mistyping +- [dduan/tre](https://github.com/dduan/tre) - Tree command, improved. +- [dalance/procs](https://github.com/dalance/procs) - A modern replacement for ps written in Rust +- [sharkdp/bat](https://github.com/sharkdp/bat) - A `cat` clone with wings. +- [ogham/exa](https://github.com/ogham/exa) - A modern replacement for ‘`ls`. + ## License -[Apache-2.0](./LICENSE) + +🪪 [Apache-2.0](./LICENSE) + +## Authors + +- 🍪 [@shuheykoyama](https://github.com/shuheykoyama) +- 🦀 [@4n12i](https://github.com/4n12i) +- 👮 [@Kobayashi123](https://github.com/Kobayashi123) +- 🧪 [@Gteruya](https://github.com/Gteruya) + +[![GitHub](https://img.shields.io/badge/-Follow--FFFFFF?style=social&logo=github&label=Follow%20pigeon-sable)](https://github.com/pigeon-sable) diff --git a/config.toml b/config.toml new file mode 100644 index 0000000..ba42d24 --- /dev/null +++ b/config.toml @@ -0,0 +1,3 @@ +[prompts] +cat = "High-contrast black and white Japanese anime illustration of small cat. It is depicted in a three-quarter view. The image focuses on the all set against a pure black background to highlight the subjects." +girl = "high-contrast black and white Japanese anime illustration of a serene Japanese girl wearing cute headphones. She is depicted in a three-quarter view with her hand gently resting on one earpiece, enjoying the music. The image focuses on the texture of her hair and the soft expression on her face, all set against a pure black background to highlight the subject. Her hair is black and outline is white." diff --git a/examples/demo.gif b/examples/demo.gif new file mode 100644 index 0000000..285116a Binary files /dev/null and b/examples/demo.gif differ diff --git a/examples/girl_with_headphone.png b/examples/girl_with_headphone.png new file mode 100644 index 0000000..d488bc7 Binary files /dev/null and b/examples/girl_with_headphone.png differ diff --git a/examples/girl_with_headphone_ascii.png b/examples/girl_with_headphone_ascii.png new file mode 100644 index 0000000..0f78cf2 Binary files /dev/null and b/examples/girl_with_headphone_ascii.png differ diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..76904ce --- /dev/null +++ b/src/app.rs @@ -0,0 +1,176 @@ +use crate::convert_image_to_ascii::convert_image_to_ascii; +use crate::util::*; +use crate::APP_EXIT; +use crate::PATHS; +use ansi_to_tui::IntoText; +use anyhow::Result; +use crossterm::event::{self, Event, KeyCode}; +use ratatui::widgets::Paragraph; +use ratatui::Frame; +use ratatui::{backend::Backend, Terminal}; +use ratatui_image::{ + picker::Picker, + protocol::{ImageSource, StatefulProtocol}, + Resize, StatefulImage, +}; +use std::path::Path; +use std::sync::atomic::Ordering::SeqCst; +use std::time::{Duration, Instant}; + +pub fn run(dir: &Path, ascii: bool) -> Result<()> { + let files = get_files(dir)?; + PATHS.lock().unwrap().extend_from_slice(&files); + + let mut app = App::new(ascii); + let mut terminal = init_terminal()?; + + app.run_tui(&mut terminal)?; + reset_terminal()?; + + Ok(()) +} + +struct App { + ascii: bool, + index: usize, + + picker: Picker, + image_source: ImageSource, + image_state: Box, +} + +impl App { + fn new(ascii: bool) -> Self { + let binding = PATHS.lock().unwrap(); + let path = binding.first().unwrap(); + + let dyn_img = image::io::Reader::open(path).unwrap().decode().unwrap(); + + let mut picker = Picker::from_termios().unwrap(); + picker.guess_protocol(); + + let image_source = ImageSource::new(dyn_img.clone(), picker.font_size); + let image_state = picker.new_resize_protocol(dyn_img); + + Self { + ascii, + index: 0, + picker, + image_source, + image_state, + } + } +} + +impl App { + fn run_tui(&mut self, terminal: &mut Terminal) -> Result<()> { + let tick_rate = Duration::from_secs(3); + let mut last_tick = Instant::now(); + + loop { + terminal.draw(|f| self.ui(f))?; + + let timeout = tick_rate.saturating_sub(last_tick.elapsed()); + if event::poll(timeout)? { + if let Event::Key(key) = event::read()? { + match key.code { + KeyCode::Char('a') => self.ascii = !self.ascii, + KeyCode::Char('q') => { + APP_EXIT.store(true, SeqCst); + return Ok(()); + } + _ => (), + } + } + } + + if last_tick.elapsed() >= tick_rate { + self.on_tick(); + last_tick = Instant::now(); + } + } + } + + fn ui(&mut self, frame: &mut Frame) { + let frame_size = frame.size(); + + if self.ascii { + let binding = PATHS.lock().unwrap(); + let path = binding.get(self.index).unwrap(); + + let ascii_art = convert_image_to_ascii(path) + .expect("Failed to convert image to ascii art") + .into_text() + .unwrap(); + let ascii_height = ascii_art.lines.len() as u16; + let offset_y = (frame_size.height - ascii_height) / 2; + let area = ratatui::layout::Rect::new(0, offset_y, frame_size.width, ascii_height); + let paragraph = Paragraph::new(ascii_art); + // println!("frame.size(): {}, area: {}", frame.size(), area); + // println!("ascii_lines: {}", ascii_lines); + frame.render_widget(paragraph, area) + } else { + let image_width = frame_size.width * 4 / 5; + let image_height = frame_size.height * 4 / 5; + + let image = StatefulImage::new(None).resize(Resize::Fit); + // 描画エリアの中央配置を計算 + let area = ratatui::layout::Rect::new( + (frame_size.width / 2).saturating_sub(image_width / 2) + frame_size.width * 1 / 7, + // (frame_size.width / 2).saturating_sub(image_width / 2), + (frame_size.height / 2).saturating_sub(image_height / 2), + image_width, + image_height, + ); + // println!( + // "frame_size.width: {}, frame_size.height: {}", + // frame_size.width, frame_size.height + // ); + // println!( + // "image_width: {}, image_height: {}", + // image_width, image_height + // ); + // println!("area: {}", area); + // println!( + // "self.image_source.image.width(): {}, + // self.image_source.image.height(): {}", + // self.image_source.image.width(), + // self.image_source.image.height() + // ); + frame.render_stateful_widget(image, area, &mut self.image_state); + } + } + + fn on_tick(&mut self) { + let binding = PATHS.lock().unwrap(); + let length = binding.len(); + self.index = (self.index + 1) % length; + + let path = binding.get(self.index).unwrap(); + let dyn_img = image::io::Reader::open(path).unwrap().decode().unwrap(); + self.image_source = ImageSource::new(dyn_img.clone(), self.picker.font_size); + self.image_state = self.picker.new_resize_protocol(dyn_img); + } +} + +// #[cfg(test)] +// mod tests { +// use super::*; +// use ratatui::backend::TestBackend; + +// #[test] +// fn test_ui(){ +// static mut PATH_IN_UI:str = "path"; +// static mut ASCII_HEIGHT:u16 = 0; +// static FILE:str = ""; + +// let mut app = App::new(FILE, false); +// let mut test_frame = Frame::new(TestBackend::new(10, 10)); + + +// app.ui(&mut test_frame); +// asserteq!(PATH_IN_UI, "***"); +// asserteq!(ASCII_HEIGHT, ); + +// } +// } \ No newline at end of file diff --git a/src/convert_image_to_ascii.rs b/src/convert_image_to_ascii.rs index d0e98a6..a2e6f79 100644 --- a/src/convert_image_to_ascii.rs +++ b/src/convert_image_to_ascii.rs @@ -1,26 +1,62 @@ use anyhow::{anyhow, Result}; use artem::{config::ConfigBuilder, convert}; +use crossterm::terminal::size; use image::io::Reader as ImageReader; +use std::num::NonZeroU32; use std::path::Path; -pub fn convert_image_to_ascii(image_path: &str) -> Result<()> { +pub fn convert_image_to_ascii(image_path: &Path) -> Result { // Open the image file - let img = ImageReader::open(Path::new(image_path)) + let img = ImageReader::open(image_path) .map_err(|e| anyhow!("Failed to open image: {}", e))? .decode() .map_err(|e| anyhow!("Failed to decode image: {}", e))?; - // // Conversion Config - // let mut config_builder = ConfigBuilder::default(); - // if let Some(w) = width { - // config_builder = config_builder.width(w); - // } - // let config = config_builder.colored(colored).build(); + // Read image data from memory + // let img = image::load_from_memory(image_data) + // .map_err(|e| anyhow!("Failed to load image from memory: {}", e))?; + + // Conversion Config + let target_size = NonZeroU32::new(ascii_size()?).expect("Width must be non-zero."); + log::info!("target size: {}", target_size); + + let config = ConfigBuilder::new() + .center_x(true) + .center_y(true) + .scale(0.380025f32) // magic number! + .target_size(target_size) + .build(); // Convert image to ASCII - let ascii_art = convert(img, &artem::config::ConfigBuilder::new().build()); + let ascii_art = convert(img, &config); + + Ok(ascii_art) +} - println!("{}", ascii_art); +fn ascii_size() -> Result { + let (columns, rows) = size()?; + let size = (std::cmp::min(columns, rows) * 2) as u32; + log::info!("ascii size: {}", size); - Ok(()) + Ok(size) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_convert_image_to_ascii() { + // TODO: Change + // Prepare image files for testing. + let image_path = Path::new("./examples/girl_with_headphone.png"); + + // Convert the image to ASCII art. + let result = convert_image_to_ascii(&image_path); + assert!(result.is_ok()); + + // Compare with the expected ASCII art. + // let expected_ascii_art = "expected_ascii_art"; + // assert_eq!(result, Ok(expected_ascii_art)); + } +} \ No newline at end of file diff --git a/src/generate_image.rs b/src/generate_image.rs index 2e34d6a..47545eb 100644 --- a/src/generate_image.rs +++ b/src/generate_image.rs @@ -1,14 +1,14 @@ use anyhow::{anyhow, Result}; -use reqwest; use serde_json::{json, Value}; use std::env; use std::fs::File; use std::io::Write; -use std::path::Path; +use std::path::PathBuf; pub fn generate_image(prompt: &str) -> Result { let api_key = env::var("OPENAI_API_KEY").expect("Expected an environment variable OPENAI_API_KEY"); + log::info!("prompt: {}", prompt); let client = reqwest::blocking::Client::new(); let response = client @@ -23,22 +23,26 @@ pub fn generate_image(prompt: &str) -> Result { })) .send()? .json::()?; + log::info!("API Response: {:?}", response); let image_url = response["data"][0]["url"] .as_str() .ok_or(anyhow!("Failed to extract image URL"))? .to_string(); + log::info!("image_url {}", image_url); Ok(image_url) } -pub fn download_image(url: &str, file_path: &str) -> Result<()> { +pub fn download_image(url: &str, path: &PathBuf) -> Result<()> { let response = reqwest::blocking::get(url)?.bytes()?; - let path = Path::new(file_path); let mut file = File::create(path)?; file.write_all(&response)?; + log::info!("Image saved to {:?}", path); - println!("Image saved to {:?}", path); Ok(()) + // let response = reqwest::blocking::get(url)?; + // let bytes = response.bytes()?; + // Ok(bytes.to_vec()) } diff --git a/src/main.rs b/src/main.rs index ee95ea7..8a83c3e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,39 +1,217 @@ -use anyhow::Result; +use anyhow::{bail, Result}; +use chrono::Local; use clap::Parser; -use convert_image_to_ascii::convert_image_to_ascii; use dotenv::dotenv; -// use generate_image::{download_image, generate_image}; +use generate_image::{download_image, generate_image}; +use once_cell::sync::Lazy; +use std::fs::create_dir_all; +use std::path::{Path, PathBuf}; +use std::sync::atomic::AtomicBool; +use std::sync::atomic::Ordering::SeqCst; +use std::sync::Mutex; +use std::{env, fs, thread}; +use toml::Value; +mod app; mod convert_image_to_ascii; -// mod generate_image; +mod generate_image; +mod util; -/// Generate image with DALL-E and print it +// Path to the directory containing the images to draw +static PATHS: Lazy>> = Lazy::new(|| Mutex::new(Vec::new())); + +static APP_EXIT: AtomicBool = AtomicBool::new(false); +static GEN_EXIT: AtomicBool = AtomicBool::new(false); + +// Maximum number of images to generates +const MAX_IMAGES: u8 = 5; + +/// You can use sample themes for tnap and generate image with default prompts or your own prompts. #[derive(Parser)] #[command(version, about, long_about = None)] // Read from Cargo.toml struct Args { - /// Prompt to pass to DALL-E - prompt: String, + /// Use the sample theme without generating images + #[arg(short, long)] + theme: Option, + + /// Generate Image by looking up the corresponding value in config.toml + /// using the subsequent string as a key and using it as a prompt. + #[arg(short, long)] + key: Option, + + /// Generate images with user-considered prompt + #[arg(short, long)] + prompt: Option, /// Convert an image to ASCII art - #[arg(short = 'a', long)] + #[arg(short, long)] ascii: bool, } fn main() -> Result<()> { dotenv().ok(); // Read environment variable from .env file + env_logger::init(); + let args = Args::parse(); + match (args.theme, args.key, args.prompt) { + (Some(theme), None, None) => display_theme(&theme, args.ascii), + (None, Some(key), None) => { + let prompt = read_config(&key)?; + display_generated_image(&prompt, args.ascii) + } + (None, None, Some(prompt)) => display_generated_image(&prompt, args.ascii), + // TODO: Set default values + (None, None, None) => display_theme("cat", args.ascii), + _ => bail!("Invalid arguments combination."), + } +} + +fn read_config(key: &str) -> Result { + let tnap_root = match env::var("TNAP_ROOT") { + Ok(val) => { + log::info!("The data was obtained from the environment variable TNAP_ROOT"); + val + }, + Err(err) => { + log::info!("{}", err); + log::info!("The data was obtained from the current directory"); + Path::new(".").canonicalize().unwrap().to_str().unwrap().to_string() + } + }; + log::info!("TNAP_ROOT: {}", tnap_root); + + let config_path = format!("{}/config.toml", tnap_root); + + let contents = fs::read_to_string(config_path).unwrap(); + let value = contents.parse::().unwrap(); + + match value + .get("prompts") + .and_then(|v| v.get(key)) + .and_then(|v| v.as_str()) + { + Some(prompt) => Ok(prompt.to_string()), + None => bail!("Key not found in config."), + } +} + +fn display_theme(theme: &str, ascii: bool) -> Result<()> { + // Obtained from the environment variable TNAP_ROOT, or if it does not exist, the path of the current directory is selected + let tnap_root = match env::var("TNAP_ROOT") { + Ok(val) => { + log::info!("The data was obtained from the environment variable TNAP_ROOT"); + val + }, + Err(err) => { + log::info!("{}", err); + log::info!("The data was obtained from the current directory"); + Path::new(".").canonicalize().unwrap().to_str().unwrap().to_string() + } + }; + log::info!("TNAP_ROOT: {}", tnap_root); + + let themes_path = format!("{}/themes", tnap_root); + log::info!("themes_path: {}", themes_path); + + let theme_path = format!("{}/{}", themes_path, theme); + log::info!("{}_path: {}", theme, theme_path); + + if Path::new(&theme_path).exists() { + let files = fs::read_dir(Path::new(&theme_path)).unwrap(); + let mut flag = false; + + for file in files { + let file_path = file.unwrap().path(); + + // Checks if a file exists and if it is an image file. + if file_path.is_file() && is_image_file(&file_path){ + log::info!("image_path: {}", file_path.display()); + } else { + flag = true; + break; + } + } - // println!("Generating an image..."); - // let image_url = generate_image(&args.prompt)?; - // download_image(&image_url, "generated_image.png")?; + // If the file contains anything other than an image file + if flag { + bail!("{} ({}) include non-image file", theme, theme_path); + } - if args.ascii { - let image_path = "./src/img/girl_with_headphone_01.png"; - convert_image_to_ascii(image_path)?; - println!("Converted image to ASCII art!"); - } else { - println!("Non-ASCII image feature is not implemented yet."); + return app::run(Path::new(&theme_path), ascii); } + bail!("{} ({}) is not found", theme, theme_path); +} + +fn is_image_file(path: &Path) -> bool { + let image_extensions = ["png", "jpg", "jpeg", "PNG", "JPG", "JPEG"]; + let extension = path.extension().unwrap().to_str().unwrap(); + + image_extensions.contains(&extension) +} + +fn display_generated_image(prompt: &str, ascii: bool) -> Result<()> { + let tnap_root = match env::var("TNAP_ROOT") { + Ok(val) => { + log::info!("The data was obtained from the environment variable TNAP_ROOT"); + val + }, + Err(err) => { + log::info!("{}", err); + log::info!("The data was obtained from the current directory"); + Path::new(".").canonicalize().unwrap().to_str().unwrap().to_string() + } + }; + log::info!("TNAP_ROOT: {}", tnap_root); + + let dir_path = format!("{}/generated_images", tnap_root); + log::info!("dir_path: {}", dir_path); + + let time = Local::now().format("%Y_%m%d_%H%M").to_string(); + let dir_path = Path::new(&dir_path).join(time); + + create_dir_all(&dir_path)?; + + // Add an image path to display while waiting for image generation + let path_to_sample = format!("{}/examples", tnap_root); + log::info!("path_to_sample: {}", path_to_sample); + + let path_to_sample = Path::new(&path_to_sample).join("girl_with_headphone.png"); + PATHS.lock().unwrap().push(path_to_sample); + + let dir = dir_path.clone(); + let prompt = prompt.to_string(); + let handle = thread::spawn(move || { + let mut url = generate_image(&prompt).unwrap(); + let mut path = dir.join("0.png"); + download_image(&url, &path).expect("Failed to download a generated image."); + + PATHS.lock().unwrap().push(path); + PATHS.lock().unwrap().remove(0); // Remove a sample image path + + for i in 1..MAX_IMAGES { + if APP_EXIT.load(SeqCst) { + break; + } + + url = generate_image(&prompt).unwrap(); + path = dir.join(&format!("{}.png", i)); + download_image(&url, &path).expect("Failed to download a generated image."); + + PATHS.lock().unwrap().push(path); + } + + GEN_EXIT.store(true, SeqCst); + }); + + app::run(&dir_path, ascii)?; + + if !GEN_EXIT.load(SeqCst) { + eprintln!("Waiting for image generation to complete..."); + } + + handle + .join() + .expect("Couldn't join on the associated thread."); Ok(()) } diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..0d6f59d --- /dev/null +++ b/src/util.rs @@ -0,0 +1,42 @@ +use anyhow::Result; +use crossterm::{ + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, + ExecutableCommand, +}; +use ratatui::{backend::CrosstermBackend, Terminal}; +use std::io::{stdout, Stdout}; +use std::{ + fs, + path::{Path, PathBuf}, +}; + +pub fn get_files(dir: &Path) -> Result> { + let mut files = vec![]; + + for entry in fs::read_dir(dir)? { + let path = entry?.path(); + if path.is_file() { + files.push(path); + } + } + + Ok(files) +} + +pub fn init_terminal() -> Result>> { + stdout().execute(EnterAlternateScreen)?; + enable_raw_mode()?; + + let backend = CrosstermBackend::new(stdout()); + let mut terminal = Terminal::new(backend)?; + terminal.clear()?; + + Ok(terminal) +} + +pub fn reset_terminal() -> Result<()> { + disable_raw_mode()?; + stdout().execute(LeaveAlternateScreen)?; + + Ok(()) +} diff --git a/themes/cat/cat_01.png b/themes/cat/cat_01.png new file mode 100644 index 0000000..1e5c304 Binary files /dev/null and b/themes/cat/cat_01.png differ diff --git a/themes/cat/cat_02.png b/themes/cat/cat_02.png new file mode 100644 index 0000000..054197a Binary files /dev/null and b/themes/cat/cat_02.png differ diff --git a/themes/cat/cat_03.png b/themes/cat/cat_03.png new file mode 100644 index 0000000..9fa6ee0 Binary files /dev/null and b/themes/cat/cat_03.png differ diff --git a/themes/cat/cat_04.png b/themes/cat/cat_04.png new file mode 100644 index 0000000..ba79d6e Binary files /dev/null and b/themes/cat/cat_04.png differ diff --git a/themes/cat/cat_05.png b/themes/cat/cat_05.png new file mode 100644 index 0000000..96c5f0a Binary files /dev/null and b/themes/cat/cat_05.png differ diff --git a/themes/girl/girl_01.png b/themes/girl/girl_01.png new file mode 100644 index 0000000..d488bc7 Binary files /dev/null and b/themes/girl/girl_01.png differ diff --git a/themes/girl/girl_02.png b/themes/girl/girl_02.png new file mode 100644 index 0000000..20b7655 Binary files /dev/null and b/themes/girl/girl_02.png differ diff --git a/themes/girl/girl_03.png b/themes/girl/girl_03.png new file mode 100644 index 0000000..a05ed5d Binary files /dev/null and b/themes/girl/girl_03.png differ diff --git a/themes/girl/girl_04.png b/themes/girl/girl_04.png new file mode 100644 index 0000000..edaa2c0 Binary files /dev/null and b/themes/girl/girl_04.png differ diff --git a/themes/girl/girl_05.png b/themes/girl/girl_05.png new file mode 100644 index 0000000..cb0292e Binary files /dev/null and b/themes/girl/girl_05.png differ