From 139f85dadd392937b200a7a32a013f9bb722cbec Mon Sep 17 00:00:00 2001 From: Jeff Zhao Date: Mon, 4 Oct 2021 22:22:48 -0400 Subject: [PATCH] initial commit --- .gitignore | 1 + Cargo.lock | 2255 +++++++++++++++++ Cargo.toml | 41 + README.md | 46 + config/client.toml | 18 + config/keymap.toml | 46 + config/server.toml | 2 + config/theme.toml | 42 + lib/dizi_commands/Cargo.toml | 13 + lib/dizi_commands/src/api_command.rs | 86 + lib/dizi_commands/src/constants.rs | 20 + lib/dizi_commands/src/error/error_kind.rs | 61 + lib/dizi_commands/src/error/error_type.rs | 80 + lib/dizi_commands/src/error/mod.rs | 7 + lib/dizi_commands/src/lib.rs | 4 + lib/dizi_commands/src/structs/mod.rs | 3 + lib/dizi_commands/src/structs/player_play.rs | 8 + src/client/commands/change_directory.rs | 36 + src/client/commands/command_line.rs | 32 + src/client/commands/cursor_move.rs | 137 + src/client/commands/mod.rs | 14 + src/client/commands/open_file.rs | 50 + src/client/commands/parent_directory.rs | 22 + src/client/commands/quit.rs | 13 + src/client/commands/reload.rs | 49 + src/client/commands/search.rs | 42 + src/client/commands/search_glob.rs | 46 + src/client/commands/search_skim.rs | 100 + src/client/commands/search_string.rs | 44 + src/client/commands/selection.rs | 28 + src/client/commands/show_hidden.rs | 21 + src/client/commands/sort.rs | 29 + src/client/config/general/client.rs | 73 + src/client/config/general/config.rs | 80 + src/client/config/general/display.rs | 69 + src/client/config/general/mod.rs | 11 + src/client/config/general/player.rs | 62 + src/client/config/general/sort.rs | 50 + src/client/config/keymap/keymapping.rs | 135 + src/client/config/keymap/mod.rs | 3 + src/client/config/mod.rs | 82 + src/client/config/theme/app_theme.rs | 127 + src/client/config/theme/mod.rs | 5 + src/client/config/theme/style.rs | 109 + src/client/context/app_context.rs | 110 + src/client/context/message_queue.rs | 59 + src/client/context/mod.rs | 5 + src/client/event.rs | 105 + src/client/fs/dirlist.rs | 153 ++ src/client/fs/entry.rs | 102 + src/client/fs/metadata.rs | 124 + src/client/fs/mod.rs | 7 + src/client/history.rs | 160 ++ src/client/key_command/commands.rs | 58 + src/client/key_command/constants.rs | 26 + src/client/key_command/impl_appcommand.rs | 60 + src/client/key_command/impl_appexecute.rs | 58 + src/client/key_command/impl_display.rs | 20 + src/client/key_command/impl_from_str.rs | 147 ++ src/client/key_command/keybind.rs | 18 + src/client/key_command/mod.rs | 14 + src/client/key_command/traits.rs | 22 + src/client/main.rs | 115 + src/client/preview/mod.rs | 2 + src/client/preview/preview_default.rs | 47 + src/client/preview/preview_dir.rs | 40 + src/client/run.rs | 52 + src/client/ui/mod.rs | 5 + src/client/ui/tui_backend.rs | 133 + src/client/ui/views/mod.rs | 9 + src/client/ui/views/tui_command_menu.rs | 64 + src/client/ui/views/tui_folder_view.rs | 144 ++ src/client/ui/views/tui_textfield.rs | 265 ++ src/client/ui/views/tui_view.rs | 26 + src/client/ui/widgets/mod.rs | 13 + src/client/ui/widgets/tui_dirlist_detailed.rs | 300 +++ src/client/ui/widgets/tui_footer.rs | 66 + src/client/ui/widgets/tui_menu.rs | 36 + src/client/ui/widgets/tui_prompt.rs | 70 + src/client/ui/widgets/tui_text.rs | 110 + src/client/ui/widgets/tui_topbar.rs | 50 + src/client/util/display_option.rs | 91 + src/client/util/format.rs | 28 + src/client/util/input.rs | 67 + src/client/util/keyparse.rs | 73 + src/client/util/mod.rs | 12 + src/client/util/search.rs | 7 + src/client/util/select.rs | 26 + src/client/util/sort_option.rs | 58 + src/client/util/sort_type.rs | 160 ++ src/client/util/string.rs | 67 + src/client/util/style.rs | 51 + src/client/util/to_string.rs | 46 + src/client/util/unix.rs | 51 + src/server/audio/mod.rs | 7 + src/server/audio/pipewire.rs | 36 + src/server/audio/player.rs | 71 + src/server/audio/playlist.rs | 48 + src/server/audio/song.rs | 29 + src/server/command.rs | 22 + src/server/commands/mod.rs | 3 + src/server/commands/player.rs | 15 + src/server/commands/playlist.rs | 42 + src/server/config/default/default.rs | 84 + src/server/config/default/mod.rs | 3 + src/server/config/mod.rs | 56 + src/server/context/app_context.rs | 31 + src/server/context/mod.rs | 5 + src/server/context/player_context.rs | 26 + src/server/error/error_type.rs | 18 + src/server/error/mod.rs | 3 + src/server/handlers/mod.rs | 5 + src/server/handlers/player.rs | 11 + src/server/handlers/playlist/add_song.rs | 49 + src/server/handlers/playlist/get_playlist.rs | 10 + src/server/handlers/playlist/mod.rs | 7 + src/server/handlers/playlist/remove_song.rs | 38 + src/server/handlers/websocket.rs | 35 + src/server/handlers/webtransport.rs | 0 src/server/main.rs | 85 + src/server/server.rs | 70 + 121 files changed, 8543 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 config/client.toml create mode 100644 config/keymap.toml create mode 100644 config/server.toml create mode 100644 config/theme.toml create mode 100644 lib/dizi_commands/Cargo.toml create mode 100644 lib/dizi_commands/src/api_command.rs create mode 100644 lib/dizi_commands/src/constants.rs create mode 100644 lib/dizi_commands/src/error/error_kind.rs create mode 100644 lib/dizi_commands/src/error/error_type.rs create mode 100644 lib/dizi_commands/src/error/mod.rs create mode 100644 lib/dizi_commands/src/lib.rs create mode 100644 lib/dizi_commands/src/structs/mod.rs create mode 100644 lib/dizi_commands/src/structs/player_play.rs create mode 100644 src/client/commands/change_directory.rs create mode 100644 src/client/commands/command_line.rs create mode 100644 src/client/commands/cursor_move.rs create mode 100644 src/client/commands/mod.rs create mode 100644 src/client/commands/open_file.rs create mode 100644 src/client/commands/parent_directory.rs create mode 100644 src/client/commands/quit.rs create mode 100644 src/client/commands/reload.rs create mode 100644 src/client/commands/search.rs create mode 100644 src/client/commands/search_glob.rs create mode 100644 src/client/commands/search_skim.rs create mode 100644 src/client/commands/search_string.rs create mode 100644 src/client/commands/selection.rs create mode 100644 src/client/commands/show_hidden.rs create mode 100644 src/client/commands/sort.rs create mode 100644 src/client/config/general/client.rs create mode 100644 src/client/config/general/config.rs create mode 100644 src/client/config/general/display.rs create mode 100644 src/client/config/general/mod.rs create mode 100644 src/client/config/general/player.rs create mode 100644 src/client/config/general/sort.rs create mode 100644 src/client/config/keymap/keymapping.rs create mode 100644 src/client/config/keymap/mod.rs create mode 100644 src/client/config/mod.rs create mode 100644 src/client/config/theme/app_theme.rs create mode 100644 src/client/config/theme/mod.rs create mode 100644 src/client/config/theme/style.rs create mode 100644 src/client/context/app_context.rs create mode 100644 src/client/context/message_queue.rs create mode 100644 src/client/context/mod.rs create mode 100644 src/client/event.rs create mode 100644 src/client/fs/dirlist.rs create mode 100644 src/client/fs/entry.rs create mode 100644 src/client/fs/metadata.rs create mode 100644 src/client/fs/mod.rs create mode 100644 src/client/history.rs create mode 100644 src/client/key_command/commands.rs create mode 100644 src/client/key_command/constants.rs create mode 100644 src/client/key_command/impl_appcommand.rs create mode 100644 src/client/key_command/impl_appexecute.rs create mode 100644 src/client/key_command/impl_display.rs create mode 100644 src/client/key_command/impl_from_str.rs create mode 100644 src/client/key_command/keybind.rs create mode 100644 src/client/key_command/mod.rs create mode 100644 src/client/key_command/traits.rs create mode 100644 src/client/main.rs create mode 100644 src/client/preview/mod.rs create mode 100644 src/client/preview/preview_default.rs create mode 100644 src/client/preview/preview_dir.rs create mode 100644 src/client/run.rs create mode 100644 src/client/ui/mod.rs create mode 100644 src/client/ui/tui_backend.rs create mode 100644 src/client/ui/views/mod.rs create mode 100644 src/client/ui/views/tui_command_menu.rs create mode 100644 src/client/ui/views/tui_folder_view.rs create mode 100644 src/client/ui/views/tui_textfield.rs create mode 100644 src/client/ui/views/tui_view.rs create mode 100644 src/client/ui/widgets/mod.rs create mode 100644 src/client/ui/widgets/tui_dirlist_detailed.rs create mode 100644 src/client/ui/widgets/tui_footer.rs create mode 100644 src/client/ui/widgets/tui_menu.rs create mode 100644 src/client/ui/widgets/tui_prompt.rs create mode 100644 src/client/ui/widgets/tui_text.rs create mode 100644 src/client/ui/widgets/tui_topbar.rs create mode 100644 src/client/util/display_option.rs create mode 100644 src/client/util/format.rs create mode 100644 src/client/util/input.rs create mode 100644 src/client/util/keyparse.rs create mode 100644 src/client/util/mod.rs create mode 100644 src/client/util/search.rs create mode 100644 src/client/util/select.rs create mode 100644 src/client/util/sort_option.rs create mode 100644 src/client/util/sort_type.rs create mode 100644 src/client/util/string.rs create mode 100644 src/client/util/style.rs create mode 100644 src/client/util/to_string.rs create mode 100644 src/client/util/unix.rs create mode 100644 src/server/audio/mod.rs create mode 100644 src/server/audio/pipewire.rs create mode 100644 src/server/audio/player.rs create mode 100644 src/server/audio/playlist.rs create mode 100644 src/server/audio/song.rs create mode 100644 src/server/command.rs create mode 100644 src/server/commands/mod.rs create mode 100644 src/server/commands/player.rs create mode 100644 src/server/commands/playlist.rs create mode 100644 src/server/config/default/default.rs create mode 100644 src/server/config/default/mod.rs create mode 100644 src/server/config/mod.rs create mode 100644 src/server/context/app_context.rs create mode 100644 src/server/context/mod.rs create mode 100644 src/server/context/player_context.rs create mode 100644 src/server/error/error_type.rs create mode 100644 src/server/error/mod.rs create mode 100644 src/server/handlers/mod.rs create mode 100644 src/server/handlers/player.rs create mode 100644 src/server/handlers/playlist/add_song.rs create mode 100644 src/server/handlers/playlist/get_playlist.rs create mode 100644 src/server/handlers/playlist/mod.rs create mode 100644 src/server/handlers/playlist/remove_song.rs create mode 100644 src/server/handlers/websocket.rs create mode 100644 src/server/handlers/webtransport.rs create mode 100644 src/server/main.rs create mode 100644 src/server/server.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..c748e75 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2255 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5" +dependencies = [ + "memchr", +] + +[[package]] +name = "alphanumeric-sort" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20e59b2ccb4c1ffbbf45af6f493e16ac65a66981c85664f1587816c0b08cd698" + +[[package]] +name = "alsa" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75c4da790adcb2ce5e758c064b4f3ec17a30349f9961d3e5e6c9688b052a9e18" +dependencies = [ + "alsa-sys", + "bitflags", + "libc", + "nix 0.20.0", +] + +[[package]] +name = "alsa-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "ansi_term" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +dependencies = [ + "winapi", +] + +[[package]] +name = "anyhow" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1" + +[[package]] +name = "arrayref" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "base-x" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4521f3e3d031370679b3b140beb36dfe4801b09ac77e30c61941f97df3ef28b" + +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + +[[package]] +name = "beef" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bed554bd50246729a1ec158d08aa3235d1b69d94ad120ebe187e28894787e736" + +[[package]] +name = "bindgen" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2da379dbebc0b76ef63ca68d8fc6e71c0f13e59432e0987e508c1820e6ab5239" +dependencies = [ + "bitflags", + "cexpr 0.4.0", + "clang-sys", + "lazy_static", + "lazycell", + "peeking_take_while", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex 0.1.1", +] + +[[package]] +name = "bindgen" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "453c49e5950bb0eb63bb3df640e31618846c89d5b7faa54040d76e98e0134375" +dependencies = [ + "bitflags", + "cexpr 0.5.0", + "clang-sys", + "clap", + "env_logger", + "lazy_static", + "lazycell", + "log", + "peeking_take_while", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex 1.1.0", + "which", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitvec" +version = "0.19.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8942c8d352ae1838c9dda0b0ca2ab657696ef2232a20147cf1b30ae1a9cb4321" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "blake2b_simd" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", +] + +[[package]] +name = "bstr" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a40b47ad93e1a5404e6c18dec46b628214fee441c70f4ab5d6942142cc268a3d" +dependencies = [ + "memchr", +] + +[[package]] +name = "bumpalo" +version = "3.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9df67f7bf9ef8498769f994239c45613ef0c5899415fb58e9add412d2c1a538" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" + +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + +[[package]] +name = "cc" +version = "1.0.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26a6ce4b6a484fa3edb70f7efa6fc430fd2b87285fe8b84304fd0936faa0dc0" +dependencies = [ + "jobserver", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cexpr" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4aedb84272dbe89af497cf81375129abda4fc0a9e7c5d317498c15cc30c0d27" +dependencies = [ + "nom 5.1.2", +] + +[[package]] +name = "cexpr" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db507a7679252d2276ed0dd8113c6875ec56d3089f9225b2b42c30cc1f8e5c89" +dependencies = [ + "nom 6.2.1", +] + +[[package]] +name = "cfg-expr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b412e83326147c2bb881f8b40edfbf9905b9b8abaebd0e47ca190ba62fda8f0e" +dependencies = [ + "smallvec", +] + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +dependencies = [ + "libc", + "num-integer", + "num-traits", + "time 0.1.43", + "winapi", +] + +[[package]] +name = "clang-sys" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10612c0ec0e0a1ff0e97980647cb058a6e7aedb913d01d009c406b8b7d0b26ee" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "2.33.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" +dependencies = [ + "ansi_term", + "atty", + "bitflags", + "strsim 0.8.0", + "textwrap", + "unicode-width", + "vec_map", +] + +[[package]] +name = "claxon" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bfbf56724aa9eca8afa4fcfadeb479e722935bb2a0900c2d37e0cc477af0688" + +[[package]] +name = "combine" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a909e4d93292cd8e9c42e189f61681eff9d67b6541f96b8a1a737f23737bd001" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "const_fn" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f92cfa0fd5690b3cf8c1ef2cabbd9b7ef22fa53cf5e1f92b05103f6d5d1cf6e7" + +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + +[[package]] +name = "cookie-factory" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396de984970346b0d9e93d1415082923c679e5ae5c3ee3dcbd104f5610af126b" + +[[package]] +name = "core-foundation-sys" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b" + +[[package]] +name = "coreaudio-rs" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11894b20ebfe1ff903cbdc52259693389eea03b94918a2def2c30c3bf227ad88" +dependencies = [ + "bitflags", + "coreaudio-sys", +] + +[[package]] +name = "coreaudio-sys" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b7e3347be6a09b46aba228d6608386739fb70beff4f61e07422da87b0bb31fa" +dependencies = [ + "bindgen 0.56.0", +] + +[[package]] +name = "cpal" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98f45f0a21f617cd2c788889ef710b63f075c949259593ea09c826f1e47a2418" +dependencies = [ + "alsa", + "core-foundation-sys", + "coreaudio-rs", + "jni", + "js-sys", + "lazy_static", + "libc", + "mach", + "ndk 0.3.0", + "ndk-glue 0.3.0", + "nix 0.20.0", + "oboe", + "parking_lot", + "stdweb 0.1.3", + "thiserror", + "web-sys", + "winapi", +] + +[[package]] +name = "crossbeam" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae5588f6b3c3cb05239e90bd110f257254aecd01e4635400391aeae07497845" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-channel 0.5.1", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils 0.8.5", +] + +[[package]] +name = "crossbeam-channel" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b153fe7cbef478c567df0f972e02e6d736db11affe43dfc9c56a9374d1adfb87" +dependencies = [ + "crossbeam-utils 0.7.2", + "maybe-uninit", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ed27e177f16d65f0f0c22a213e17c696ace5dd64b14258b52f9417ccb52db4" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-utils 0.8.5", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-epoch", + "crossbeam-utils 0.8.5", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec02e091aa634e2c3ada4a392989e7c3116673ef0ac5b72232439094d73b7fd" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-utils 0.8.5", + "lazy_static", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b10ddc024425c88c2ad148c1b0fd53f4c6d38db9697c9f1588381212fa657c9" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-utils 0.8.5", +] + +[[package]] +name = "crossbeam-utils" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" +dependencies = [ + "autocfg", + "cfg-if 0.1.10", + "lazy_static", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db" +dependencies = [ + "cfg-if 1.0.0", + "lazy_static", +] + +[[package]] +name = "darling" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.9.3", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "defer-drop" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "828aca0e5e4341b0320a319209cbc6255b8b06254849ce8a5f33d33f7f2fa0f0" +dependencies = [ + "crossbeam-channel 0.4.4", + "once_cell", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_builder" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2658621297f2cf68762a6f7dc0bb7e1ff2cfd6583daef8ee0fed6f7ec468ec0" +dependencies = [ + "darling", + "derive_builder_core", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_builder_core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2791ea3e372c8495c0bc2033991d76b512cd799d07491fbd6890124db9458bef" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dirs" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fd78930633bd1c6e35c4b42b1df7b0cbc6bc191146e512bb3bedf243fcc3901" +dependencies = [ + "libc", + "redox_users 0.3.5", + "winapi", +] + +[[package]] +name = "dirs" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13aea89a5c93364a98e9b37b2fa237effbb694d5cfe01c5b70941f7eb087d5e3" +dependencies = [ + "cfg-if 0.1.10", + "dirs-sys", +] + +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if 1.0.0", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03d86534ed367a67548dc68113a0f5db55432fdfbb6e6f9d77704397d95d5780" +dependencies = [ + "libc", + "redox_users 0.4.0", + "winapi", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users 0.4.0", + "winapi", +] + +[[package]] +name = "discard" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" + +[[package]] +name = "dizi" +version = "0.1.0" +dependencies = [ + "alphanumeric-sort", + "chrono", + "dirs-next", + "dizi_commands", + "globset", + "lazy_static", + "libc", + "pipewire", + "rodio", + "rustyline", + "serde", + "serde_derive", + "serde_json", + "shell-words", + "shellexpand", + "signal-hook", + "skim", + "structopt", + "termion", + "toml", + "tui", + "unicode-segmentation", + "unicode-width", + "xdg", +] + +[[package]] +name = "dizi_commands" +version = "0.1.0" +dependencies = [ + "globset", + "rodio", + "serde", + "serde_derive", + "serde_json", +] + +[[package]] +name = "either" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" + +[[package]] +name = "env_logger" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "errno" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68f2fb9cae9d37c9b2b3584aba698a2e97f72d7aef7b9f7aa71d8b54ce46fe" +dependencies = [ + "errno-dragonfly", + "libc", + "winapi", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "funty" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" + +[[package]] +name = "fuzzy-matcher" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" +dependencies = [ + "thread_local", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi 0.10.2+wasi-snapshot-preview1", +] + +[[package]] +name = "glob" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" + +[[package]] +name = "globset" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10463d9ff00a2a068db14231982f5132edebad0d7660cd956a1c30292dbcbfbd" +dependencies = [ + "aho-corasick", + "bstr", + "fnv", + "log", + "regex", +] + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hound" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a164bb2ceaeff4f42542bdb847c41517c78a60f5649671b2a07312b6e117549" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "instant" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "716d3d89f35ac6a34fd0eed635395f4c3b76fa889338a4632e5231a8684216bd" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "itertools" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69ddb889f9d0d08a67338271fa9b62996bc788c7796a5c18cf057420aaed5eaf" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" + +[[package]] +name = "jni" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" +dependencies = [ + "cesu8", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "jobserver" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af25a77299a7f711a01975c35a6a424eb6862092cc2d6c72c4ed6cbc56dfc1fa" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "lewton" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "777b48df9aaab155475a83a7df3070395ea1ac6902f5cd062b8f2b028075c030" +dependencies = [ + "byteorder", + "ogg", + "tinyvec", +] + +[[package]] +name = "lexical-core" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe" +dependencies = [ + "arrayvec", + "bitflags", + "cfg-if 1.0.0", + "ryu", + "static_assertions", +] + +[[package]] +name = "libc" +version = "0.2.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8f7255a17a627354f321ef0055d63b898c6fb27eff628af4d1b66b7331edf6" + +[[package]] +name = "libloading" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f84d96438c15fcd6c3f244c8fce01d1e2b9c6b5623e9c711dc9286d8fc92d6a" +dependencies = [ + "cfg-if 1.0.0", + "winapi", +] + +[[package]] +name = "libspa" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aeb373e8b03740369c5fe48a557c6408b6898982d57e17940de144375d472743" +dependencies = [ + "bitflags", + "cc", + "cookie-factory", + "errno", + "libc", + "libspa-sys", + "nom 6.2.1", + "system-deps", +] + +[[package]] +name = "libspa-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d301a2fc2fed0a97c13836408a4d98f419af0c2695ecf74e634a214c17beefa6" +dependencies = [ + "bindgen 0.59.1", + "system-deps", +] + +[[package]] +name = "lock_api" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "mach" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa" +dependencies = [ + "libc", +] + +[[package]] +name = "maybe-uninit" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" + +[[package]] +name = "memchr" +version = "2.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" + +[[package]] +name = "memoffset" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59accc507f1338036a0477ef61afdae33cde60840f4dfe481319ce3ad116ddf9" +dependencies = [ + "autocfg", +] + +[[package]] +name = "minimp3" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985438f75febf74c392071a975a29641b420dd84431135a6e6db721de4b74372" +dependencies = [ + "minimp3-sys", + "slice-deque", + "thiserror", +] + +[[package]] +name = "minimp3-sys" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e21c73734c69dc95696c9ed8926a2b393171d98b3f5f5935686a26a487ab9b90" +dependencies = [ + "cc", +] + +[[package]] +name = "ndk" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8794322172319b972f528bf90c6b467be0079f1fa82780ffb431088e741a73ab" +dependencies = [ + "jni-sys", + "ndk-sys", + "num_enum", + "thiserror", +] + +[[package]] +name = "ndk" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d64d6af06fde0e527b1ba5c7b79a6cc89cfc46325b0b2887dffe8f70197e0c3c" +dependencies = [ + "bitflags", + "jni-sys", + "ndk-sys", + "num_enum", + "thiserror", +] + +[[package]] +name = "ndk-glue" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5caf0c24d51ac1c905c27d4eda4fa0635bbe0de596b8f79235e0b17a4d29385" +dependencies = [ + "lazy_static", + "libc", + "log", + "ndk 0.3.0", + "ndk-macro", + "ndk-sys", +] + +[[package]] +name = "ndk-glue" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e9e94628f24e7a3cb5b96a2dc5683acd9230bf11991c2a1677b87695138420" +dependencies = [ + "lazy_static", + "libc", + "log", + "ndk 0.4.0", + "ndk-macro", + "ndk-sys", +] + +[[package]] +name = "ndk-macro" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05d1c6307dc424d0f65b9b06e94f88248e6305726b14729fd67a5e47b2dc481d" +dependencies = [ + "darling", + "proc-macro-crate 0.1.5", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "ndk-sys" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c44922cb3dbb1c70b5e5f443d63b64363a898564d739ba5198e3a9138442868d" + +[[package]] +name = "nix" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dbdc256eaac2e3bd236d93ad999d3479ef775c863dbda3068c4006a92eec51b" +dependencies = [ + "bitflags", + "cc", + "cfg-if 0.1.10", + "libc", + "void", +] + +[[package]] +name = "nix" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c722bee1037d430d0f8e687bbdbf222f27cc6e4e68d5caf630857bb2b6dbdce" +dependencies = [ + "bitflags", + "cc", + "cfg-if 0.1.10", + "libc", + "void", +] + +[[package]] +name = "nix" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ccba0cfe4fdf15982d1674c69b1fd80bad427d293849982668dfe454bd61f2" +dependencies = [ + "bitflags", + "cc", + "cfg-if 1.0.0", + "libc", +] + +[[package]] +name = "nix" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa9b4819da1bc61c0ea48b63b7bc8604064dd43013e7cc325df098d49cd7c18a" +dependencies = [ + "bitflags", + "cc", + "cfg-if 1.0.0", + "libc", +] + +[[package]] +name = "nom" +version = "5.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af" +dependencies = [ + "memchr", + "version_check", +] + +[[package]] +name = "nom" +version = "6.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c5c51b9083a3c620fa67a2a635d1ce7d95b897e957d6b28ff9a5da960a103a6" +dependencies = [ + "bitvec", + "funty", + "lexical-core", + "memchr", + "version_check", +] + +[[package]] +name = "num-derive" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "num_enum" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9bd055fb730c4f8f4f57d45d35cd6b3f0980535b056dc7ff119cee6a66ed6f" +dependencies = [ + "derivative", + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "486ea01961c4a818096de679a8b740b26d9033146ac5291b1c98557658f8cdd9" +dependencies = [ + "proc-macro-crate 1.1.0", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "numtoa" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" + +[[package]] +name = "oboe" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e15e22bc67e047fe342a32ecba55f555e3be6166b04dd157cd0f803dfa9f48e1" +dependencies = [ + "jni", + "ndk 0.4.0", + "ndk-glue 0.4.0", + "num-derive", + "num-traits", + "oboe-sys", +] + +[[package]] +name = "oboe-sys" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "338142ae5ab0aaedc8275aa8f67f460e43ae0fca76a695a742d56da0a269eadc" +dependencies = [ + "cc", +] + +[[package]] +name = "ogg" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6951b4e8bf21c8193da321bcce9c9dd2e13c858fe078bf9054a288b419ae5d6e" +dependencies = [ + "byteorder", +] + +[[package]] +name = "once_cell" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" + +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" +dependencies = [ + "cfg-if 1.0.0", + "instant", + "libc", + "redox_syscall 0.2.10", + "smallvec", + "winapi", +] + +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + +[[package]] +name = "pipewire" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de050d879e7b8d9313429ec314b88b26fe48ba29a6ecc3bc8289d3673fee6c8" +dependencies = [ + "anyhow", + "bitflags", + "errno", + "libc", + "libspa", + "libspa-sys", + "once_cell", + "pipewire-sys", + "signal", + "thiserror", +] + +[[package]] +name = "pipewire-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4aa5ef9f3afef7dbb335106f69bd6bb541259e8796c693810cde20db1eb949" +dependencies = [ + "bindgen 0.59.1", + "libspa-sys", + "system-deps", +] + +[[package]] +name = "pkg-config" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c9b1041b4387893b91ee6746cddfc28516aff326a3519fb2adf820932c5e6cb" + +[[package]] +name = "proc-macro-crate" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" +dependencies = [ + "toml", +] + +[[package]] +name = "proc-macro-crate" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebace6889caf889b4d3f76becee12e90353f2b8c7d875534a71e5742f8f6f83" +dependencies = [ + "thiserror", + "toml", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" + +[[package]] +name = "proc-macro2" +version = "1.0.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9f5105d4fdaab20335ca9565e106a5d9b82b6219b5ba735731124ac6711d23d" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radium" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8" + +[[package]] +name = "rayon" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06aca804d41dbc8ba42dfd964f0d01334eceb64314b9ecf7c5fad5188a06d90" +dependencies = [ + "autocfg", + "crossbeam-deque", + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78120e2c850279833f1dd3582f730c4ab53ed95aeaaaa862a2a5c71b1656d8e" +dependencies = [ + "crossbeam-channel 0.5.1", + "crossbeam-deque", + "crossbeam-utils 0.8.5", + "lazy_static", + "num_cpus", +] + +[[package]] +name = "redox_syscall" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" + +[[package]] +name = "redox_syscall" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_termios" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8440d8acb4fd3d277125b4bd01a6f38aee8d814b3b5fc09b3f2b825d37d3fe8f" +dependencies = [ + "redox_syscall 0.2.10", +] + +[[package]] +name = "redox_users" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d" +dependencies = [ + "getrandom 0.1.16", + "redox_syscall 0.1.57", + "rust-argon2", +] + +[[package]] +name = "redox_users" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" +dependencies = [ + "getrandom 0.2.3", + "redox_syscall 0.2.10", +] + +[[package]] +name = "regex" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a26af418b574bd56588335b3a3659a65725d4e636eb1016c2f9e3b38c7cc759" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + +[[package]] +name = "rodio" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d98f5e557b61525057e2bc142c8cd7f0e70d75dc32852309bec440e6e046bf9" +dependencies = [ + "claxon", + "cpal", + "hound", + "lewton", + "minimp3", +] + +[[package]] +name = "rust-argon2" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb" +dependencies = [ + "base64", + "blake2b_simd", + "constant_time_eq", + "crossbeam-utils 0.8.5", +] + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +dependencies = [ + "semver", +] + +[[package]] +name = "rustyline" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f47ea1ceb347d2deae482d655dc8eef4bd82363d3329baffa3818bd76fea48b" +dependencies = [ + "dirs 1.0.5", + "libc", + "log", + "memchr", + "nix 0.13.1", + "unicode-segmentation", + "unicode-width", + "utf8parse 0.1.1", + "winapi", +] + +[[package]] +name = "ryu" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + +[[package]] +name = "serde" +version = "1.0.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" + +[[package]] +name = "serde_derive" +version = "1.0.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d" + +[[package]] +name = "shell-words" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fa3938c99da4914afedd13bf3d79bcb6c277d1b2c398d23257a304d9e1b074" + +[[package]] +name = "shellexpand" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83bdb7831b2d85ddf4a7b148aa19d0587eddbe8671a436b7bd1182eaad0f2829" +dependencies = [ + "dirs-next", +] + +[[package]] +name = "shlex" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fdf1b9db47230893d76faad238fd6097fd6d6a9245cd7a4d90dbd639536bbd2" + +[[package]] +name = "shlex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" + +[[package]] +name = "signal" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f6ce83b159ab6984d2419f495134972b48754d13ff2e3f8c998339942b56ed9" +dependencies = [ + "libc", + "nix 0.14.1", +] + +[[package]] +name = "signal-hook" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c98891d737e271a2954825ef19e46bd16bdb98e2746f2eec4f7a4ef7946efd1" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +dependencies = [ + "libc", +] + +[[package]] +name = "skim" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b9d19f904221fab15163486d2ce116cb86e60296470bb4e956d6687f04ebbb4" +dependencies = [ + "atty", + "beef", + "bitflags", + "chrono", + "clap", + "crossbeam", + "defer-drop", + "derive_builder", + "env_logger", + "fuzzy-matcher", + "lazy_static", + "log", + "nix 0.19.1", + "rayon", + "regex", + "shlex 0.1.1", + "time 0.2.27", + "timer", + "tuikit", + "unicode-width", + "vte", +] + +[[package]] +name = "slice-deque" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31ef6ee280cdefba6d2d0b4b78a84a1c1a3f3a4cec98c2d4231c8bc225de0f25" +dependencies = [ + "libc", + "mach", + "winapi", +] + +[[package]] +name = "smallvec" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309" + +[[package]] +name = "standback" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e113fb6f3de07a243d434a56ec6f186dfd51cb08448239fe7bcae73f87ff28ff" +dependencies = [ + "version_check", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "stdweb" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef5430c8e36b713e13b48a9f709cc21e046723fe44ce34587b73a830203b533e" + +[[package]] +name = "stdweb" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5" +dependencies = [ + "discard", + "rustc_version", + "stdweb-derive", + "stdweb-internal-macros", + "stdweb-internal-runtime", + "wasm-bindgen", +] + +[[package]] +name = "stdweb-derive" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "serde_derive", + "syn", +] + +[[package]] +name = "stdweb-internal-macros" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11" +dependencies = [ + "base-x", + "proc-macro2", + "quote", + "serde", + "serde_derive", + "serde_json", + "sha1", + "syn", +] + +[[package]] +name = "stdweb-internal-runtime" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" + +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[package]] +name = "strsim" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" + +[[package]] +name = "structopt" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf9d950ef167e25e0bdb073cf1d68e9ad2795ac826f2f3f59647817cf23c0bfa" +dependencies = [ + "clap", + "lazy_static", + "structopt-derive", +] + +[[package]] +name = "structopt-derive" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "134d838a2c9943ac3125cf6df165eda53493451b719f3255b2a26b85f772d0ba" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "strum" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aaf86bbcfd1fa9670b7a129f64fc0c9fcbbfe4f1bc4210e9e98fe71ffc12cde2" + +[[package]] +name = "strum_macros" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d06aaeeee809dbc59eb4556183dd927df67db1540de5be8d3ec0b6636358a5ec" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "1.0.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4eac2e6c19f5c3abc0c229bea31ff0b9b091c7b14990e8924b92902a303a0c0" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "system-deps" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "480c269f870722b3b08d2f13053ce0c2ab722839f472863c3e2d61ff3a1c2fa6" +dependencies = [ + "anyhow", + "cfg-expr", + "heck", + "itertools", + "pkg-config", + "strum", + "strum_macros", + "thiserror", + "toml", + "version-compare", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "term" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0863a3345e70f61d613eab32ee046ccd1bcc5f9105fe402c61fcd0c13eeb8b5" +dependencies = [ + "dirs 2.0.2", + "winapi", +] + +[[package]] +name = "termcolor" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "termion" +version = "1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "077185e2eac69c3f8379a4298e1e07cd36beb962290d4a51199acf0fdc10607e" +dependencies = [ + "libc", + "numtoa", + "redox_syscall 0.2.10", + "redox_termios", +] + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "thiserror" +version = "1.0.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "602eca064b2d83369e2b2f34b09c70b605402801927c65c11071ac911d299b88" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad553cc2c78e8de258400763a647e80e6d1b31ee237275d756f6836d204494c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8018d24e04c95ac8790716a5987d0fec4f8b27249ffa0f7d33f1369bdfb88cbd" +dependencies = [ + "once_cell", +] + +[[package]] +name = "time" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "time" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4752a97f8eebd6854ff91f1c1824cd6160626ac4bd44287f7f4ea2035a02a242" +dependencies = [ + "const_fn", + "libc", + "standback", + "stdweb 0.4.20", + "time-macros", + "version_check", + "winapi", +] + +[[package]] +name = "time-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "957e9c6e26f12cb6d0dd7fc776bb67a706312e7299aed74c8dd5b17ebb27e2f1" +dependencies = [ + "proc-macro-hack", + "time-macros-impl", +] + +[[package]] +name = "time-macros-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3c141a1b43194f3f56a1411225df8646c55781d5f26db825b3d98507eb482f" +dependencies = [ + "proc-macro-hack", + "proc-macro2", + "quote", + "standback", + "syn", +] + +[[package]] +name = "timer" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31d42176308937165701f50638db1c31586f183f1aab416268216577aec7306b" +dependencies = [ + "chrono", +] + +[[package]] +name = "tinyvec" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83b2a3d4d9091d0abd7eba4dc2710b1718583bd4d8992e2190720ea38f391f7" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "toml" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +dependencies = [ + "serde", +] + +[[package]] +name = "tui" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39c8ce4e27049eed97cfa363a5048b09d995e209994634a0efc26a14ab6c0c23" +dependencies = [ + "bitflags", + "cassowary", + "termion", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "tuikit" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c628cfc5752254a33ebccf73eb79ef6508fab77de5d5ef76246b5e45010a51f" +dependencies = [ + "bitflags", + "lazy_static", + "log", + "nix 0.14.1", + "term", + "unicode-width", +] + +[[package]] +name = "unicode-segmentation" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" + +[[package]] +name = "unicode-width" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "utf8parse" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8772a4ccbb4e89959023bc5b7cb8623a795caa7092d99f3aa9501b9484d4557d" + +[[package]] +name = "utf8parse" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "936e4b492acfd135421d8dca4b1aa80a7bfc26e702ef3af710e0752684df5372" + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "version-compare" +version = "0.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c18c859eead79d8b95d09e4678566e8d70105c4e7b251f707a03df32442661b" + +[[package]] +name = "version_check" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" + +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" + +[[package]] +name = "vte" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e7745610024d50ab1ebfa41f8f8ee361c567f7ab51032f93cc1cc4cbf0c547a" +dependencies = [ + "arrayvec", + "utf8parse 0.2.0", + "vte_generate_state_changes", +] + +[[package]] +name = "vte_generate_state_changes" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "walkdir" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +dependencies = [ + "same-file", + "winapi", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" + +[[package]] +name = "wasm-bindgen" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce" +dependencies = [ + "cfg-if 1.0.0", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc" + +[[package]] +name = "web-sys" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "which" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d011071ae14a2f6671d0b74080ae0cd8ebf3a6f8c9589a2cd45f23126fe29724" +dependencies = [ + "libc", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "wyz" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" + +[[package]] +name = "xdg" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d089681aa106a86fade1b0128fb5daf07d5867a509ab036d99988dec80429a57" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..4737c89 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "dizi" +version = "0.1.0" +authors = ["Jeff Zhao "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[[bin]] +name = "dizi-server" +path = "src/server/main.rs" + +[[bin]] +name = "dizi" +path = "src/client/main.rs" + +[dependencies] +alphanumeric-sort = "^1" +chrono = "^0" +dirs-next = "^2" +dizi_commands = { path = "lib/dizi_commands", version = "^0" } +globset = "^0" +libc = "^0" +lazy_static = "^1" +pipewire = "^0" +rodio = "^0" +rustyline = "^4" +serde = "^1" +serde_derive = "^1" +serde_json = "^1" +shell-words = "^1" +shellexpand = "^2" +signal-hook = "^0" +skim = "^0" +structopt = "^0" +termion = "^1" +toml = "^0" +tui = "^0" +unicode-width = "^0" +unicode-segmentation = "^1" +xdg = "^2" diff --git a/README.md b/README.md new file mode 100644 index 0000000..8f21ba5 --- /dev/null +++ b/README.md @@ -0,0 +1,46 @@ +# dizi +Server-client music player written in Rust (WIP) + +The goal of this project is to create a modern version of mocp in Rust. + +## Why? +mocp currently interfaces with ALSA to play audio. +This doesn't play well with pipewire-alsa plugin; +whenever mocp is playing music, other audio/video apps stop working and vice versa. + +## Dependencies + - [cargo](https://github.com/rust-lang/cargo/) + - [rustc](https://www.rust-lang.org/) + +## Building +``` +~$ cargo build +``` + +## Installation +#### For single user +``` +~$ cargo install --path=. --force +``` + +#### System wide +``` +~# cargo install --path=. --force --root=/usr/local # /usr also works +``` + +## Usage +``` +~ $ dizi +``` + +## TODOs + + - [ ] Pipewire to play audio + - [ ] Pulseaudio to play audio + - [ ] play/pause support + - [ ] playlist support + - [ ] show music progress + - [ ] shuffle, repeat, next + - [ ] volume support + - [ ] theming support + - [ ] custom layout support diff --git a/config/client.toml b/config/client.toml new file mode 100644 index 0000000..45b8dbf --- /dev/null +++ b/config/client.toml @@ -0,0 +1,18 @@ +[client] +socket = "/tmp/kamiyaa/dizi-server-socket" + +home_dir = "~/music" + +[client.player] +shuffle = false +repeat = true +next = true +# on_song_change = "" + +[client.display] +show_borders = true +show_hidden = false + +[client.display.sort] +reverse = false +sort_method = "natural" diff --git a/config/keymap.toml b/config/keymap.toml new file mode 100644 index 0000000..01932ce --- /dev/null +++ b/config/keymap.toml @@ -0,0 +1,46 @@ +mapcommand = [ + { keys = [ "q" ], command = "close" }, + { keys = [ "Q" ], command = "quit" }, + + { keys = [ "R" ], command = "reload_dirlist" }, + { keys = [ "z", "h" ], command = "toggle_hidden" }, + + # arrow keys + { keys = [ "arrow_up" ], command = "cursor_move_up" }, + { keys = [ "arrow_down" ], command = "cursor_move_down" }, + { keys = [ "arrow_left" ], command = "cd .." }, + { keys = [ "arrow_right" ], command = "open" }, + { keys = [ "\n" ], command = "open" }, + { keys = [ "end" ], command = "cursor_move_end" }, + { keys = [ "home" ], command = "cursor_move_home" }, + { keys = [ "page_up" ], command = "cursor_move_page_up" }, + { keys = [ "page_down" ], command = "cursor_move_page_down" }, + + { keys = [ "c", "d" ], command = ":cd " }, + + { keys = [ " " ], command = "select --toggle=true" }, + { keys = [ "t" ], command = "select --all=true --toggle=true" }, + + { keys = [ ":" ], command = ":" }, + { keys = [ ";" ], command = ":" }, + + { keys = [ "/" ], command = ":search " }, + { keys = [ "\\" ], command = ":search_glob " }, + { keys = [ "S" ], command = ":search_skim " }, + + { keys = [ "n" ], command = "search_next" }, + { keys = [ "N" ], command = "search_prev" }, + + { keys = [ "s", "r" ], command = "sort reverse" }, + { keys = [ "s", "l" ], command = "sort lexical" }, + { keys = [ "s", "m" ], command = "sort mtime" }, + { keys = [ "s", "n" ], command = "sort natural" }, + { keys = [ "s", "s" ], command = "sort size" }, + { keys = [ "s", "e" ], command = "sort ext" }, + + { keys = [ "g", "r" ], command = "cd /" }, + { keys = [ "g", "c" ], command = "cd ~/.config" }, + { keys = [ "g", "d" ], command = "cd ~/Downloads" }, + { keys = [ "g", "e" ], command = "cd /etc" }, + { keys = [ "g", "h" ], command = "cd ~/" }, +] diff --git a/config/server.toml b/config/server.toml new file mode 100644 index 0000000..d8d8843 --- /dev/null +++ b/config/server.toml @@ -0,0 +1,2 @@ +[server] +socket = "/tmp/kamiyaa/dizi-server-socket" diff --git a/config/theme.toml b/config/theme.toml new file mode 100644 index 0000000..c949233 --- /dev/null +++ b/config/theme.toml @@ -0,0 +1,42 @@ +[regular] +fg = "white" + +[directory] +fg = "light_blue" +bold = true + +[link] +fg = "cyan" +bold = true + +[ext] + +[ext.m3u8] +fg = "red" + +[ext.wav] +fg = "magenta" +[ext.flac] +fg = "magenta" +[ext.mp3] +fg = "magenta" +[ext.avi] +fg = "magenta" +[ext.m3u] +fg = "magenta" +[ext.mov] +fg = "magenta" +[ext.m4v] +fg = "magenta" +[ext.mp4] +fg = "magenta" +[ext.mkv] +fg = "magenta" +[ext.m4a] +fg = "magenta" +[ext.ts] +fg = "magenta" +[ext.webm] +fg = "magenta" +[ext.wmv] +fg = "magenta" diff --git a/lib/dizi_commands/Cargo.toml b/lib/dizi_commands/Cargo.toml new file mode 100644 index 0000000..e62190a --- /dev/null +++ b/lib/dizi_commands/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "dizi_commands" +version = "0.1.0" +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +globset = "^0" +rodio = "^0" +serde = "^1" +serde_derive = "^1" +serde_json = "^1" diff --git a/lib/dizi_commands/src/api_command.rs b/lib/dizi_commands/src/api_command.rs new file mode 100644 index 0000000..f42bf01 --- /dev/null +++ b/lib/dizi_commands/src/api_command.rs @@ -0,0 +1,86 @@ +use crate::constants::*; +use crate::error::{DiziError, DiziErrorKind}; + +#[derive(Copy, Clone, Debug)] +pub enum ApiCommand { + Quit, + + PlaylistGet, + PlaylistAdd, + PlaylistRemove, + + PlayerGet, + PlayerPlay, + PlayerPause, + PlayerTogglePlay, + PlayerToggleShuffle, + PlayerToggleRepeat, + PlayerToggleNext, + + PlayerVolumeUp, + PlayerVolumeDown, + + PlayerRewind, + PlayerFastForward, +} + +impl ApiCommand { + pub fn to_str(&self) -> &'static str { + match self { + Self::Quit => API_QUIT, + + Self::PlaylistGet => API_PLAYLIST_GET, + Self::PlaylistAdd => API_PLAYLIST_ADD, + Self::PlaylistRemove => API_PLAYLIST_REMOVE, + + Self::PlayerGet => API_PLAYER_GET, + Self::PlayerPlay => API_PLAYER_PLAY, + Self::PlayerPause => API_PLAYER_PAUSE, + + Self::PlayerTogglePlay => API_PLAYER_TOGGLE_PLAY, + Self::PlayerToggleShuffle => API_PLAYER_TOGGLE_SHUFFLE, + Self::PlayerToggleRepeat => API_PLAYER_TOGGLE_REPEAT, + Self::PlayerToggleNext => API_PLAYER_TOGGLE_NEXT, + + Self::PlayerVolumeUp => API_PLAYER_VOLUME_UP, + Self::PlayerVolumeDown => API_PLAYER_VOLUME_DOWN, + + Self::PlayerRewind => API_PLAYER_REWIND, + Self::PlayerFastForward => API_PLAYER_FAST_FORWARD, + } + } +} + +impl std::str::FromStr for ApiCommand { + type Err = DiziError; + + fn from_str(s: &str) -> Result { + match s { + API_QUIT => Ok(Self::Quit), + + API_PLAYLIST_GET => Ok(Self::PlaylistGet), + API_PLAYLIST_ADD => Ok(Self::PlaylistAdd), + API_PLAYLIST_REMOVE => Ok(Self::PlaylistRemove), + + API_PLAYER_GET => Ok(Self::PlayerGet), + API_PLAYER_PLAY => Ok(Self::PlayerPlay), + API_PLAYER_PAUSE => Ok(Self::PlayerPause), + + API_PLAYER_TOGGLE_PLAY => Ok(Self::PlayerTogglePlay), + API_PLAYER_TOGGLE_SHUFFLE => Ok(Self::PlayerToggleShuffle), + API_PLAYER_TOGGLE_REPEAT => Ok(Self::PlayerToggleRepeat), + API_PLAYER_TOGGLE_NEXT => Ok(Self::PlayerToggleNext), + + API_PLAYER_VOLUME_UP => Ok(Self::PlayerVolumeUp), + API_PLAYER_VOLUME_DOWN => Ok(Self::PlayerVolumeDown), + + API_PLAYER_REWIND => Ok(Self::PlayerRewind), + API_PLAYER_FAST_FORWARD => Ok(Self::PlayerFastForward), + + command => Err(DiziError::new( + DiziErrorKind::UnrecognizedCommand, + format!("Unrecognized command '{}'", command), + )) + } + } +} diff --git a/lib/dizi_commands/src/constants.rs b/lib/dizi_commands/src/constants.rs new file mode 100644 index 0000000..5edac70 --- /dev/null +++ b/lib/dizi_commands/src/constants.rs @@ -0,0 +1,20 @@ + +pub const API_QUIT: &str = "/quit"; + +pub const API_PLAYLIST_GET: &str = "/playlist/get"; +pub const API_PLAYLIST_ADD: &str = "/playlist/add"; +pub const API_PLAYLIST_REMOVE: &str = "/playlist/remove"; + +pub const API_PLAYER_GET: &str = "/player/get"; +pub const API_PLAYER_PLAY: &str = "/player/play"; +pub const API_PLAYER_PAUSE: &str = "/player/pause"; +pub const API_PLAYER_TOGGLE_PLAY: &str = "/player/toggle/play"; +pub const API_PLAYER_TOGGLE_SHUFFLE: &str = "/player/toggle/shuffle"; +pub const API_PLAYER_TOGGLE_REPEAT: &str = "/player/toggle/repeat"; +pub const API_PLAYER_TOGGLE_NEXT: &str = "/player/toggle/next"; + +pub const API_PLAYER_VOLUME_UP: &str = "/player/volume/increase"; +pub const API_PLAYER_VOLUME_DOWN: &str = "/player/volume/decrease"; + +pub const API_PLAYER_REWIND: &str = "/player/rewind"; +pub const API_PLAYER_FAST_FORWARD: &str = "/player/fastforward"; diff --git a/lib/dizi_commands/src/error/error_kind.rs b/lib/dizi_commands/src/error/error_kind.rs new file mode 100644 index 0000000..19a4d30 --- /dev/null +++ b/lib/dizi_commands/src/error/error_kind.rs @@ -0,0 +1,61 @@ +use std::io; + +#[derive(Debug)] +pub enum DiziErrorKind { + // io related + IoError(io::ErrorKind), + + // environment variable not found + EnvVarNotPresent, + + // parse error + ParseError, + ClipboardError, + + Glob, + + InvalidParameters, + + DecoderError, + PlayError, + StreamError(rodio::StreamError), + + UnrecognizedArgument, + UnrecognizedCommand, +} + +impl std::convert::From for DiziErrorKind { + fn from(err: io::ErrorKind) -> Self { + Self::IoError(err) + } +} + +impl std::convert::From<&globset::ErrorKind> for DiziErrorKind { + fn from(_: &globset::ErrorKind) -> Self { + Self::Glob + } +} + +impl std::convert::From for DiziErrorKind { + fn from(_: std::env::VarError) -> Self { + Self::EnvVarNotPresent + } +} + +impl std::convert::From for DiziErrorKind { + fn from(err: rodio::PlayError) -> Self { + Self::PlayError + } +} + +impl std::convert::From for DiziErrorKind { + fn from(err: rodio::StreamError) -> Self { + Self::StreamError(err) + } +} + +impl std::convert::From for DiziErrorKind { + fn from(err: rodio::decoder::DecoderError) -> Self { + Self::DecoderError + } +} diff --git a/lib/dizi_commands/src/error/error_type.rs b/lib/dizi_commands/src/error/error_type.rs new file mode 100644 index 0000000..e42dbe4 --- /dev/null +++ b/lib/dizi_commands/src/error/error_type.rs @@ -0,0 +1,80 @@ +use std::io; + +use super::DiziErrorKind; + +#[derive(Debug)] +pub struct DiziError { + _kind: DiziErrorKind, + _cause: String, +} + +impl DiziError { + pub fn new(_kind: DiziErrorKind, _cause: String) -> Self { + Self { _kind, _cause } + } + + pub fn kind(&self) -> &DiziErrorKind { + &self._kind + } +} + +impl std::fmt::Display for DiziError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", self._cause) + } +} + +impl std::convert::From for DiziError { + fn from(err: io::Error) -> Self { + Self { + _kind: DiziErrorKind::from(err.kind()), + _cause: err.to_string(), + } + } +} + +impl std::convert::From for DiziError { + fn from(err: globset::Error) -> Self { + Self { + _kind: DiziErrorKind::from(err.kind()), + _cause: err.to_string(), + } + } +} + +impl std::convert::From for DiziError { + fn from(err: std::env::VarError) -> Self { + Self { + _kind: DiziErrorKind::from(err), + _cause: "Environment variable not found".to_string(), + } + } +} + +impl std::convert::From for DiziError { + fn from(err: rodio::PlayError) -> Self { + let err_str = err.to_string(); + Self { + _kind: DiziErrorKind::from(err), + _cause: err_str, + } + } +} + +impl std::convert::From for DiziError { + fn from(err: rodio::StreamError) -> Self { + Self { + _kind: DiziErrorKind::from(err), + _cause: "Error with audio system".to_string(), + } + } +} + +impl std::convert::From for DiziError { + fn from(err: rodio::decoder::DecoderError) -> Self { + Self { + _kind: DiziErrorKind::from(err), + _cause: "Unsupported audio format".to_string(), + } + } +} diff --git a/lib/dizi_commands/src/error/mod.rs b/lib/dizi_commands/src/error/mod.rs new file mode 100644 index 0000000..f4ee428 --- /dev/null +++ b/lib/dizi_commands/src/error/mod.rs @@ -0,0 +1,7 @@ +mod error_kind; +mod error_type; + +pub use self::error_kind::DiziErrorKind; +pub use self::error_type::DiziError; + +pub type DiziResult = Result; diff --git a/lib/dizi_commands/src/lib.rs b/lib/dizi_commands/src/lib.rs new file mode 100644 index 0000000..cf61e1f --- /dev/null +++ b/lib/dizi_commands/src/lib.rs @@ -0,0 +1,4 @@ +pub mod api_command; +pub mod constants; +pub mod error; +pub mod structs; diff --git a/lib/dizi_commands/src/structs/mod.rs b/lib/dizi_commands/src/structs/mod.rs new file mode 100644 index 0000000..5f2a032 --- /dev/null +++ b/lib/dizi_commands/src/structs/mod.rs @@ -0,0 +1,3 @@ +pub mod player_play; + +pub use self::player_play::PlayerPlay; diff --git a/lib/dizi_commands/src/structs/player_play.rs b/lib/dizi_commands/src/structs/player_play.rs new file mode 100644 index 0000000..74c8981 --- /dev/null +++ b/lib/dizi_commands/src/structs/player_play.rs @@ -0,0 +1,8 @@ +use std::path::PathBuf; +use serde_derive::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct PlayerPlay { + pub command: String, + pub path: PathBuf, +} diff --git a/src/client/commands/change_directory.rs b/src/client/commands/change_directory.rs new file mode 100644 index 0000000..acbb9f6 --- /dev/null +++ b/src/client/commands/change_directory.rs @@ -0,0 +1,36 @@ +use std::path; +use std::io; + +use dizi_commands::error::DiziResult; + +use crate::context::AppContext; +use crate::history::DirectoryHistory; + +pub fn cd(path: &path::Path, context: &mut AppContext) -> io::Result<()> { + std::env::set_current_dir(path)?; + context.set_cwd(path); + Ok(()) +} + +fn _change_directory(path: &path::Path, context: &mut AppContext) -> io::Result<()> { + cd(path, context)?; + let options = context.config_ref().display_options_ref().clone(); + context + .history_mut() + .populate_to_root(path, &options)?; + + Ok(()) +} + +pub fn change_directory(context: &mut AppContext, path: &path::Path) -> DiziResult<()> { + let new_cwd = if path.is_absolute() { + path.canonicalize()? + } else { + let mut new_cwd = std::env::current_dir()?; + new_cwd.push(path.canonicalize()?); + new_cwd + }; + + _change_directory(new_cwd.as_path(), context)?; + Ok(()) +} diff --git a/src/client/commands/command_line.rs b/src/client/commands/command_line.rs new file mode 100644 index 0000000..a0a0c82 --- /dev/null +++ b/src/client/commands/command_line.rs @@ -0,0 +1,32 @@ +use std::str::FromStr; + +use dizi_commands::error::DiziResult; + +use crate::config::AppKeyMapping; +use crate::context::AppContext; +use crate::key_command::{AppExecute, Command}; +use crate::ui::views::TuiTextField; +use crate::ui::TuiBackend; + +pub fn read_and_execute( + context: &mut AppContext, + backend: &mut TuiBackend, + keymap_t: &AppKeyMapping, + prefix: &str, + suffix: &str, +) -> DiziResult<()> { + context.flush_event(); + let user_input: Option = TuiTextField::default() + .prompt(":") + .prefix(prefix) + .suffix(suffix) + .get_input(backend, context); + + if let Some(s) = user_input { + let trimmed = s.trim_start(); + let command = Command::from_str(trimmed)?; + command.execute(context, backend, keymap_t) + } else { + Ok(()) + } +} diff --git a/src/client/commands/cursor_move.rs b/src/client/commands/cursor_move.rs new file mode 100644 index 0000000..ddc9f23 --- /dev/null +++ b/src/client/commands/cursor_move.rs @@ -0,0 +1,137 @@ +use dizi_commands::error::DiziResult; + +use crate::context::AppContext; +use crate::ui::TuiBackend; + +pub fn cursor_move(new_index: usize, context: &mut AppContext) { + let mut new_index = new_index; + if let Some(curr_list) = context.curr_list_mut() { + if !curr_list.is_empty() { + let dir_len = curr_list.len(); + if new_index >= dir_len { + new_index = dir_len - 1; + } + curr_list.index = Some(new_index); + } + } +} + +pub fn up(context: &mut AppContext, u: usize) -> DiziResult<()> { + let movement = match context.curr_list_ref() { + Some(curr_list) => curr_list.index.map(|idx| if idx > u { idx - u } else { 0 }), + None => None, + }; + + if let Some(s) = movement { + cursor_move(s, context); + } + Ok(()) +} + +pub fn down(context: &mut AppContext, u: usize) -> DiziResult<()> { + let movement = match context.curr_list_ref() { + Some(curr_list) => curr_list.index.map(|idx| idx + u), + None => None, + }; + if let Some(s) = movement { + cursor_move(s, context); + } + Ok(()) +} + +pub fn home(context: &mut AppContext) -> DiziResult<()> { + let movement: Option = match context.curr_list_ref() { + Some(curr_list) => { + let len = curr_list.len(); + if len == 0 { + None + } else { + Some(0) + } + } + None => None, + }; + + if let Some(s) = movement { + cursor_move(s, context); + } + Ok(()) +} + +pub fn end(context: &mut AppContext) -> DiziResult<()> { + let movement: Option = match context.curr_list_ref() { + Some(curr_list) => { + let len = curr_list.len(); + if len == 0 { + None + } else { + Some(len - 1) + } + } + None => None, + }; + + if let Some(s) = movement { + cursor_move(s, context); + } + Ok(()) +} + +fn get_page_size(context: &AppContext, backend: &TuiBackend) -> Option { + let config = context.config_ref(); + let rect = backend.terminal.as_ref().map(|t| t.size())?.ok()?; + + let rect_height = rect.height as usize; + if config.display_options_ref().show_borders() { + if rect_height >= 4 { + Some(rect_height - 4) + } else { + None + } + } else { + if rect_height >= 2 { + Some(rect_height - 2) + } else { + None + } + } +} + +pub fn page_up(context: &mut AppContext, backend: &mut TuiBackend) -> DiziResult<()> { + let page_size = get_page_size(context, backend).unwrap_or(10); + + let movement = match context.curr_list_ref() { + Some(curr_list) => curr_list + .index + .map(|idx| if idx > page_size { idx - page_size } else { 0 }), + None => None, + }; + + if let Some(s) = movement { + cursor_move(s, context); + } + Ok(()) +} + +pub fn page_down(context: &mut AppContext, backend: &mut TuiBackend) -> DiziResult<()> { + let page_size = get_page_size(context, backend).unwrap_or(10); + + let movement = match context.curr_list_ref() { + Some(curr_list) => { + let dir_len = curr_list.len(); + curr_list.index.map(|idx| { + if idx + page_size > dir_len - 1 { + dir_len - 1 + } else { + idx + page_size + } + }) + } + None => None, + }; + + if let Some(s) = movement { + cursor_move(s, context); + } + Ok(()) +} diff --git a/src/client/commands/mod.rs b/src/client/commands/mod.rs new file mode 100644 index 0000000..e2aca11 --- /dev/null +++ b/src/client/commands/mod.rs @@ -0,0 +1,14 @@ +pub mod change_directory; +pub mod command_line; +pub mod cursor_move; +pub mod open_file; +pub mod parent_directory; +pub mod quit; +pub mod reload; +pub mod search; +pub mod search_glob; +pub mod search_skim; +pub mod search_string; +pub mod selection; +pub mod show_hidden; +pub mod sort; diff --git a/src/client/commands/open_file.rs b/src/client/commands/open_file.rs new file mode 100644 index 0000000..dd482a4 --- /dev/null +++ b/src/client/commands/open_file.rs @@ -0,0 +1,50 @@ +use std::io; +use std::io::{Read, Write}; +use std::path; + +use dizi_commands::error::{DiziError, DiziErrorKind, DiziResult}; +use dizi_commands::structs::PlayerPlay; +use dizi_commands::api_command::ApiCommand; + +use crate::context::AppContext; +use crate::fs::DirEntry; +use crate::ui::views::TuiTextField; +use crate::ui::TuiBackend; + +use super::change_directory; + +pub const NEWLINE: &[u8] = &['\n' as u8]; + +pub fn open(context: &mut AppContext, backend: &mut TuiBackend) -> DiziResult<()> { + let config = context.config_ref(); + + if let Some(entry) = context + .curr_list_ref() + .and_then(|s| s.curr_entry_ref()) + { + if entry.file_path().is_dir() { + let path = entry.file_path().to_path_buf(); + change_directory::cd(path.as_path(), context)?; + } else { + play_file(context, backend, entry.file_path().to_path_buf()); + } + } + Ok(()) +} + +pub fn play_file(context: &mut AppContext, backend: &mut TuiBackend, path: path::PathBuf) -> DiziResult<()> { + let request = PlayerPlay { + command: ApiCommand::PlayerPlay.to_str().to_string(), + path, + }; + + eprintln!("{:?}", request); + + let json = serde_json::to_string(&request).unwrap(); + eprintln!("{:?}", json); + + let res = context.stream.write(json.as_bytes()); + eprintln!("{:?}", res); + let res = context.stream.write(NEWLINE); + Ok(()) +} diff --git a/src/client/commands/parent_directory.rs b/src/client/commands/parent_directory.rs new file mode 100644 index 0000000..cfb6045 --- /dev/null +++ b/src/client/commands/parent_directory.rs @@ -0,0 +1,22 @@ +use dizi_commands::error::DiziResult; +use crate::commands::reload; +use crate::context::AppContext; + +pub fn parent_directory_helper(context: &mut AppContext) -> std::io::Result<()> { + if let Some(parent) = context + .cwd() + .parent() + .map(|p| p.to_path_buf()) + { + std::env::set_current_dir(&parent)?; + context + .set_cwd(parent.as_path()); + } + Ok(()) +} + +pub fn parent_directory(context: &mut AppContext) -> DiziResult<()> { + parent_directory_helper(context)?; + reload::soft_reload(context)?; + Ok(()) +} diff --git a/src/client/commands/quit.rs b/src/client/commands/quit.rs new file mode 100644 index 0000000..27f5fb1 --- /dev/null +++ b/src/client/commands/quit.rs @@ -0,0 +1,13 @@ + +use crate::context::{AppContext, QuitType}; +use dizi_commands::error::DiziResult; + +pub fn close(context: &mut AppContext) -> DiziResult<()> { + context.quit = QuitType::Normal; + Ok(()) +} + +pub fn quit_server(context: &mut AppContext) -> DiziResult<()> { + context.quit = QuitType::Server; + Ok(()) +} diff --git a/src/client/commands/reload.rs b/src/client/commands/reload.rs new file mode 100644 index 0000000..822808f --- /dev/null +++ b/src/client/commands/reload.rs @@ -0,0 +1,49 @@ +use dizi_commands::error::DiziResult; + +use crate::context::AppContext; +use crate::history::create_dirlist_with_history; + +// reload only if we have a queued reload +pub fn soft_reload(context: &mut AppContext) -> std::io::Result<()> { + let mut paths = Vec::with_capacity(1); + if let Some(curr_list) = context.curr_list_ref() { + if curr_list.need_update() { + paths.push(curr_list.file_path().to_path_buf()); + } + } + + if !paths.is_empty() { + let options = context.config_ref().display_options_ref().clone(); + let mut history = context.history_mut(); + for path in paths { + let new_dirlist = create_dirlist_with_history(history, path.as_path(), &options)?; + history.insert(path, new_dirlist); + } + } + Ok(()) +} + +pub fn reload(context: &mut AppContext) -> std::io::Result<()> { + let mut paths = Vec::with_capacity(1); + if let Some(curr_list) = context.curr_list_ref() { + paths.push(curr_list.file_path().to_path_buf()); + } + + if !paths.is_empty() { + let options = context.config_ref().display_options_ref().clone(); + let mut history = context.history_mut(); + for path in paths { + let new_dirlist = create_dirlist_with_history(history, path.as_path(), &options)?; + history.insert(path, new_dirlist); + } + } + context + .message_queue_mut() + .push_success("Directory listing reloaded!".to_string()); + Ok(()) +} + +pub fn reload_dirlist(context: &mut AppContext) -> DiziResult<()> { + reload(context)?; + Ok(()) +} diff --git a/src/client/commands/search.rs b/src/client/commands/search.rs new file mode 100644 index 0000000..0870073 --- /dev/null +++ b/src/client/commands/search.rs @@ -0,0 +1,42 @@ +use dizi_commands::error::DiziResult; + +use crate::context::AppContext; +use crate::util::search::SearchPattern; + +use super::cursor_move; +use super::search_glob; +use super::search_string; + +pub fn search_next(context: &mut AppContext) -> DiziResult<()> { + if let Some(search_context) = context.get_search_context() { + let index = match search_context { + SearchPattern::Glob(s) => { + search_glob::search_glob_fwd(context.curr_list_ref().unwrap(), s) + } + SearchPattern::String(s) => { + search_string::search_string_fwd(context.curr_list_ref().unwrap(), s) + } + }; + if let Some(index) = index { + let _ = cursor_move::cursor_move(index, context); + } + } + Ok(()) +} + +pub fn search_prev(context: &mut AppContext) -> DiziResult<()> { + if let Some(search_context) = context.get_search_context() { + let index = match search_context { + SearchPattern::Glob(s) => { + search_glob::search_glob_rev(context.curr_list_ref().unwrap(), s) + } + SearchPattern::String(s) => { + search_string::search_string_rev(context.curr_list_ref().unwrap(), s) + } + }; + if let Some(index) = index { + let _ = cursor_move::cursor_move(index, context); + } + } + Ok(()) +} diff --git a/src/client/commands/search_glob.rs b/src/client/commands/search_glob.rs new file mode 100644 index 0000000..f47efb2 --- /dev/null +++ b/src/client/commands/search_glob.rs @@ -0,0 +1,46 @@ +use globset::{GlobBuilder, GlobMatcher}; + +use dizi_commands::error::DiziResult; + +use crate::context::AppContext; +use crate::fs::DirList; +use crate::util::search::SearchPattern; + +use super::cursor_move; + +pub fn search_glob_fwd(curr_list: &DirList, glob: &GlobMatcher) -> Option { + let offset = curr_list.index? + 1; + let contents_len = curr_list.len(); + for i in 0..contents_len { + let file_name = curr_list.contents[(offset + i) % contents_len].file_name(); + if glob.is_match(file_name) { + return Some((offset + i) % contents_len); + } + } + None +} +pub fn search_glob_rev(curr_list: &DirList, glob: &GlobMatcher) -> Option { + let offset = curr_list.index?; + let contents_len = curr_list.len(); + for i in (0..contents_len).rev() { + let file_name = curr_list.contents[(offset + i) % contents_len].file_name(); + if glob.is_match(file_name) { + return Some((offset + i) % contents_len); + } + } + None +} + +pub fn search_glob(context: &mut AppContext, pattern: &str) -> DiziResult<()> { + let glob = GlobBuilder::new(pattern) + .case_insensitive(true) + .build()? + .compile_matcher(); + + let index = search_glob_fwd(context.curr_list_ref().unwrap(), &glob); + if let Some(index) = index { + let _ = cursor_move::cursor_move(index, context); + } + context.set_search_context(SearchPattern::Glob(glob)); + Ok(()) +} diff --git a/src/client/commands/search_skim.rs b/src/client/commands/search_skim.rs new file mode 100644 index 0000000..794e0b3 --- /dev/null +++ b/src/client/commands/search_skim.rs @@ -0,0 +1,100 @@ +use std::borrow; +use std::io; +use std::sync; +use std::thread; + +use skim::prelude::*; + +use dizi_commands::error::{DiziError, DiziErrorKind, DiziResult}; + +use crate::commands::cursor_move; +use crate::context::AppContext; +use crate::ui::TuiBackend; +use crate::util::search::SearchPattern; + +#[derive(Clone, Debug)] +pub struct DiziSkimItem { + pub idx: usize, + pub value: String, +} + +impl SkimItem for DiziSkimItem { + fn text(&self) -> Cow { + borrow::Cow::Borrowed(self.value.as_str()) + } +} + +pub fn search_skim(context: &mut AppContext, backend: &mut TuiBackend) -> DiziResult<()> { + let options = SkimOptionsBuilder::default() + .height(Some("100%")) + .multi(true) + .build() + .unwrap(); + + let items = context + .curr_list_ref() + .map(|list| { + let v: Vec = list + .iter() + .enumerate() + .map(|(i, e)| DiziSkimItem { + idx: i, + value: e.file_name().to_string(), + }) + .collect(); + v + }) + .unwrap_or_else(|| vec![]); + + if items.is_empty() { + return Err(DiziError::new( + DiziErrorKind::IoError(io::ErrorKind::InvalidData), + "no files to select".to_string(), + )); + } + + let (s, r): (SkimItemSender, SkimItemReceiver) = unbounded(); + let thread = thread::spawn(move || { + for item in items { + let _ = s.send(sync::Arc::new(item)); + } + }); + + backend.terminal_drop(); + + let skim_output = Skim::run_with(&options, Some(r)); + + backend.terminal_restore()?; + + let _ = thread.join(); + + if let Some(skim_output) = skim_output { + if skim_output.final_key == Key::ESC { + return Ok(()); + } + + let query = skim_output.query; + if !query.is_empty() { + context.set_search_context(SearchPattern::String(query)); + } + + for sk_item in skim_output.selected_items { + let item: Option<&DiziSkimItem> = + (*sk_item).as_any().downcast_ref::(); + + match item { + Some(item) => { + cursor_move::cursor_move(item.idx, context); + } + None => { + return Err(DiziError::new( + DiziErrorKind::IoError(io::ErrorKind::InvalidData), + "Error casting".to_string(), + )) + } + } + } + } + + Ok(()) +} diff --git a/src/client/commands/search_string.rs b/src/client/commands/search_string.rs new file mode 100644 index 0000000..8a4fdb6 --- /dev/null +++ b/src/client/commands/search_string.rs @@ -0,0 +1,44 @@ +use dizi_commands::error::DiziResult; + +use crate::context::AppContext; +use crate::fs::DirList; +use crate::util::search::SearchPattern; + +use super::cursor_move; + +pub fn search_string_fwd(curr_list: &DirList, pattern: &str) -> Option { + let offset = curr_list.index? + 1; + let contents_len = curr_list.contents.len(); + for i in 0..contents_len { + let file_name_lower = curr_list.contents[(offset + i) % contents_len] + .file_name() + .to_lowercase(); + if file_name_lower.contains(pattern) { + return Some((offset + i) % contents_len); + } + } + None +} +pub fn search_string_rev(curr_list: &DirList, pattern: &str) -> Option { + let offset = curr_list.index?; + let contents_len = curr_list.contents.len(); + for i in (0..contents_len).rev() { + let file_name_lower = curr_list.contents[(offset + i) % contents_len] + .file_name() + .to_lowercase(); + if file_name_lower.contains(pattern) { + return Some((offset + i) % contents_len); + } + } + None +} + +pub fn search_string(context: &mut AppContext, pattern: &str) -> DiziResult<()> { + let pattern = pattern.to_lowercase(); + let index = search_string_fwd(context.curr_list_ref().unwrap(), pattern.as_str()); + if let Some(index) = index { + let _ = cursor_move::cursor_move(index, context); + } + context.set_search_context(SearchPattern::String(pattern)); + Ok(()) +} diff --git a/src/client/commands/selection.rs b/src/client/commands/selection.rs new file mode 100644 index 0000000..213aba1 --- /dev/null +++ b/src/client/commands/selection.rs @@ -0,0 +1,28 @@ +use globset::Glob; + +use dizi_commands::error::{DiziError, DiziErrorKind, DiziResult}; + +use crate::context::AppContext; +use crate::util::select::SelectOption; + +use super::cursor_move; + +pub fn select_files(context: &mut AppContext, pattern: &str, options: &SelectOption) -> DiziResult<()> { + if pattern.is_empty() { + select_without_pattern(context, options) + } else { + select_with_pattern(context, pattern, options) + } +} + +fn select_without_pattern(context: &mut AppContext, options: &SelectOption) -> DiziResult<()> { + Ok(()) +} + +fn select_with_pattern( + context: &mut AppContext, + pattern: &str, + options: &SelectOption, +) -> DiziResult<()> { + Ok(()) +} diff --git a/src/client/commands/show_hidden.rs b/src/client/commands/show_hidden.rs new file mode 100644 index 0000000..7a24f52 --- /dev/null +++ b/src/client/commands/show_hidden.rs @@ -0,0 +1,21 @@ +use dizi_commands::error::DiziResult; + +use crate::context::AppContext; +use crate::history::DirectoryHistory; + +use super::reload; + +pub fn _toggle_hidden(context: &mut AppContext) { + let opposite = !context.config_ref().display_options_ref().show_hidden(); + context + .config_mut() + .display_options_mut() + .set_show_hidden(opposite); + + context.history_mut().depreciate_all_entries(); +} + +pub fn toggle_hidden(context: &mut AppContext) -> DiziResult<()> { + _toggle_hidden(context); + reload::reload_dirlist(context) +} diff --git a/src/client/commands/sort.rs b/src/client/commands/sort.rs new file mode 100644 index 0000000..b85dabe --- /dev/null +++ b/src/client/commands/sort.rs @@ -0,0 +1,29 @@ +use dizi_commands::error::DiziResult; + +use crate::context::AppContext; +use crate::history::DirectoryHistory; +use crate::util::sort_type::SortType; + +use super::reload; + +pub fn set_sort(context: &mut AppContext, method: SortType) -> DiziResult<()> { + context + .config_mut() + .sort_options_mut() + .set_sort_method(method); + context.history_mut().depreciate_all_entries(); + refresh(context) +} + +pub fn toggle_reverse(context: &mut AppContext) -> DiziResult<()> { + let reversed = !context.config_ref().sort_options_ref().reverse; + context.config_mut().sort_options_mut().reverse = reversed; + + context.history_mut().depreciate_all_entries(); + refresh(context) +} + +fn refresh(context: &mut AppContext) -> DiziResult<()> { + reload::soft_reload(context)?; + Ok(()) +} diff --git a/src/client/config/general/client.rs b/src/client/config/general/client.rs new file mode 100644 index 0000000..3fed035 --- /dev/null +++ b/src/client/config/general/client.rs @@ -0,0 +1,73 @@ +use std::path::PathBuf; + +use serde_derive::Deserialize; + +use crate::config::Flattenable; +use crate::util::display_option::DisplayOption; + +use super::display::RawDisplayOption; +use super::player::{PlayerOption, RawPlayerOption}; + +const fn default_true() -> bool { + true +} + +#[derive(Clone, Debug, Deserialize)] +pub struct RawClientConfig { + #[serde(default)] + pub socket: String, + #[serde(default)] + pub home_dir: String, + + #[serde(default, rename = "display")] + pub display_options: RawDisplayOption, + #[serde(default, rename = "player")] + pub player_options: RawPlayerOption, +} + +impl Flattenable for RawClientConfig { + fn flatten(self) -> ClientConfig { + ClientConfig { + socket: PathBuf::from(self.socket), + home_dir: PathBuf::from(self.home_dir), + display_options: self.display_options.flatten(), + player_options: self.player_options.flatten(), + } + } +} + +impl std::default::Default for RawClientConfig { + fn default() -> Self { + Self { + socket: "".to_string(), + home_dir: "".to_string(), + display_options: RawDisplayOption::default(), + player_options: RawPlayerOption::default(), + } + } +} + +#[derive(Clone, Debug)] +pub struct ClientConfig { + pub socket: PathBuf, + pub home_dir: PathBuf, + pub display_options: DisplayOption, + pub player_options: PlayerOption, +} + +impl ClientConfig { + pub fn display_options_ref(&self) -> &DisplayOption { + &self.display_options + } +} + +impl std::default::Default for ClientConfig { + fn default() -> Self { + Self { + socket: PathBuf::from(""), + home_dir: PathBuf::from(""), + display_options: DisplayOption::default(), + player_options: PlayerOption::default(), + } + } +} diff --git a/src/client/config/general/config.rs b/src/client/config/general/config.rs new file mode 100644 index 0000000..b590193 --- /dev/null +++ b/src/client/config/general/config.rs @@ -0,0 +1,80 @@ +use serde_derive::Deserialize; + +use crate::config::{parse_to_config_file, ConfigStructure, Flattenable}; +use crate::util::display_option::DisplayOption; +use crate::util::sort_option::SortOption; + +use super::client::{ClientConfig, RawClientConfig}; + +const fn default_true() -> bool { + true +} +const fn default_scroll_offset() -> usize { + 6 +} +const fn default_max_preview_size() -> u64 { + 2 * 1024 * 1024 // 2 MB +} + +#[derive(Clone, Debug, Deserialize)] +pub struct RawAppConfig { + #[serde(default)] + pub client: RawClientConfig, +} + +impl Flattenable for RawAppConfig { + fn flatten(self) -> AppConfig { + AppConfig { + _client: self.client.flatten(), + } + } +} + +#[derive(Debug, Clone)] +pub struct AppConfig { + _client: ClientConfig, +} + +impl AppConfig { + pub fn new(client: ClientConfig) -> Self { + Self { + _client: client, + } + } + + pub fn client_ref(&self) -> &ClientConfig { + &self._client + } + + pub fn client_mut(&mut self) -> &mut ClientConfig { + &mut self._client + } + + pub fn display_options_ref(&self) -> &DisplayOption { + &self.client_ref().display_options + } + pub fn display_options_mut(&mut self) -> &mut DisplayOption { + &mut self.client_mut().display_options + } + + pub fn sort_options_ref(&self) -> &SortOption { + self.display_options_ref().sort_options_ref() + } + pub fn sort_options_mut(&mut self) -> &mut SortOption { + self.display_options_mut().sort_options_mut() + } +} + +impl ConfigStructure for AppConfig { + fn get_config(file_name: &str) -> Self { + parse_to_config_file::(file_name).unwrap_or_else(Self::default) + } +} + +impl std::default::Default for AppConfig { + fn default() -> Self { + Self { + _client: ClientConfig::default(), + } + } +} diff --git a/src/client/config/general/display.rs b/src/client/config/general/display.rs new file mode 100644 index 0000000..64317e8 --- /dev/null +++ b/src/client/config/general/display.rs @@ -0,0 +1,69 @@ +use serde_derive::Deserialize; + +use tui::layout::Constraint; + +use crate::config::Flattenable; +use crate::util::display_option::{default_column_ratio, DisplayOption}; + +use super::SortRawOption; + +const fn default_true() -> bool { + true +} + +#[derive(Clone, Debug, Deserialize)] +pub struct RawDisplayOption { + #[serde(default)] + show_hidden: bool, + + #[serde(default = "default_true")] + show_borders: bool, + + #[serde(default)] + column_ratio: Option<[usize; 3]>, + + #[serde(default, rename = "sort")] + sort_options: SortRawOption, +} + +impl Flattenable for RawDisplayOption { + fn flatten(self) -> DisplayOption { + let column_ratio = match self.column_ratio { + Some(s) => (s[0], s[1], s[2]), + _ => default_column_ratio(), + }; + + let total = (column_ratio.0 + column_ratio.1 + column_ratio.2) as u32; + + let default_layout = [ + Constraint::Ratio(column_ratio.0 as u32, total), + Constraint::Ratio(column_ratio.1 as u32, total), + Constraint::Ratio(column_ratio.2 as u32, total), + ]; + let no_preview_layout = [ + Constraint::Ratio(column_ratio.0 as u32, total), + Constraint::Ratio(column_ratio.1 as u32 + column_ratio.2 as u32, total), + Constraint::Ratio(0, total), + ]; + + DisplayOption { + column_ratio, + _show_hidden: self.show_hidden, + _show_borders: self.show_borders, + _sort_options: self.sort_options.into(), + default_layout, + no_preview_layout, + } + } +} + +impl std::default::Default for RawDisplayOption { + fn default() -> Self { + Self { + show_hidden: false, + show_borders: true, + column_ratio: None, + sort_options: SortRawOption::default(), + } + } +} diff --git a/src/client/config/general/mod.rs b/src/client/config/general/mod.rs new file mode 100644 index 0000000..dc3b84c --- /dev/null +++ b/src/client/config/general/mod.rs @@ -0,0 +1,11 @@ +pub mod client; +pub mod config; +pub mod display; +pub mod player; +pub mod sort; + +pub use self::client::ClientConfig; +pub use self::config::AppConfig; +pub use self::display::RawDisplayOption; +pub use self::player::PlayerOption; +pub use self::sort::*; diff --git a/src/client/config/general/player.rs b/src/client/config/general/player.rs new file mode 100644 index 0000000..3c5ba4f --- /dev/null +++ b/src/client/config/general/player.rs @@ -0,0 +1,62 @@ +use std::path::PathBuf; + +use serde_derive::Deserialize; + +use crate::config::Flattenable; + +const fn default_true() -> bool { + true +} + +#[derive(Clone, Debug, Deserialize)] +pub struct RawPlayerOption { + #[serde(default)] + pub shuffle: bool, + #[serde(default = "default_true")] + pub repeat: bool, + #[serde(default)] + pub next: bool, + #[serde(default)] + pub on_song_change: Option +} + +impl Flattenable for RawPlayerOption { + fn flatten(self) -> PlayerOption { + PlayerOption { + shuffle: self.shuffle, + repeat: self.repeat, + next: self.next, + on_song_change: self.on_song_change, + } + } +} + +impl std::default::Default for RawPlayerOption { + fn default() -> Self { + Self { + shuffle: false, + repeat: true, + next: true, + on_song_change: None + } + } +} + +#[derive(Clone, Debug)] +pub struct PlayerOption { + pub shuffle: bool, + pub repeat: bool, + pub next: bool, + pub on_song_change: Option +} + +impl std::default::Default for PlayerOption { + fn default() -> Self { + Self { + shuffle: false, + repeat: true, + next: true, + on_song_change: None + } + } +} diff --git a/src/client/config/general/sort.rs b/src/client/config/general/sort.rs new file mode 100644 index 0000000..8e01f51 --- /dev/null +++ b/src/client/config/general/sort.rs @@ -0,0 +1,50 @@ +use serde_derive::Deserialize; + +use crate::util::sort_option::SortOption; +use crate::util::sort_type::{SortTypes, SortType}; + +const fn default_true() -> bool { + true +} + +#[derive(Clone, Debug, Deserialize)] +pub struct SortRawOption { + #[serde(default = "default_true")] + pub directories_first: bool, + #[serde(default)] + pub case_sensitive: bool, + #[serde(default = "default_true")] + pub reverse: bool, + #[serde(default)] + pub sort_method: Option, +} + +impl SortRawOption { + pub fn into(self) -> SortOption { + let sort_method = match self.sort_method.as_ref() { + Some(s) => SortType::parse(s).unwrap_or(SortType::Natural), + None => SortType::Natural, + }; + + let mut sort_methods = SortTypes::default(); + sort_methods.reorganize(sort_method); + + SortOption { + directories_first: self.directories_first, + case_sensitive: self.case_sensitive, + reverse: self.reverse, + sort_methods, + } + } +} + +impl std::default::Default for SortRawOption { + fn default() -> Self { + Self { + directories_first: true, + case_sensitive: false, + reverse: true, + sort_method: None, + } + } +} diff --git a/src/client/config/keymap/keymapping.rs b/src/client/config/keymap/keymapping.rs new file mode 100644 index 0000000..0fc4bc9 --- /dev/null +++ b/src/client/config/keymap/keymapping.rs @@ -0,0 +1,135 @@ +use serde_derive::Deserialize; + +use std::collections::{hash_map::Entry, HashMap}; +use std::str::FromStr; + +#[cfg(feature = "mouse")] +use termion::event::MouseEvent; +use termion::event::{Event, Key}; + +use crate::config::{parse_to_config_file, ConfigStructure, Flattenable}; +use crate::key_command::{Command, CommandKeybind}; +use crate::util::keyparse::str_to_event; + +#[derive(Debug, Deserialize)] +struct CommandKeymap { + pub command: String, + pub keys: Vec, +} + +#[derive(Debug, Deserialize)] +struct RawAppKeyMapping { + #[serde(default)] + mapcommand: Vec, +} + +impl Flattenable for RawAppKeyMapping { + fn flatten(self) -> AppKeyMapping { + let mut keymaps = AppKeyMapping::new(); + for m in self.mapcommand { + match Command::from_str(m.command.as_str()) { + Ok(command) => { + let events: Vec = m + .keys + .iter() + .filter_map(|s| str_to_event(s.as_str())) + .collect(); + + if events.len() != m.keys.len() { + eprintln!("Failed to parse events: {:?}", m.keys); + continue; + } + + let result = insert_keycommand(&mut keymaps, command, &events); + match result { + Ok(_) => {} + Err(e) => eprintln!("{}", e), + } + } + Err(e) => eprintln!("{}", e), + } + } + keymaps + } +} + +#[derive(Debug)] +pub struct AppKeyMapping { + map: HashMap, +} + +impl std::convert::AsRef> for AppKeyMapping { + fn as_ref(&self) -> &HashMap { + &self.map + } +} + +impl std::convert::AsMut> for AppKeyMapping { + fn as_mut(&mut self) -> &mut HashMap { + &mut self.map + } +} + +impl AppKeyMapping { + pub fn new() -> Self { + Self { + map: HashMap::new(), + } + } +} + +impl std::default::Default for AppKeyMapping { + fn default() -> Self { + let m = Self { + map: HashMap::new(), + }; + m + } +} + +impl ConfigStructure for AppKeyMapping { + fn get_config(file_name: &str) -> Self { + parse_to_config_file::(file_name) + .unwrap_or_else(Self::default) + } +} + +fn insert_keycommand( + keymap: &mut AppKeyMapping, + keycommand: Command, + events: &[Event], +) -> Result<(), String> { + let num_events = events.len(); + if num_events == 0 { + return Ok(()); + } + + let event = events[0].clone(); + if num_events == 1 { + match keymap.as_mut().entry(event) { + Entry::Occupied(_) => { + return Err(format!("Error: Keybindings ambiguous for {}", keycommand)) + } + Entry::Vacant(entry) => entry.insert(CommandKeybind::SimpleKeybind(keycommand)), + }; + return Ok(()); + } + + match keymap.as_mut().entry(event) { + Entry::Occupied(mut entry) => match entry.get_mut() { + CommandKeybind::CompositeKeybind(ref mut m) => { + insert_keycommand(m, keycommand, &events[1..]) + } + _ => Err(format!("Error: Keybindings ambiguous for {}", keycommand)), + }, + Entry::Vacant(entry) => { + let mut new_map = AppKeyMapping::new(); + let result = insert_keycommand(&mut new_map, keycommand, &events[1..]); + if result.is_ok() { + let composite_command = CommandKeybind::CompositeKeybind(new_map); + entry.insert(composite_command); + } + result + } + } +} diff --git a/src/client/config/keymap/mod.rs b/src/client/config/keymap/mod.rs new file mode 100644 index 0000000..14ad630 --- /dev/null +++ b/src/client/config/keymap/mod.rs @@ -0,0 +1,3 @@ +mod keymapping; + +pub use self::keymapping::AppKeyMapping; diff --git a/src/client/config/mod.rs b/src/client/config/mod.rs new file mode 100644 index 0000000..891fe52 --- /dev/null +++ b/src/client/config/mod.rs @@ -0,0 +1,82 @@ +pub mod general; +pub mod keymap; +pub mod theme; + +pub use self::general::AppConfig; +pub use self::keymap::AppKeyMapping; +pub use self::theme::{AppStyle, AppTheme}; + +use serde::de::DeserializeOwned; +use std::fs; +use std::path::{Path, PathBuf}; + +use crate::CONFIG_HIERARCHY; + +pub trait ConfigStructure { + fn get_config(file_name: &str) -> Self; +} + +// implemented by config file implementations to turn a RawConfig into a Config +trait Flattenable { + fn flatten(self) -> T; +} + +// searches a list of folders for a given file in order of preference +pub fn search_directories

(filename: &str, directories: &[P]) -> Option +where + P: AsRef, +{ + for path in directories.iter() { + let filepath = path.as_ref().join(filename); + if filepath.exists() { + return Some(filepath); + } + } + None +} + +// parses a config file into its appropriate format +fn parse_to_config_file(filename: &str) -> Option +where + T: DeserializeOwned + Flattenable, +{ + let file_path = search_directories(filename, &CONFIG_HIERARCHY)?; + let file_contents = match fs::read_to_string(&file_path) { + Ok(content) => content, + Err(e) => { + eprintln!("Error reading {} file: {}", filename, e); + return None; + } + }; + let config = match toml::from_str::(&file_contents) { + Ok(config) => config, + Err(e) => { + eprintln!("Error parsing {} file: {}", filename, e); + return None; + } + }; + Some(config.flatten()) +} + +// parses a config file into its appropriate format +fn parse_config_file(filename: &str) -> Option +where + T: DeserializeOwned, +{ + let file_path = search_directories(filename, &CONFIG_HIERARCHY)?; + let file_contents = match fs::read_to_string(&file_path) { + Ok(content) => content, + Err(e) => { + eprintln!("Error reading {} file: {}", filename, e); + return None; + } + }; + + match toml::from_str::(&file_contents) { + Ok(config) => Some(config), + Err(e) => { + eprintln!("Error parsing {} file: {}", filename, e); + None + } + } +} diff --git a/src/client/config/theme/app_theme.rs b/src/client/config/theme/app_theme.rs new file mode 100644 index 0000000..1b7eab5 --- /dev/null +++ b/src/client/config/theme/app_theme.rs @@ -0,0 +1,127 @@ +use serde_derive::Deserialize; +use std::collections::HashMap; + +use tui::style::{Color, Modifier}; + +use super::{AppStyle, RawAppStyle}; +use crate::config::{parse_to_config_file, ConfigStructure, Flattenable}; + +#[derive(Clone, Debug, Deserialize)] +pub struct RawAppTheme { + #[serde(default)] + pub regular: RawAppStyle, + #[serde(default)] + pub selection: RawAppStyle, + #[serde(default)] + pub directory: RawAppStyle, + #[serde(default)] + pub executable: RawAppStyle, + #[serde(default)] + pub link: RawAppStyle, + #[serde(default)] + pub link_invalid: RawAppStyle, + #[serde(default)] + pub socket: RawAppStyle, + #[serde(default)] + pub ext: HashMap, +} + +impl std::default::Default for RawAppTheme { + fn default() -> Self { + Self { + regular: RawAppStyle::default(), + selection: RawAppStyle::default(), + directory: RawAppStyle::default(), + executable: RawAppStyle::default(), + link: RawAppStyle::default(), + link_invalid: RawAppStyle::default(), + socket: RawAppStyle::default(), + ext: HashMap::default(), + } + } +} + +impl Flattenable for RawAppTheme { + fn flatten(self) -> AppTheme { + let selection = self.selection.to_style_theme(); + let executable = self.executable.to_style_theme(); + let regular = self.regular.to_style_theme(); + let directory = self.directory.to_style_theme(); + let link = self.link.to_style_theme(); + let link_invalid = self.link_invalid.to_style_theme(); + let socket = self.socket.to_style_theme(); + let ext: HashMap = self + .ext + .iter() + .map(|(k, v)| { + let style = v.to_style_theme(); + (k.clone(), style) + }) + .collect(); + + AppTheme { + selection, + executable, + regular, + directory, + link, + link_invalid, + socket, + ext, + } + } +} + +#[derive(Clone, Debug)] +pub struct AppTheme { + pub regular: AppStyle, + pub selection: AppStyle, + pub directory: AppStyle, + pub executable: AppStyle, + pub link: AppStyle, + pub link_invalid: AppStyle, + pub socket: AppStyle, + pub ext: HashMap, +} + +impl ConfigStructure for AppTheme { + fn get_config(file_name: &str) -> Self { + parse_to_config_file::(file_name).unwrap_or_else(Self::default) + } +} + +impl std::default::Default for AppTheme { + fn default() -> Self { + let selection = AppStyle::default() + .set_fg(Color::LightYellow) + .insert(Modifier::BOLD); + let executable = AppStyle::default() + .set_fg(Color::LightGreen) + .insert(Modifier::BOLD); + let regular = AppStyle::default().set_fg(Color::White); + let directory = AppStyle::default() + .set_fg(Color::LightBlue) + .insert(Modifier::BOLD); + let link = AppStyle::default() + .set_fg(Color::LightCyan) + .insert(Modifier::BOLD); + let link_invalid = AppStyle::default() + .set_fg(Color::Red) + .insert(Modifier::BOLD); + let socket = AppStyle::default() + .set_fg(Color::LightMagenta) + .insert(Modifier::BOLD); + let ext = HashMap::new(); + + Self { + selection, + executable, + regular, + directory, + link, + link_invalid, + socket, + ext, + } + } +} diff --git a/src/client/config/theme/mod.rs b/src/client/config/theme/mod.rs new file mode 100644 index 0000000..7dc8b4b --- /dev/null +++ b/src/client/config/theme/mod.rs @@ -0,0 +1,5 @@ +mod app_theme; +mod style; + +pub use self::app_theme::AppTheme; +pub use self::style::{AppStyle, RawAppStyle}; diff --git a/src/client/config/theme/style.rs b/src/client/config/theme/style.rs new file mode 100644 index 0000000..a66d0c4 --- /dev/null +++ b/src/client/config/theme/style.rs @@ -0,0 +1,109 @@ +use serde_derive::Deserialize; + +use tui::style; + +const fn default_color() -> style::Color { + style::Color::Reset +} + +#[derive(Clone, Debug, Deserialize)] +pub struct RawAppStyle { + #[serde(default)] + pub fg: String, + #[serde(default)] + pub bg: String, + #[serde(default)] + pub bold: bool, + #[serde(default)] + pub underline: bool, + #[serde(default)] + pub invert: bool, +} + +impl RawAppStyle { + pub fn to_style_theme(&self) -> AppStyle { + let bg = Self::str_to_color(self.bg.as_str()); + let fg = Self::str_to_color(self.fg.as_str()); + + let mut modifier = style::Modifier::empty(); + if self.bold { + modifier.insert(style::Modifier::BOLD); + } + if self.underline { + modifier.insert(style::Modifier::UNDERLINED); + } + if self.invert { + modifier.insert(style::Modifier::REVERSED); + } + + AppStyle::default().set_fg(fg).set_bg(bg).insert(modifier) + } + + pub fn str_to_color(s: &str) -> style::Color { + match s { + "black" => style::Color::Black, + "red" => style::Color::Red, + "green" => style::Color::Green, + "yellow" => style::Color::Yellow, + "blue" => style::Color::Blue, + "magenta" => style::Color::Magenta, + "cyan" => style::Color::Cyan, + "gray" => style::Color::Gray, + "dark_gray" => style::Color::DarkGray, + "light_red" => style::Color::LightRed, + "light_green" => style::Color::LightGreen, + "light_yellow" => style::Color::LightYellow, + "light_blue" => style::Color::LightBlue, + "light_magenta" => style::Color::LightMagenta, + "light_cyan" => style::Color::LightCyan, + "white" => style::Color::White, + "reset" => style::Color::Reset, + s => style::Color::Reset, + } + } +} + +impl std::default::Default for RawAppStyle { + fn default() -> Self { + Self { + bg: "".to_string(), + fg: "".to_string(), + bold: false, + underline: false, + invert: false, + } + } +} + +#[derive(Clone, Debug)] +pub struct AppStyle { + pub fg: style::Color, + pub bg: style::Color, + pub modifier: style::Modifier, +} + +impl AppStyle { + pub fn set_bg(mut self, bg: style::Color) -> Self { + self.bg = bg; + self + } + pub fn set_fg(mut self, fg: style::Color) -> Self { + self.fg = fg; + self + } + + pub fn insert(mut self, modifier: style::Modifier) -> Self { + self.modifier.insert(modifier); + self + } +} + +impl std::default::Default for AppStyle { + fn default() -> Self { + Self { + fg: default_color(), + bg: default_color(), + modifier: style::Modifier::empty(), + } + } +} diff --git a/src/client/context/app_context.rs b/src/client/context/app_context.rs new file mode 100644 index 0000000..0c42422 --- /dev/null +++ b/src/client/context/app_context.rs @@ -0,0 +1,110 @@ +use std::os::unix::net::UnixStream; +use std::sync::mpsc; +use std::path::{Path, PathBuf}; + +use crate::config; +use crate::context::MessageQueue; +use crate::event::{AppEvent, Events}; +use crate::fs::DirList; +use crate::history::{DirectoryHistory, History}; +use crate::util::search::SearchPattern; + +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum QuitType { + DoNot, + Normal, + Server, +} + +pub struct AppContext { + pub quit: QuitType, + // event loop querying + pub events: Events, + // server unix socket + pub stream: UnixStream, + // app config + config: config::AppConfig, + + _cwd: PathBuf, + // directory history + history: History, + // context related to searching + search_context: Option, + // message queue for displaying messages + message_queue: MessageQueue, +} + +impl AppContext { + pub fn new(config: config::AppConfig, cwd: PathBuf, stream: UnixStream) -> Self { + let events = Events::new(); + let event_tx = events.event_tx.clone(); + + Self { + quit: QuitType::DoNot, + stream, + events, + _cwd: cwd, + history: History::new(), + search_context: None, + message_queue: MessageQueue::new(), + config, + } + } + + // event related + pub fn poll_event(&self) -> Result { + self.events.next() + } + pub fn flush_event(&self) { + self.events.flush(); + } + pub fn clone_event_tx(&self) -> mpsc::Sender { + self.events.event_tx.clone() + } + + pub fn config_ref(&self) -> &config::AppConfig { + &self.config + } + pub fn config_mut(&mut self) -> &mut config::AppConfig { + &mut self.config + } + + pub fn message_queue_ref(&self) -> &MessageQueue { + &self.message_queue + } + pub fn message_queue_mut(&mut self) -> &mut MessageQueue { + &mut self.message_queue + } + + pub fn get_search_context(&self) -> Option<&SearchPattern> { + self.search_context.as_ref() + } + pub fn set_search_context(&mut self, pattern: SearchPattern) { + self.search_context = Some(pattern); + } + + pub fn history_ref(&self) -> &History { + &self.history + } + pub fn history_mut(&mut self) -> &mut History { + &mut self.history + } + + pub fn cwd(&self) -> &Path { + &self._cwd + } + pub fn set_cwd(&mut self, path: &Path) { + self._cwd = path.to_path_buf(); + } + + + pub fn curr_list_ref(&self) -> Option<&DirList> { + self.history.get(self.cwd()) + } + pub fn curr_list_mut(&mut self) -> Option<&mut DirList> { + self.history.get_mut(self._cwd.as_path()) + } + + +} + diff --git a/src/client/context/message_queue.rs b/src/client/context/message_queue.rs new file mode 100644 index 0000000..f2995c0 --- /dev/null +++ b/src/client/context/message_queue.rs @@ -0,0 +1,59 @@ +use std::collections::VecDeque; + +use tui::style::{Color, Style}; + +pub struct Message { + pub content: String, + pub style: Style, +} + +impl Message { + pub fn new(content: String, style: Style) -> Self { + Self { content, style } + } +} + +pub struct MessageQueue { + contents: VecDeque, +} + +impl MessageQueue { + pub fn new() -> Self { + Self::default() + } + + pub fn push_success(&mut self, msg: String) { + let message = Message::new(msg, Style::default().fg(Color::Green)); + self.push_msg(message); + } + + pub fn push_info(&mut self, msg: String) { + let message = Message::new(msg, Style::default().fg(Color::Yellow)); + self.push_msg(message); + } + + pub fn push_error(&mut self, msg: String) { + let message = Message::new(msg, Style::default().fg(Color::Red)); + self.push_msg(message); + } + + fn push_msg(&mut self, msg: Message) { + self.contents.push_back(msg); + } + + pub fn pop_front(&mut self) -> Option { + self.contents.pop_front() + } + + pub fn current_message(&self) -> Option<&Message> { + self.contents.front() + } +} + +impl std::default::Default for MessageQueue { + fn default() -> Self { + Self { + contents: VecDeque::new(), + } + } +} diff --git a/src/client/context/mod.rs b/src/client/context/mod.rs new file mode 100644 index 0000000..5f1260c --- /dev/null +++ b/src/client/context/mod.rs @@ -0,0 +1,5 @@ +mod app_context; +mod message_queue; + +pub use self::app_context::*; +pub use self::message_queue::*; diff --git a/src/client/event.rs b/src/client/event.rs new file mode 100644 index 0000000..257d5f2 --- /dev/null +++ b/src/client/event.rs @@ -0,0 +1,105 @@ +use std::io; +use std::path; +use std::sync::mpsc; +use std::thread; + +use signal_hook::consts::signal; +use signal_hook::iterator::exfiltrator::SignalOnly; +use signal_hook::iterator::SignalsInfo; + +use termion::event::Event; +use termion::input::TermRead; + +use crate::fs::DirList; + +#[derive(Debug)] +pub enum AppEvent { + Termion(Event), + PreviewDir(io::Result), + Signal(i32), +} + +#[derive(Debug, Clone, Copy)] +pub struct Config {} + +impl Default for Config { + fn default() -> Config { + Config {} + } +} + +pub struct Events { + pub event_tx: mpsc::Sender, + event_rx: mpsc::Receiver, + pub input_tx: mpsc::SyncSender<()>, +} + +impl Events { + pub fn new() -> Self { + Events::with_config() + } + + pub fn with_config() -> Self { + let (input_tx, input_rx) = mpsc::sync_channel(1); + let (event_tx, event_rx) = mpsc::channel(); + + // signal thread + let event_tx2 = event_tx.clone(); + let _ = thread::spawn(move || { + let sigs = vec![signal::SIGWINCH]; + let mut signals = SignalsInfo::::new(&sigs).unwrap(); + for signal in &mut signals { + if let Err(e) = event_tx2.send(AppEvent::Signal(signal)) { + eprintln!("Signal thread send err: {:#?}", e); + return; + } + } + }); + + // input thread + let event_tx2 = event_tx.clone(); + let _ = thread::spawn(move || { + let stdin = io::stdin(); + let mut events = stdin.events(); + match events.next() { + Some(event) => match event { + Ok(event) => { + if let Err(e) = event_tx2.send(AppEvent::Termion(event)) { + eprintln!("Input thread send err: {:#?}", e); + return; + } + } + Err(_) => return, + }, + None => return, + } + + while input_rx.recv().is_ok() { + if let Some(Ok(event)) = events.next() { + if let Err(e) = event_tx2.send(AppEvent::Termion(event)) { + eprintln!("Input thread send err: {:#?}", e); + return; + } + } + } + }); + + Events { + event_tx, + event_rx, + input_tx, + } + } + + // We need a next() and a flush() so we don't continuously consume + // input from the console. Sometimes, other applications need to + // read terminal inputs while joshuto is in the background + pub fn next(&self) -> Result { + let event = self.event_rx.recv()?; + Ok(event) + } + + pub fn flush(&self) { + let _ = self.input_tx.send(()); + } +} diff --git a/src/client/fs/dirlist.rs b/src/client/fs/dirlist.rs new file mode 100644 index 0000000..0ec86b4 --- /dev/null +++ b/src/client/fs/dirlist.rs @@ -0,0 +1,153 @@ +use std::path; +use std::slice::{Iter, IterMut}; + +use crate::fs::{DirEntry, Metadata}; +use crate::history::read_directory; +use crate::util::display_option::DisplayOption; + +#[derive(Clone, Debug)] +pub struct DirList { + pub index: Option, + path: path::PathBuf, + _need_update: bool, + pub metadata: Metadata, + pub contents: Vec, +} + +impl DirList { + pub fn new( + path: path::PathBuf, + contents: Vec, + index: Option, + metadata: Metadata, + ) -> Self { + Self { + index, + path, + _need_update: false, + metadata, + contents, + } + } + + pub fn from_path(path: path::PathBuf, options: &DisplayOption) -> std::io::Result { + let filter_func = options.filter_func(); + let mut contents = read_directory(path.as_path(), filter_func, options)?; + + let sort_options = options.sort_options_ref(); + contents.sort_by(|f1, f2| sort_options.compare(f1, f2)); + + let index = if contents.is_empty() { None } else { Some(0) }; + + let metadata = Metadata::from(&path)?; + + Ok(Self { + index, + path, + _need_update: false, + metadata, + contents, + }) + } + + pub fn iter(&self) -> Iter { + self.contents.iter() + } + + pub fn iter_mut(&mut self) -> IterMut { + self.contents.iter_mut() + } + + pub fn len(&self) -> usize { + self.contents.len() + } + + pub fn is_empty(&self) -> bool { + self.contents.is_empty() + } + + pub fn modified(&self) -> bool { + let metadata = std::fs::symlink_metadata(self.file_path()); + match metadata { + Ok(m) => match m.modified() { + Ok(s) => s > self.metadata.modified(), + _ => false, + }, + _ => false, + } + } + + pub fn depreciate(&mut self) { + self._need_update = true; + } + + pub fn need_update(&self) -> bool { + self._need_update + } + + pub fn file_path(&self) -> &path::PathBuf { + &self.path + } + + pub fn any_selected(&self) -> bool { + self.contents.iter().any(|e| e.is_selected()) + } + + pub fn iter_selected(&self) -> impl Iterator { + self.contents.iter().filter(|entry| entry.is_selected()) + } + + pub fn iter_selected_mut(&mut self) -> impl Iterator { + self.contents.iter_mut().filter(|entry| entry.is_selected()) + } + + pub fn get_selected_paths(&self) -> Vec { + let vec: Vec = self + .iter_selected() + .map(|e| e.file_path().to_path_buf()) + .collect(); + if !vec.is_empty() { + vec + } else { + match self.curr_entry_ref() { + Some(s) => vec![s.file_path().to_path_buf()], + _ => vec![], + } + } + } + + pub fn curr_entry_ref(&self) -> Option<&DirEntry> { + self.get_curr_ref_(self.index?) + } + + pub fn curr_entry_mut(&mut self) -> Option<&mut DirEntry> { + self.get_curr_mut_(self.index?) + } + + /// For a given number of entries, visible in a UI, this method returns the index of the entry + /// with which the UI should start to list the entries. + /// + /// This method assures that the cursor is always in the viewport of the UI. + pub fn first_index_for_viewport(&self, viewport_height: usize) -> usize { + match self.index { + Some(index) => index / viewport_height as usize * viewport_height as usize, + None => 0, + } + } + + fn get_curr_mut_(&mut self, index: usize) -> Option<&mut DirEntry> { + if index < self.contents.len() { + Some(&mut self.contents[index]) + } else { + None + } + } + + fn get_curr_ref_(&self, index: usize) -> Option<&DirEntry> { + if index < self.contents.len() { + Some(&self.contents[index]) + } else { + None + } + } +} diff --git a/src/client/fs/entry.rs b/src/client/fs/entry.rs new file mode 100644 index 0000000..ec743fd --- /dev/null +++ b/src/client/fs/entry.rs @@ -0,0 +1,102 @@ +use std::{fs, io, path}; + +use crate::fs::{FileType, Metadata}; + +use crate::util::display_option::DisplayOption; + +#[derive(Clone, Debug)] +pub struct DirEntry { + name: String, + label: String, + path: path::PathBuf, + pub metadata: Metadata, + selected: bool, + marked: bool, +} + +impl DirEntry { + pub fn from(direntry: &fs::DirEntry, options: &DisplayOption) -> io::Result { + let path = direntry.path(); + + let mut metadata = Metadata::from(&path)?; + let name = direntry + .file_name() + .as_os_str() + .to_string_lossy() + .to_string(); + + let label = name.clone(); + + Ok(Self { + name, + label, + path, + metadata, + selected: false, + marked: false, + }) + } + + pub fn update_label(&mut self, label: String) { + self.label = label; + } + + pub fn file_name(&self) -> &str { + self.name.as_str() + } + + pub fn label(&self) -> &str { + self.label.as_str() + } + + pub fn file_path(&self) -> &path::Path { + self.path.as_path() + } + + pub fn is_selected(&self) -> bool { + self.selected + } + + pub fn set_selected(&mut self, selected: bool) { + self.selected = selected; + } + + pub fn get_ext(&self) -> &str { + let fname = self.file_name(); + match fname.rfind('.') { + Some(pos) => &fname[pos..], + None => "", + } + } +} + +impl std::fmt::Display for DirEntry { + fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + write!(f, "{}", self.file_name()) + } +} + +impl std::convert::AsRef for DirEntry { + fn as_ref(&self) -> &str { + self.file_name() + } +} + +impl std::cmp::PartialEq for DirEntry { + fn eq(&self, other: &Self) -> bool { + self.file_path() == other.file_path() + } +} +impl std::cmp::Eq for DirEntry {} + +impl std::cmp::PartialOrd for DirEntry { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl std::cmp::Ord for DirEntry { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.file_path().cmp(other.file_path()) + } +} diff --git a/src/client/fs/metadata.rs b/src/client/fs/metadata.rs new file mode 100644 index 0000000..c864290 --- /dev/null +++ b/src/client/fs/metadata.rs @@ -0,0 +1,124 @@ +use std::{fs, io, path, time}; + +#[derive(Clone, Debug, PartialEq)] +pub enum FileType { + Directory, + File, +} + +impl FileType { + pub fn is_dir(&self) -> bool { + *self == Self::Directory + } + pub fn is_file(&self) -> bool { + *self == Self::File + } +} + +#[derive(Clone, Debug)] +pub enum LinkType { + Normal, + Symlink(String, bool), // link target, link validity +} + +#[derive(Clone, Debug)] +pub struct Metadata { + _len: u64, + _modified: time::SystemTime, + _permissions: fs::Permissions, + _file_type: FileType, + _link_type: LinkType, + #[cfg(unix)] + pub uid: u32, + #[cfg(unix)] + pub gid: u32, + #[cfg(unix)] + pub mode: u32, +} + +impl Metadata { + pub fn from(path: &path::Path) -> io::Result { + #[cfg(unix)] + use std::os::unix::fs::MetadataExt; + + let symlink_metadata = fs::symlink_metadata(path)?; + let metadata = fs::metadata(path); + let (_len, _modified, _permissions) = match metadata.as_ref() { + Ok(m) => (m.len(), m.modified()?, m.permissions()), + Err(_) => ( + symlink_metadata.len(), + symlink_metadata.modified()?, + symlink_metadata.permissions(), + ), + }; + + let _file_type = match metadata.as_ref() { + Ok(m) if m.file_type().is_dir() => FileType::Directory, + _ => FileType::File, + }; + + let _link_type = if symlink_metadata.file_type().is_symlink() { + let mut link = "".to_string(); + + if let Ok(path) = fs::read_link(path) { + if let Some(s) = path.to_str() { + link = s.to_string(); + } + } + + let exists = path.exists(); + LinkType::Symlink(link, exists) + } else { + LinkType::Normal + }; + + #[cfg(unix)] + let uid = symlink_metadata.uid(); + #[cfg(unix)] + let gid = symlink_metadata.gid(); + #[cfg(unix)] + let mode = symlink_metadata.mode(); + + Ok(Self { + _len, + _modified, + _permissions, + _file_type, + _link_type, + #[cfg(unix)] + uid, + #[cfg(unix)] + gid, + #[cfg(unix)] + mode, + }) + } + + pub fn len(&self) -> u64 { + self._len + } + + pub fn modified(&self) -> time::SystemTime { + self._modified + } + + pub fn permissions_ref(&self) -> &fs::Permissions { + &self._permissions + } + + pub fn permissions_mut(&mut self) -> &mut fs::Permissions { + &mut self._permissions + } + + pub fn file_type(&self) -> &FileType { + &self._file_type + } + + pub fn link_type(&self) -> &LinkType { + &self._link_type + } + + pub fn is_dir(&self) -> bool { + self._file_type == FileType::Directory + } +} diff --git a/src/client/fs/mod.rs b/src/client/fs/mod.rs new file mode 100644 index 0000000..d64bf5b --- /dev/null +++ b/src/client/fs/mod.rs @@ -0,0 +1,7 @@ +mod dirlist; +mod entry; +mod metadata; + +pub use self::dirlist::DirList; +pub use self::entry::DirEntry; +pub use self::metadata::{FileType, Metadata, LinkType}; diff --git a/src/client/history.rs b/src/client/history.rs new file mode 100644 index 0000000..6b121ae --- /dev/null +++ b/src/client/history.rs @@ -0,0 +1,160 @@ +use std::collections::HashMap; +use std::fs; +use std::io; +use std::path::{Path, PathBuf}; + +use crate::fs::{DirEntry, DirList, Metadata}; +use crate::util::display_option::DisplayOption; + +pub trait DirectoryHistory { + fn populate_to_root(&mut self, path: &Path, options: &DisplayOption) -> io::Result<()>; + fn create_or_soft_update(&mut self, path: &Path, options: &DisplayOption) -> io::Result<()>; + fn create_or_reload(&mut self, path: &Path, options: &DisplayOption) -> io::Result<()>; + fn reload(&mut self, path: &Path, options: &DisplayOption) -> io::Result<()>; + fn depreciate_all_entries(&mut self); + + fn depreciate_entry(&mut self, path: &Path); +} + +pub type History = HashMap; + +impl DirectoryHistory for History { + fn populate_to_root(&mut self, path: &Path, options: &DisplayOption) -> io::Result<()> { + let mut dirlists = Vec::new(); + + let mut prev: Option<&Path> = None; + for curr in path.ancestors() { + if self.contains_key(curr) { + let mut new_dirlist = create_dirlist_with_history(self, curr, options)?; + if let Some(ancestor) = prev.as_ref() { + if let Some(i) = get_index_of_value(&new_dirlist.contents, ancestor) { + new_dirlist.index = Some(i); + } + } + dirlists.push(new_dirlist); + } else { + let mut new_dirlist = + DirList::from_path(curr.to_path_buf().clone(), options)?; + if let Some(ancestor) = prev.as_ref() { + if let Some(i) = get_index_of_value(&new_dirlist.contents, ancestor) { + new_dirlist.index = Some(i); + } + } + dirlists.push(new_dirlist); + } + prev = Some(curr); + } + for dirlist in dirlists { + self.insert(dirlist.file_path().to_path_buf(), dirlist); + } + Ok(()) + } + + fn create_or_soft_update(&mut self, path: &Path, options: &DisplayOption) -> io::Result<()> { + let (contains_key, need_update) = if let Some(dirlist) = self.get(path) { + (true, dirlist.need_update()) + } else { + (false, true) + }; + if need_update { + let dirlist = if contains_key { + create_dirlist_with_history(self, path, options)? + } else { + DirList::from_path(path.to_path_buf(), options)? + }; + self.insert(path.to_path_buf(), dirlist); + } + Ok(()) + } + + fn create_or_reload(&mut self, path: &Path, options: &DisplayOption) -> io::Result<()> { + let dirlist = if self.contains_key(path) { + create_dirlist_with_history(self, path, options)? + } else { + DirList::from_path(path.to_path_buf(), options)? + }; + self.insert(path.to_path_buf(), dirlist); + Ok(()) + } + + fn reload(&mut self, path: &Path, options: &DisplayOption) -> io::Result<()> { + let dirlist = create_dirlist_with_history(self, path, options)?; + self.insert(path.to_path_buf(), dirlist); + Ok(()) + } + + fn depreciate_all_entries(&mut self) { + self.iter_mut().for_each(|(_, v)| v.depreciate()); + } + + fn depreciate_entry(&mut self, path: &Path) { + if let Some(v) = self.get_mut(path) { + v.depreciate(); + } + } +} + +fn get_index_of_value(arr: &[DirEntry], val: &Path) -> Option { + arr.iter().enumerate().find_map(|(i, dir)| { + if dir.file_path() == val { + Some(i) + } else { + None + } + }) +} + +pub fn create_dirlist_with_history( + history: &History, + path: &Path, + options: &DisplayOption, +) -> io::Result { + let filter_func = options.filter_func(); + let mut contents = read_directory(path, filter_func, options)?; + + let sort_options = options.sort_options_ref(); + contents.sort_by(|f1, f2| sort_options.compare(f1, f2)); + + let contents_len = contents.len(); + let index: Option = if contents_len == 0 { + None + } else { + match history.get(path) { + Some(dirlist) => match dirlist.index { + Some(i) if i >= contents_len => Some(contents_len - 1), + Some(i) => { + let entry = &dirlist.contents[i]; + contents + .iter() + .enumerate() + .find(|(_, e)| e.file_name() == entry.file_name()) + .map(|(i, _)| i) + .or(Some(i)) + } + None => Some(0), + }, + None => Some(0), + } + }; + + let metadata = Metadata::from(path)?; + let dirlist = DirList::new(path.to_path_buf(), contents, index, metadata); + + Ok(dirlist) +} + +pub fn read_directory( + path: &Path, + filter_func: F, + options: &DisplayOption, +) -> io::Result> +where + F: Fn(&Result) -> bool, +{ + let results: Vec = fs::read_dir(path)? + .filter(filter_func) + .filter_map(|res| DirEntry::from(&res.ok()?, options).ok()) + .collect(); + + Ok(results) +} diff --git a/src/client/key_command/commands.rs b/src/client/key_command/commands.rs new file mode 100644 index 0000000..b6ce7fa --- /dev/null +++ b/src/client/key_command/commands.rs @@ -0,0 +1,58 @@ +use std::path; +use std::time; + +use crate::util::select::SelectOption; +use crate::util::sort_type::SortType; + +#[derive(Clone, Debug)] +pub enum Command { + Close, + Quit, + + ChangeDirectory(path::PathBuf), + CommandLine(String, String), + + CursorMoveUp(usize), + CursorMoveDown(usize), + CursorMoveHome, + CursorMoveEnd, + CursorMovePageUp, + CursorMovePageDown, + + OpenFile, + ParentDirectory, + + ReloadDirList, + + SearchGlob(String), + SearchString(String), + SearchSkim, + SearchNext, + SearchPrev, + + SelectFiles(String, SelectOption), + + Sort(SortType), + SortReverse, + + ToggleHiddenFiles, + + // player related + PlaylistGet, + PlaylistAdd, + PlaylistRemove, + + PlayerGet, + PlayerPlay, + PlayerPause, + PlayerTogglePlay, + PlayerToggleShuffle, + PlayerToggleRepeat, + PlayerToggleNext, + + PlayerVolumeUp(usize), + PlayerVolumeDown(usize), + + PlayerRewind(time::Duration), + PlayerFastForward(time::Duration), +} diff --git a/src/client/key_command/constants.rs b/src/client/key_command/constants.rs new file mode 100644 index 0000000..602bb4a --- /dev/null +++ b/src/client/key_command/constants.rs @@ -0,0 +1,26 @@ +pub const CMD_HELP: &str = "help"; + +pub const CMD_CLOSE: &str = "close"; +pub const CMD_QUIT: &str = "quit"; + +pub const CMD_CHANGE_DIRECTORY: &str = "cd"; +pub const CMD_COMMAND_LINE: &str = ":"; + +pub const CMD_CURSOR_MOVE_UP: &str = "cursor_move_up"; +pub const CMD_CURSOR_MOVE_DOWN: &str = "cursor_move_down"; +pub const CMD_CURSOR_MOVE_HOME: &str = "cursor_move_home"; +pub const CMD_CURSOR_MOVE_END: &str = "cursor_move_end"; +pub const CMD_CURSOR_MOVE_PAGEUP: &str = "cursor_move_page_up"; +pub const CMD_CURSOR_MOVE_PAGEDOWN: &str = "cursor_move_page_down"; +pub const CMD_OPEN_FILE: &str = "open"; +pub const CMD_PARENT_DIRECTORY: &str = "cd .."; +pub const CMD_RELOAD_DIRECTORY_LIST: &str = "reload_dirlist"; +pub const CMD_SEARCH_STRING: &str = "search"; +pub const CMD_SEARCH_GLOB: &str = "search_glob"; +pub const CMD_SEARCH_SKIM: &str = "search_skim"; +pub const CMD_SEARCH_NEXT: &str = "search_next"; +pub const CMD_SEARCH_PREV: &str = "search_prev"; +pub const CMD_SELECT_FILES: &str = "select"; +pub const CMD_SORT: &str = "sort"; +pub const CMD_SORT_REVERSE: &str = "sort reverse"; +pub const CMD_TOGGLE_HIDDEN: &str = "toggle_hidden"; diff --git a/src/client/key_command/impl_appcommand.rs b/src/client/key_command/impl_appcommand.rs new file mode 100644 index 0000000..7e0306b --- /dev/null +++ b/src/client/key_command/impl_appcommand.rs @@ -0,0 +1,60 @@ +use dizi_commands::constants::*; + +use super::constants::*; +use super::{AppCommand, Command}; + +impl AppCommand for Command { + fn command(&self) -> &'static str { + match self { + Self::Close => CMD_CLOSE, + Self::Quit => CMD_QUIT, + + Self::ChangeDirectory(_) => CMD_CHANGE_DIRECTORY, + Self::CommandLine(_, _) => CMD_COMMAND_LINE, + + Self::CursorMoveUp(_) => CMD_CURSOR_MOVE_UP, + Self::CursorMoveDown(_) => CMD_CURSOR_MOVE_DOWN, + Self::CursorMoveHome => CMD_CURSOR_MOVE_HOME, + Self::CursorMoveEnd => CMD_CURSOR_MOVE_END, + Self::CursorMovePageUp => CMD_CURSOR_MOVE_PAGEUP, + Self::CursorMovePageDown => CMD_CURSOR_MOVE_PAGEDOWN, + + Self::OpenFile => CMD_OPEN_FILE, + Self::ParentDirectory => CMD_PARENT_DIRECTORY, + + Self::ReloadDirList => CMD_RELOAD_DIRECTORY_LIST, + + Self::SearchString(_) => CMD_SEARCH_STRING, + Self::SearchGlob(_) => CMD_SEARCH_GLOB, + Self::SearchSkim => CMD_SEARCH_SKIM, + Self::SearchNext => CMD_SEARCH_NEXT, + Self::SearchPrev => CMD_SEARCH_PREV, + + Self::SelectFiles(_, _) => CMD_SELECT_FILES, + + Self::Sort(_) => CMD_SORT, + Self::SortReverse => CMD_SORT_REVERSE, + + Self::ToggleHiddenFiles => CMD_TOGGLE_HIDDEN, + + Self::PlaylistGet => API_PLAYLIST_GET, + Self::PlaylistAdd => API_PLAYLIST_ADD, + Self::PlaylistRemove => API_PLAYLIST_REMOVE, + + Self::PlayerGet => API_PLAYER_GET, + Self::PlayerPlay => API_PLAYER_PLAY, + Self::PlayerPause => API_PLAYER_PAUSE, + Self::PlayerTogglePlay => API_PLAYER_TOGGLE_PLAY, + Self::PlayerToggleShuffle => API_PLAYER_TOGGLE_SHUFFLE, + Self::PlayerToggleRepeat => API_PLAYER_TOGGLE_REPEAT, + Self::PlayerToggleNext => API_PLAYER_TOGGLE_NEXT, + + Self::PlayerVolumeUp(_) => API_PLAYER_VOLUME_UP, + Self::PlayerVolumeDown(_) => API_PLAYER_VOLUME_DOWN, + + Self::PlayerRewind(_) => API_PLAYER_REWIND, + Self::PlayerFastForward(_) => API_PLAYER_FAST_FORWARD, + + } + } +} diff --git a/src/client/key_command/impl_appexecute.rs b/src/client/key_command/impl_appexecute.rs new file mode 100644 index 0000000..b043d75 --- /dev/null +++ b/src/client/key_command/impl_appexecute.rs @@ -0,0 +1,58 @@ +use dizi_commands::error::DiziResult; + +use crate::commands::*; +use crate::config::AppKeyMapping; +use crate::context::AppContext; +use crate::ui::TuiBackend; + +use super::{AppExecute, Command}; + +impl AppExecute for Command { + fn execute( + &self, + context: &mut AppContext, + backend: &mut TuiBackend, + keymap_t: &AppKeyMapping, + ) -> DiziResult<()> { + match &*self { + Self::ChangeDirectory(p) => { + change_directory::change_directory(context, p.as_path())?; + Ok(()) + } + Self::CommandLine(p, s) => { + command_line::read_and_execute(context, backend, keymap_t, p.as_str(), s.as_str()) + } + + Self::CursorMoveUp(u) => cursor_move::up(context, *u), + Self::CursorMoveDown(u) => cursor_move::down(context, *u), + Self::CursorMoveHome => cursor_move::home(context), + Self::CursorMoveEnd => cursor_move::end(context), + Self::CursorMovePageUp => cursor_move::page_up(context, backend), + Self::CursorMovePageDown => cursor_move::page_down(context, backend), + + Self::OpenFile => open_file::open(context, backend), + Self::ParentDirectory => parent_directory::parent_directory(context), + + Self::Close => quit::close(context), + Self::Quit => quit::quit_server(context), + + Self::ReloadDirList => reload::reload_dirlist(context), + + Self::SearchGlob(pattern) => search_glob::search_glob(context, pattern.as_str()), + Self::SearchString(pattern) => search_string::search_string(context, pattern.as_str()), + Self::SearchSkim => search_skim::search_skim(context, backend), + Self::SearchNext => search::search_next(context), + Self::SearchPrev => search::search_prev(context), + + Self::SelectFiles(pattern, options) => { + selection::select_files(context, pattern.as_str(), options) + } + + Self::ToggleHiddenFiles => show_hidden::toggle_hidden(context), + + Self::Sort(t) => sort::set_sort(context, *t), + Self::SortReverse => sort::toggle_reverse(context), + s => { Ok(()) }, + } + } +} diff --git a/src/client/key_command/impl_display.rs b/src/client/key_command/impl_display.rs new file mode 100644 index 0000000..efbe6d7 --- /dev/null +++ b/src/client/key_command/impl_display.rs @@ -0,0 +1,20 @@ +use super::{AppCommand, Command}; + +impl std::fmt::Display for Command { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match &*self { + Self::ChangeDirectory(p) => write!(f, "{} {:?}", self.command(), p), + Self::CommandLine(s, p) => write!(f, "{} {} {}", self.command(), s, p), + Self::CursorMoveUp(i) => write!(f, "{} {}", self.command(), i), + Self::CursorMoveDown(i) => write!(f, "{} {}", self.command(), i), + + Self::SearchGlob(s) => write!(f, "{} {}", self.command(), s), + Self::SearchString(s) => write!(f, "{} {}", self.command(), s), + Self::SelectFiles(pattern, options) => { + write!(f, "{} {} {}", self.command(), pattern, options) + } + Self::Sort(t) => write!(f, "{} {}", self.command(), t), + _ => write!(f, "{}", self.command()), + } + } +} diff --git a/src/client/key_command/impl_from_str.rs b/src/client/key_command/impl_from_str.rs new file mode 100644 index 0000000..d21d18c --- /dev/null +++ b/src/client/key_command/impl_from_str.rs @@ -0,0 +1,147 @@ +use std::path; + +use dirs_next::home_dir; +use shellexpand::tilde_with_context; + +use dizi_commands::error::{DiziError, DiziErrorKind}; + +use crate::util::select::SelectOption; +use crate::util::sort_type::SortType; + +use crate::HOME_DIR; + +use super::constants::*; +use super::Command; + +impl std::str::FromStr for Command { + type Err = DiziError; + + fn from_str(s: &str) -> Result { + if let Some(stripped) = s.strip_prefix(':') { + return Ok(Self::CommandLine(stripped.to_owned(), "".to_owned())); + } + + let (command, arg) = match s.find(' ') { + Some(i) => (&s[..i], s[i..].trim_start()), + None => (s, ""), + }; + + if command == CMD_CHANGE_DIRECTORY { + match arg { + "" => match HOME_DIR.as_ref() { + Some(s) => Ok(Self::ChangeDirectory(s.clone())), + None => Err(DiziError::new( + DiziErrorKind::EnvVarNotPresent, + format!("{}: Cannot find home directory", command), + )), + }, + ".." => Ok(Self::ParentDirectory), + arg => Ok({ + let path_accepts_tilde = tilde_with_context(arg, home_dir); + Self::ChangeDirectory(path::PathBuf::from(path_accepts_tilde.as_ref())) + }), + } + } else if command == CMD_CURSOR_MOVE_HOME { + Ok(Self::CursorMoveHome) + } else if command == CMD_CURSOR_MOVE_END { + Ok(Self::CursorMoveEnd) + } else if command == CMD_CURSOR_MOVE_PAGEUP { + Ok(Self::CursorMovePageUp) + } else if command == CMD_CURSOR_MOVE_PAGEDOWN { + Ok(Self::CursorMovePageDown) + } else if command == CMD_CURSOR_MOVE_DOWN { + match arg { + "" => Ok(Self::CursorMoveDown(1)), + arg => match arg.trim().parse::() { + Ok(s) => Ok(Self::CursorMoveDown(s)), + Err(e) => Err(DiziError::new( + DiziErrorKind::ParseError, + e.to_string(), + )), + }, + } + } else if command == CMD_CURSOR_MOVE_UP { + match arg { + "" => Ok(Self::CursorMoveUp(1)), + arg => match arg.trim().parse::() { + Ok(s) => Ok(Self::CursorMoveUp(s)), + Err(e) => Err(DiziError::new( + DiziErrorKind::ParseError, + e.to_string(), + )), + }, + } + } else if command == CMD_CLOSE { + Ok(Self::Close) + } else if command == CMD_QUIT { + Ok(Self::Quit) + } else if command == CMD_OPEN_FILE { + Ok(Self::OpenFile) + } else if command == CMD_RELOAD_DIRECTORY_LIST { + Ok(Self::ReloadDirList) + } else if command == CMD_SEARCH_STRING { + match arg { + "" => Err(DiziError::new( + DiziErrorKind::InvalidParameters, + format!("{}: Expected 1, got 0", command), + )), + arg => Ok(Self::SearchString(arg.to_string())), + } + } else if command == CMD_SEARCH_GLOB { + match arg { + "" => Err(DiziError::new( + DiziErrorKind::InvalidParameters, + format!("{}: Expected 1, got 0", command), + )), + arg => Ok(Self::SearchGlob(arg.to_string())), + } + } else if command == CMD_SEARCH_SKIM { + Ok(Self::SearchSkim) + } else if command == CMD_SEARCH_NEXT { + Ok(Self::SearchNext) + } else if command == CMD_SEARCH_PREV { + Ok(Self::SearchPrev) + } else if command == CMD_SELECT_FILES { + let mut options = SelectOption::default(); + let mut pattern = ""; + match shell_words::split(arg) { + Ok(args) => { + for arg in args.iter() { + match arg.as_str() { + "--toggle=true" => options.toggle = true, + "--all=true" => options.all = true, + "--toggle=false" => options.toggle = false, + "--all=false" => options.all = false, + "--deselect=true" => options.reverse = true, + "--deselect=false" => options.reverse = false, + s => pattern = s, + } + } + Ok(Self::SelectFiles(pattern.to_string(), options)) + } + Err(e) => Err(DiziError::new( + DiziErrorKind::InvalidParameters, + format!("{}: {}", arg, e), + )), + } + } else if command == CMD_SORT { + match arg { + "reverse" => Ok(Self::SortReverse), + arg => match SortType::parse(arg) { + Some(s) => Ok(Self::Sort(s)), + None => Err(DiziError::new( + DiziErrorKind::InvalidParameters, + format!("{}: Unknown option '{}'", command, arg), + )), + }, + } + } else if command == CMD_TOGGLE_HIDDEN { + Ok(Self::ToggleHiddenFiles) + } else { + Err(DiziError::new( + DiziErrorKind::UnrecognizedCommand, + format!("Unrecognized command '{}'", command), + )) + } + } +} diff --git a/src/client/key_command/keybind.rs b/src/client/key_command/keybind.rs new file mode 100644 index 0000000..e989644 --- /dev/null +++ b/src/client/key_command/keybind.rs @@ -0,0 +1,18 @@ +use crate::config::AppKeyMapping; + +use super::Command; + +#[derive(Debug)] +pub enum CommandKeybind { + SimpleKeybind(Command), + CompositeKeybind(AppKeyMapping), +} + +impl std::fmt::Display for CommandKeybind { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + CommandKeybind::SimpleKeybind(s) => write!(f, "{}", s), + CommandKeybind::CompositeKeybind(_) => write!(f, "..."), + } + } +} diff --git a/src/client/key_command/mod.rs b/src/client/key_command/mod.rs new file mode 100644 index 0000000..1473cd2 --- /dev/null +++ b/src/client/key_command/mod.rs @@ -0,0 +1,14 @@ +pub mod keybind; +pub mod constants; +pub mod commands; +pub mod traits; + +mod impl_appcommand; +mod impl_appexecute; +mod impl_display; +mod impl_from_str; + +pub use self::commands::*; +pub use self::constants::*; +pub use self::keybind::*; +pub use self::traits::*; diff --git a/src/client/key_command/traits.rs b/src/client/key_command/traits.rs new file mode 100644 index 0000000..2e00514 --- /dev/null +++ b/src/client/key_command/traits.rs @@ -0,0 +1,22 @@ +use dizi_commands::error::DiziResult; + +use crate::config::AppKeyMapping; +use crate::context::AppContext; +use crate::ui::TuiBackend; + +pub trait AppExecute { + fn execute( + &self, + context: &mut AppContext, + backend: &mut TuiBackend, + keymap_t: &AppKeyMapping, + ) -> DiziResult<()>; +} + +pub trait AppCommand: AppExecute + std::fmt::Display + std::fmt::Debug { + fn command(&self) -> &'static str; +} + +pub trait CommandComment { + fn comment(&self) -> &'static str; +} diff --git a/src/client/main.rs b/src/client/main.rs new file mode 100644 index 0000000..9b3d534 --- /dev/null +++ b/src/client/main.rs @@ -0,0 +1,115 @@ +mod commands; +mod config; +mod context; +mod event; +mod fs; +mod history; +mod key_command; +mod preview; +mod run; +mod ui; +mod util; + +use std::os::unix::net::UnixStream; +use std::path::PathBuf; +use std::process; + +use lazy_static::lazy_static; +use structopt::StructOpt; +use dizi_commands::error::DiziResult; + +use crate::config::{AppConfig, AppKeyMapping, AppTheme, ConfigStructure}; +use crate::context::AppContext; +use crate::history::DirectoryHistory; +use crate::run::run; + +const PROGRAM_NAME: &str = "dizi"; +const CONFIG_FILE: &str = "client.toml"; +const KEYMAP_FILE: &str = "keymap.toml"; +const THEME_FILE: &str = "theme.toml"; + +lazy_static! { + // dynamically builds the config hierarchy + static ref CONFIG_HIERARCHY: Vec = { + let mut config_dirs = vec![]; + + if let Ok(p) = std::env::var("DIZI_CONFIG_HOME") { + let p = PathBuf::from(p); + if p.is_dir() { + config_dirs.push(p); + } + } + + if let Ok(dirs) = xdg::BaseDirectories::with_prefix(PROGRAM_NAME) { + config_dirs.push(dirs.get_config_home()); + } + + if let Ok(p) = std::env::var("HOME") { + let mut p = PathBuf::from(p); + p.push(format!(".config/{}", PROGRAM_NAME)); + if p.is_dir() { + config_dirs.push(p); + } + } + + // adds the default config files to the config hierarchy if running through cargo + if cfg!(debug_assertions) { + config_dirs.push(PathBuf::from("./config")); + } + config_dirs + }; + + static ref THEME_T: AppTheme = AppTheme::get_config(THEME_FILE); + static ref HOME_DIR: Option = dirs_next::home_dir(); +} + +#[derive(Clone, Debug, StructOpt)] +pub struct Args { + #[structopt(short = "v", long = "version")] + version: bool, +} + +fn run_app(args: Args) -> DiziResult<()> { + if args.version { + let version = env!("CARGO_PKG_VERSION"); + println!("{}", version); + return Ok(()); + } + + let config = AppConfig::get_config(CONFIG_FILE); + let keymap = AppKeyMapping::get_config(KEYMAP_FILE); + + if let Err(_) = UnixStream::connect(&config.client_ref().socket) { + process::Command::new("dizi-server") + .spawn(); + } + let stream = UnixStream::connect(&config.client_ref().socket)?; + + let cwd = std::env::current_dir()?; + let mut context = AppContext::new(config, cwd.clone(), stream); + + let display_options = context + .config_ref() + .client_ref() + .display_options_ref() + .clone(); + context.history_mut().populate_to_root(cwd.as_path(), &display_options)?; + + + let mut backend: ui::TuiBackend = ui::TuiBackend::new()?; + run(&mut backend, &mut context, keymap)?; + + Ok(()) +} + +fn main() { + let args = Args::from_args(); + + match run_app(args) { + Ok(_) => {} + Err(e) => { + eprintln!("{}", e.to_string()); + process::exit(1); + } + } +} diff --git a/src/client/preview/mod.rs b/src/client/preview/mod.rs new file mode 100644 index 0000000..70bdaf1 --- /dev/null +++ b/src/client/preview/mod.rs @@ -0,0 +1,2 @@ +pub mod preview_default; +pub mod preview_dir; diff --git a/src/client/preview/preview_default.rs b/src/client/preview/preview_default.rs new file mode 100644 index 0000000..6d3fef6 --- /dev/null +++ b/src/client/preview/preview_default.rs @@ -0,0 +1,47 @@ +use std::path; + +use crate::context::AppContext; +use crate::fs::Metadata; +use crate::preview::preview_dir; +use crate::ui::TuiBackend; + +pub fn load_preview_path( + context: &mut AppContext, + backend: &mut TuiBackend, + p: path::PathBuf, + metadata: Metadata, +) { + if metadata.is_dir() { + let need_to_load = context + .history_ref() + .get(p.as_path()) + .map(|e| e.need_update()) + .unwrap_or(true); + + if need_to_load { + preview_dir::Background::load_preview(context, p); + } + } +} + +pub fn load_preview(context: &mut AppContext, backend: &mut TuiBackend) { + let mut load_list = Vec::with_capacity(2); + + match context.curr_list_ref() { + Some(curr_list) => { + if let Some(index) = curr_list.index { + let entry = &curr_list.contents[index]; + load_list.push((entry.file_path().to_path_buf(), entry.metadata.clone())); + } + } + None => { + if let Ok(metadata) = Metadata::from(context.cwd()) { + load_list.push((context.cwd().to_path_buf(), metadata)); + } + } + } + + for (path, metadata) in load_list { + load_preview_path(context, backend, path, metadata); + } +} diff --git a/src/client/preview/preview_dir.rs b/src/client/preview/preview_dir.rs new file mode 100644 index 0000000..7d9bbfc --- /dev/null +++ b/src/client/preview/preview_dir.rs @@ -0,0 +1,40 @@ +use std::io; +use std::path; +use std::thread; + +use crate::context::AppContext; +use crate::event::AppEvent; +use crate::fs::DirList; +use crate::history::DirectoryHistory; + +pub struct Foreground {} + +impl Foreground { + pub fn load_preview(context: &mut AppContext, p: path::PathBuf) -> io::Result<()> { + let options = context.config_ref().display_options_ref().clone(); + let history = context.history_mut(); + if history + .create_or_soft_update(p.as_path(), &options) + .is_err() + { + history.remove(p.as_path()); + } + Ok(()) + } +} + +pub struct Background {} + +impl Background { + pub fn load_preview(context: &mut AppContext, p: path::PathBuf) -> thread::JoinHandle<()> { + let event_tx = context.events.event_tx.clone(); + let options = context.config_ref().display_options_ref().clone(); + let handle = thread::spawn(move || match DirList::from_path(p, &options) { + Ok(dirlist) => { + let _ = event_tx.send(AppEvent::PreviewDir(Ok(dirlist))); + } + Err(_) => {} + }); + handle + } +} diff --git a/src/client/run.rs b/src/client/run.rs new file mode 100644 index 0000000..c73d105 --- /dev/null +++ b/src/client/run.rs @@ -0,0 +1,52 @@ +use termion::event::Event; + +use dizi_commands::error::DiziResult; + +use crate::key_command::{AppExecute, Command, CommandKeybind}; +use crate::config::AppKeyMapping; +use crate::context::{AppContext, QuitType}; +use crate::event::AppEvent; +use crate::preview::preview_default; +use crate::ui::views::TuiView; +use crate::ui::TuiBackend; +use crate::util::input; + +pub fn run( + backend: &mut TuiBackend, + context: &mut AppContext, + keymap_t: AppKeyMapping, +) -> DiziResult<()> { + + while context.quit == QuitType::DoNot { + backend.render(TuiView::new(&context)); + + let event = match context.poll_event() { + Ok(event) => event, + Err(_) => return Ok(()), // TODO + }; + + match event { + AppEvent::Termion(Event::Mouse(event)) => { + context.flush_event(); + } + AppEvent::Termion(key) => { + match keymap_t.as_ref().get(&key) { + None => { + // handle error + } + Some(CommandKeybind::SimpleKeybind(command)) => { + if let Err(e) = command.execute(context, backend, &keymap_t) { + // handle error + } + } + Some(CommandKeybind::CompositeKeybind(m)) => { + } + } + context.flush_event(); + preview_default::load_preview(context, backend); + } + event => input::process_noninteractive(event, context), + } + } + Ok(()) +} diff --git a/src/client/ui/mod.rs b/src/client/ui/mod.rs new file mode 100644 index 0000000..16ebd19 --- /dev/null +++ b/src/client/ui/mod.rs @@ -0,0 +1,5 @@ +mod tui_backend; +pub mod views; +pub mod widgets; + +pub use tui_backend::*; diff --git a/src/client/ui/tui_backend.rs b/src/client/ui/tui_backend.rs new file mode 100644 index 0000000..336226f --- /dev/null +++ b/src/client/ui/tui_backend.rs @@ -0,0 +1,133 @@ +use std::io::stdout; +use std::io::Write; + +use termion::raw::{IntoRawMode, RawTerminal}; +use termion::screen::AlternateScreen; +use tui::backend::TermionBackend; +use tui::layout::{Constraint, Direction, Layout, Rect}; +use tui::widgets::{Block, Borders, Widget}; + +#[cfg(feature = "mouse")] +use termion::input::MouseTerminal; + +use crate::util::display_option::DisplayOption; + +trait New { + fn new() -> std::io::Result + where + Self: Sized; +} + +#[cfg(feature = "mouse")] +type Screen = MouseTerminal>>; +#[cfg(feature = "mouse")] +impl New for Screen { + fn new() -> std::io::Result { + let stdout = std::io::stdout().into_raw_mode()?; + let alt_screen = MouseTerminal::from(AlternateScreen::from(stdout)); + return Ok(alt_screen); + } +} +#[cfg(not(feature = "mouse"))] +type Screen = AlternateScreen>; +#[cfg(not(feature = "mouse"))] +impl New for Screen { + fn new() -> std::io::Result { + let stdout = std::io::stdout().into_raw_mode()?; + let alt_screen = AlternateScreen::from(stdout); + Ok(alt_screen) + } +} + +pub type JoshutoTerminal = tui::Terminal>; + +pub struct TuiBackend { + pub terminal: Option, +} + +impl TuiBackend { + pub fn new() -> std::io::Result { + let mut alt_screen = Screen::new()?; + // clears the screen of artifacts + write!(alt_screen, "{}", termion::clear::All)?; + + let backend = TermionBackend::new(alt_screen); + let mut terminal = tui::Terminal::new(backend)?; + terminal.hide_cursor()?; + Ok(Self { + terminal: Some(terminal), + }) + } + + pub fn render(&mut self, widget: W) + where + W: Widget, + { + let _ = self.terminal_mut().draw(|frame| { + let rect = frame.size(); + frame.render_widget(widget, rect); + }); + } + + pub fn terminal_mut(&mut self) -> &mut JoshutoTerminal { + self.terminal.as_mut().unwrap() + } + + pub fn terminal_drop(&mut self) { + let _ = self.terminal.take(); + let _ = stdout().flush(); + } + + pub fn terminal_restore(&mut self) -> std::io::Result<()> { + let mut new_backend = TuiBackend::new()?; + std::mem::swap(&mut self.terminal, &mut new_backend.terminal); + Ok(()) + } +} + +pub fn build_layout( + area: Rect, + constraints: &[Constraint; 3], + display_options: &DisplayOption, +) -> Vec { + let layout_rect = if display_options.show_borders() { + let area = Rect { + y: area.top() + 1, + height: area.height - 2, + ..area + }; + + let block = Block::default().borders(Borders::ALL); + let inner = block.inner(area); + + let layout_rect = Layout::default() + .direction(Direction::Horizontal) + .constraints(constraints.as_ref()) + .split(inner); + + let block = Block::default().borders(Borders::RIGHT); + let inner1 = block.inner(layout_rect[0]); + + let block = Block::default().borders(Borders::LEFT); + let inner3 = block.inner(layout_rect[2]); + + vec![inner1, layout_rect[1], inner3] + } else { + let mut layout_rect = Layout::default() + .direction(Direction::Horizontal) + .vertical_margin(1) + .constraints(constraints.as_ref()) + .split(area); + + layout_rect[0] = Rect { + width: layout_rect[0].width - 1, + ..layout_rect[0] + }; + layout_rect[1] = Rect { + width: layout_rect[1].width - 1, + ..layout_rect[1] + }; + layout_rect + }; + layout_rect +} diff --git a/src/client/ui/views/mod.rs b/src/client/ui/views/mod.rs new file mode 100644 index 0000000..cded290 --- /dev/null +++ b/src/client/ui/views/mod.rs @@ -0,0 +1,9 @@ +mod tui_command_menu; +mod tui_folder_view; +mod tui_textfield; +mod tui_view; + +pub use self::tui_command_menu::TuiCommandMenu; +pub use self::tui_folder_view::TuiFolderView; +pub use self::tui_textfield::TuiTextField; +pub use self::tui_view::TuiView; diff --git a/src/client/ui/views/tui_command_menu.rs b/src/client/ui/views/tui_command_menu.rs new file mode 100644 index 0000000..bc94752 --- /dev/null +++ b/src/client/ui/views/tui_command_menu.rs @@ -0,0 +1,64 @@ +use std::iter::Iterator; + +use tui::buffer::Buffer; +use tui::layout::Rect; +use tui::widgets::{Clear, Widget}; + +use crate::config::AppKeyMapping; +use crate::context::AppContext; +use crate::ui::views::TuiView; +use crate::ui::widgets::TuiMenu; +use crate::util::to_string::ToString; + +const BORDER_HEIGHT: usize = 1; +const BOTTOM_MARGIN: usize = 1; + +pub struct TuiCommandMenu<'a> { + context: &'a AppContext, + keymap: &'a AppKeyMapping, +} + +impl<'a> TuiCommandMenu<'a> { + pub fn new(context: &'a AppContext, keymap: &'a AppKeyMapping) -> Self { + Self { context, keymap } + } +} + +impl<'a> Widget for TuiCommandMenu<'a> { + fn render(self, area: Rect, buf: &mut Buffer) { + TuiView::new(self.context).render(area, buf); + + // draw menu + let mut display_vec: Vec = self + .keymap + .as_ref() + .iter() + .map(|(k, v)| format!(" {} {}", k.to_string(), v)) + .collect(); + display_vec.sort(); + let display_str: Vec<&str> = display_vec.iter().map(|v| v.as_str()).collect(); + let display_str_len = display_str.len(); + + let y = if (area.height as usize) < display_str_len + BORDER_HEIGHT + BOTTOM_MARGIN { + 0 + } else { + area.height - (BORDER_HEIGHT + BOTTOM_MARGIN) as u16 - display_str_len as u16 + }; + + let menu_height = if display_str_len + BORDER_HEIGHT > area.height as usize { + area.height + } else { + (display_str_len + BORDER_HEIGHT) as u16 + }; + + let menu_rect = Rect { + x: 0, + y, + width: area.width, + height: menu_height, + }; + + Clear.render(menu_rect, buf); + TuiMenu::new(&display_str).render(menu_rect, buf); + } +} diff --git a/src/client/ui/views/tui_folder_view.rs b/src/client/ui/views/tui_folder_view.rs new file mode 100644 index 0000000..82cc343 --- /dev/null +++ b/src/client/ui/views/tui_folder_view.rs @@ -0,0 +1,144 @@ +use tui::buffer::Buffer; +use tui::layout::{Constraint, Direction, Layout, Rect}; +use tui::style::{Color, Style}; +use tui::symbols::line::{HORIZONTAL_DOWN, HORIZONTAL_UP}; +use tui::text::Span; +use tui::widgets::{Block, Borders, Paragraph, Widget, Wrap}; + +use crate::context::AppContext; +use crate::ui::widgets::{ + TuiDirListDetailed, TuiFooter, TuiTopBar, +}; + +const TAB_VIEW_WIDTH: u16 = 15; + +pub struct TuiFolderView<'a> { + pub context: &'a AppContext, + pub show_bottom_status: bool, +} + +impl<'a> TuiFolderView<'a> { + pub fn new(context: &'a AppContext) -> Self { + Self { + context, + show_bottom_status: true, + } + } +} + +impl<'a> Widget for TuiFolderView<'a> { + fn render(self, area: Rect, buf: &mut Buffer) { + let curr_list = self.context.curr_list_ref(); + + let curr_entry = curr_list.and_then(|c| c.curr_entry_ref()); + + let config = self.context.config_ref(); + let display_options = config.display_options_ref(); + + let (default_layout, constraints): (bool, &[Constraint; 3]) = + (true, &display_options.default_layout); + + let layout_rect = if config.display_options_ref().show_borders() { + let area = Rect { + y: area.top() + 1, + height: area.height - 2, + ..area + }; + + let block = Block::default().borders(Borders::ALL); + let inner = block.inner(area); + block.render(area, buf); + + let layout_rect = Layout::default() + .direction(Direction::Horizontal) + .constraints(constraints.as_ref()) + .split(inner); + + // Render inner borders properly. + { + let top = area.top(); + let bottom = area.bottom() - 1; + let left = layout_rect[1].left() - 1; + let right = layout_rect[2].left(); + let intersections = Intersections { + top, + bottom, + left, + right, + }; + + intersections.render_left(buf); + if default_layout { + intersections.render_right(buf); + } + } + + let block = Block::default().borders(Borders::RIGHT); + let inner1 = block.inner(layout_rect[0]); + block.render(layout_rect[0], buf); + + let block = Block::default().borders(Borders::LEFT); + let inner3 = block.inner(layout_rect[2]); + block.render(layout_rect[2], buf); + + vec![inner1, layout_rect[1], inner3] + } else { + let mut layout_rect = Layout::default() + .direction(Direction::Horizontal) + .vertical_margin(1) + .constraints(constraints.as_ref()) + .split(area); + + layout_rect[0] = Rect { + width: layout_rect[0].width - 1, + ..layout_rect[0] + }; + layout_rect[1] = Rect { + width: layout_rect[1].width - 1, + ..layout_rect[1] + }; + layout_rect + }; + + // render current view + if let Some(list) = curr_list.as_ref() { + TuiDirListDetailed::new(list).render(layout_rect[1], buf); + let rect = Rect { + x: 0, + y: area.height - 1, + width: area.width, + height: 1, + }; + } + + let topbar_width = area.width; + let rect = Rect { + x: 0, + y: 0, + width: topbar_width, + height: 1, + }; + TuiTopBar::new(self.context, self.context.cwd()).render(rect, buf); + } +} + +struct Intersections { + top: u16, + bottom: u16, + left: u16, + right: u16, +} + +impl Intersections { + fn render_left(&self, buf: &mut Buffer) { + buf.get_mut(self.left, self.top).set_symbol(HORIZONTAL_DOWN); + buf.get_mut(self.left, self.bottom) + .set_symbol(HORIZONTAL_UP); + } + fn render_right(&self, buf: &mut Buffer) { + buf.get_mut(self.right, self.top) + .set_symbol(HORIZONTAL_DOWN); + buf.get_mut(self.right, self.bottom) + .set_symbol(HORIZONTAL_UP); + } +} diff --git a/src/client/ui/views/tui_textfield.rs b/src/client/ui/views/tui_textfield.rs new file mode 100644 index 0000000..c86b49f --- /dev/null +++ b/src/client/ui/views/tui_textfield.rs @@ -0,0 +1,265 @@ +use rustyline::completion::{Candidate, Completer, FilenameCompleter, Pair}; +use rustyline::line_buffer; + +use termion::event::{Event, Key}; +use tui::layout::Rect; +use tui::widgets::Clear; +use unicode_width::UnicodeWidthStr; + +use crate::context::AppContext; +use crate::event::AppEvent; +use crate::ui::views::TuiView; +use crate::ui::widgets::{TuiMenu, TuiMultilineText}; +use crate::ui::TuiBackend; +use crate::util::input; + +struct CompletionTracker { + pub index: usize, + pub pos: usize, + pub original: String, + pub candidates: Vec, +} + +impl CompletionTracker { + pub fn new(pos: usize, candidates: Vec, original: String) -> Self { + CompletionTracker { + index: 0, + pos, + original, + candidates, + } + } +} + +pub struct CursorInfo { + pub x: usize, + pub y: usize, +} + +pub struct TuiTextField<'a> { + _prompt: &'a str, + _prefix: &'a str, + _suffix: &'a str, + _menu_items: Vec<&'a str>, +} + +impl<'a> TuiTextField<'a> { + pub fn menu_items(&mut self, items: I) -> &mut Self + where + I: Iterator, + { + self._menu_items = items.collect(); + self + } + + pub fn prompt(&mut self, prompt: &'a str) -> &mut Self { + self._prompt = prompt; + self + } + + pub fn prefix(&mut self, prefix: &'a str) -> &mut Self { + self._prefix = prefix; + self + } + + pub fn suffix(&mut self, suffix: &'a str) -> &mut Self { + self._suffix = suffix; + self + } + + pub fn get_input( + &mut self, + backend: &mut TuiBackend, + context: &mut AppContext, + ) -> Option { + let mut line_buffer = line_buffer::LineBuffer::with_capacity(255); + let completer = FilenameCompleter::new(); + + let mut completion_tracker: Option = None; + + let char_idx = self._prefix.chars().map(|c| c.len_utf8()).sum(); + + line_buffer.insert_str(0, self._suffix); + line_buffer.insert_str(0, self._prefix); + line_buffer.set_pos(char_idx); + + let terminal = backend.terminal_mut(); + let _ = terminal.show_cursor(); + + loop { + terminal + .draw(|frame| { + let area: Rect = frame.size(); + if area.height == 0 { + return; + } + { + let mut view = TuiView::new(context); + view.show_bottom_status = false; + frame.render_widget(view, area); + } + + let area_width = area.width as usize; + let buffer_str = line_buffer.as_str(); + let cursor_xpos = line_buffer.pos(); + + let line_str = format!("{}{}", self._prompt, buffer_str); + let multiline = TuiMultilineText::new(line_str.as_str(), area_width); + let multiline_height = multiline.height(); + + // render menu + { + let menu_widget = TuiMenu::new(self._menu_items.as_slice()); + let menu_len = menu_widget.len(); + let menu_y = if menu_len + 1 > area.height as usize { + 0 + } else { + (area.height as usize - menu_len - 1) as u16 + }; + + let menu_rect = Rect { + x: 0, + y: menu_y - multiline_height as u16, + width: area.width, + height: menu_len as u16 + 1, + }; + frame.render_widget(Clear, menu_rect); + frame.render_widget(menu_widget, menu_rect); + } + + let multiline_rect = Rect { + x: 0, + y: area.height - multiline_height as u16, + width: area.width, + height: multiline_height as u16, + }; + let mut cursor_info = CursorInfo { + x: 0, + y: area.height as usize, + }; + + // get cursor render position + let cursor_prefix_width = + buffer_str[0..cursor_xpos].width() + self._prompt.len(); + let y_offset = cursor_prefix_width / area_width; + cursor_info.y = area.height as usize - multiline_height + y_offset; + cursor_info.x = cursor_prefix_width % area_width + y_offset; + + // render multiline textfield + frame.render_widget(Clear, multiline_rect); + frame.render_widget(multiline, multiline_rect); + + // render cursor + frame.set_cursor(cursor_info.x as u16, cursor_info.y as u16); + }) + .unwrap(); + + if let Ok(event) = context.poll_event() { + match event { + AppEvent::Termion(Event::Key(key)) => { + match key { + Key::Backspace => { + if line_buffer.backspace(1) { + completion_tracker.take(); + } + } + Key::Left => { + if line_buffer.move_backward(1) { + completion_tracker.take(); + } + } + Key::Right => { + if line_buffer.move_forward(1) { + completion_tracker.take(); + } + } + Key::Delete => { + if line_buffer.delete(1).is_some() { + completion_tracker.take(); + } + } + Key::Home => { + line_buffer.move_home(); + completion_tracker.take(); + } + Key::End => { + line_buffer.move_end(); + completion_tracker.take(); + } + Key::Up => {} + Key::Down => {} + Key::Esc => { + let _ = terminal.hide_cursor(); + return None; + } + Key::Char('\t') => { + if completion_tracker.is_none() { + let res = completer + .complete_path(line_buffer.as_str(), line_buffer.pos()); + if let Ok((pos, mut candidates)) = res { + candidates.sort_by(|x, y| { + x.display() + .partial_cmp(y.display()) + .unwrap_or(std::cmp::Ordering::Less) + }); + let ct = CompletionTracker::new( + pos, + candidates, + String::from(line_buffer.as_str()), + ); + completion_tracker = Some(ct); + } + } + + if let Some(ref mut s) = completion_tracker { + if s.index < s.candidates.len() { + let candidate = &s.candidates[s.index]; + completer.update( + &mut line_buffer, + s.pos, + candidate.display(), + ); + s.index += 1; + } + } + } + Key::Char('\n') => { + break; + } + Key::Char(c) => { + if line_buffer.insert(c, 1).is_some() { + completion_tracker.take(); + } + } + _ => {} + } + context.flush_event(); + } + AppEvent::Termion(_) => { + context.flush_event(); + } + event => input::process_noninteractive(event, context), + }; + } + } + let _ = terminal.hide_cursor(); + + if line_buffer.as_str().is_empty() { + None + } else { + let input_string = line_buffer.to_string(); + Some(input_string) + } + } +} + +impl<'a> std::default::Default for TuiTextField<'a> { + fn default() -> Self { + Self { + _prompt: "", + _prefix: "", + _suffix: "", + _menu_items: vec![], + } + } +} diff --git a/src/client/ui/views/tui_view.rs b/src/client/ui/views/tui_view.rs new file mode 100644 index 0000000..2178475 --- /dev/null +++ b/src/client/ui/views/tui_view.rs @@ -0,0 +1,26 @@ +use tui::buffer::Buffer; +use tui::layout::Rect; +use tui::widgets::Widget; + +use super::TuiFolderView; +use crate::context::AppContext; + +pub struct TuiView<'a> { + pub context: &'a AppContext, + pub show_bottom_status: bool, +} + +impl<'a> TuiView<'a> { + pub fn new(context: &'a AppContext) -> Self { + Self { + context, + show_bottom_status: true, + } + } +} + +impl<'a> Widget for TuiView<'a> { + fn render(self, area: Rect, buf: &mut Buffer) { + TuiFolderView::new(self.context).render(area, buf); + } +} diff --git a/src/client/ui/widgets/mod.rs b/src/client/ui/widgets/mod.rs new file mode 100644 index 0000000..8ca1672 --- /dev/null +++ b/src/client/ui/widgets/mod.rs @@ -0,0 +1,13 @@ +mod tui_dirlist_detailed; +mod tui_footer; +mod tui_menu; +mod tui_prompt; +mod tui_text; +mod tui_topbar; + +pub use self::tui_dirlist_detailed::{trim_file_label, TuiDirListDetailed}; +pub use self::tui_footer::TuiFooter; +pub use self::tui_menu::TuiMenu; +pub use self::tui_prompt::TuiPrompt; +pub use self::tui_text::TuiMultilineText; +pub use self::tui_topbar::TuiTopBar; diff --git a/src/client/ui/widgets/tui_dirlist_detailed.rs b/src/client/ui/widgets/tui_dirlist_detailed.rs new file mode 100644 index 0000000..645623f --- /dev/null +++ b/src/client/ui/widgets/tui_dirlist_detailed.rs @@ -0,0 +1,300 @@ +use tui::buffer::Buffer; +use tui::layout::Rect; +use tui::style::{Color, Modifier, Style}; +use tui::widgets::Widget; + +use crate::fs::{FileType, DirEntry, DirList, LinkType}; +use crate::util::format; +use crate::util::string::UnicodeTruncate; +use crate::util::style; +use unicode_width::UnicodeWidthStr; + +const MIN_LEFT_LABEL_WIDTH: i32 = 15; + +const ELLIPSIS: &str = "…"; + +pub struct TuiDirListDetailed<'a> { + dirlist: &'a DirList, +} + +impl<'a> TuiDirListDetailed<'a> { + pub fn new(dirlist: &'a DirList) -> Self { + Self { dirlist } + } +} + +impl<'a> Widget for TuiDirListDetailed<'a> { + fn render(self, area: Rect, buf: &mut Buffer) { + if area.width < 4 || area.height < 1 { + return; + } + + let x = area.left(); + let y = area.top(); + let curr_index = match self.dirlist.index { + Some(i) => i, + None => { + let style = Style::default().bg(Color::Red).fg(Color::White); + buf.set_stringn(x, y, "empty", area.width as usize, style); + return; + } + }; + + let drawing_width = area.width as usize; + let skip_dist = self.dirlist.first_index_for_viewport(area.height as usize); + + // draw every entry + self.dirlist + .iter() + .skip(skip_dist) + .enumerate() + .take(area.height as usize) + .for_each(|(i, entry)| { + let style = style::entry_style(entry); + print_entry(buf, entry, style, (x + 1, y + i as u16), drawing_width - 1); + }); + + // draw selected entry in a different style + let screen_index = curr_index % area.height as usize; + + let entry = self.dirlist.curr_entry_ref().unwrap(); + let style = style::entry_style(entry).add_modifier(Modifier::REVERSED); + + let space_fill = " ".repeat(drawing_width); + buf.set_string(x, y + screen_index as u16, space_fill.as_str(), style); + + print_entry( + buf, + entry, + style, + (x + 1, y + screen_index as u16), + drawing_width - 1, + ); + } +} + +fn print_entry( + buf: &mut Buffer, + entry: &DirEntry, + style: Style, + (x, y): (u16, u16), + drawing_width: usize, +) { + let size_string = match entry.metadata.file_type() { + FileType::Directory => "".to_string(), + FileType::File => format::file_size_to_string(entry.metadata.len()), + }; + let symlink_string = match entry.metadata.link_type() { + LinkType::Normal => "", + LinkType::Symlink(_, _) => "-> ", + }; + let left_label_original = entry.label(); + let right_label_original = format!(" {}{} ", symlink_string, size_string); + + let (left_label, right_label) = factor_labels_for_entry( + left_label_original, + right_label_original.as_str(), + drawing_width, + ); + + let right_width = right_label.width(); + buf.set_stringn(x, y, left_label, drawing_width, style); + buf.set_stringn( + x + drawing_width as u16 - right_width as u16, + y, + right_label, + drawing_width, + style, + ); +} + +fn factor_labels_for_entry<'a>( + left_label_original: &'a str, + right_label_original: &'a str, + drawing_width: usize, +) -> (String, &'a str) { + let left_label_original_width = left_label_original.width(); + let right_label_original_width = right_label_original.width(); + + let left_width_remainder = drawing_width as i32 - right_label_original_width as i32; + let width_remainder = left_width_remainder as i32 - left_label_original_width as i32; + + if drawing_width == 0 { + ("".to_string(), "") + } else if width_remainder >= 0 { + (left_label_original.to_string(), right_label_original) + } else { + if left_width_remainder < MIN_LEFT_LABEL_WIDTH { + ( + if left_label_original.width() as i32 <= left_width_remainder { + trim_file_label(left_label_original, drawing_width) + } else { + left_label_original.to_string() + }, + "", + ) + } else { + ( + trim_file_label(left_label_original, left_width_remainder as usize), + right_label_original, + ) + } + } +} + +pub fn trim_file_label(name: &str, drawing_width: usize) -> String { + // pre-condition: string name is longer than width + let (stem, extension) = match name.rfind('.') { + None => (name, ""), + Some(i) => name.split_at(i), + }; + if drawing_width < 1 { + "".to_string() + } else if stem.is_empty() || extension.is_empty() { + let full = format!("{}{}", stem, extension); + let mut truncated = full.trunc(drawing_width - 1); + truncated.push_str(ELLIPSIS); + truncated + } else { + let ext_width = extension.width(); + if ext_width > drawing_width { + // file ext does not fit + let stem_width = drawing_width; + let truncated_stem = stem.trunc(stem_width - 3); + format!("{}{}.{}", truncated_stem, ELLIPSIS, ELLIPSIS) + } else if ext_width == drawing_width { + extension.replacen('.', ELLIPSIS, 1) + } else { + let stem_width = drawing_width - ext_width; + let truncated_stem = stem.trunc(stem_width - 1); + format!("{}{}{}", truncated_stem, ELLIPSIS, extension) + } + } +} + +#[cfg(test)] +mod test_factor_labels { + use super::{factor_labels_for_entry, MIN_LEFT_LABEL_WIDTH}; + + #[test] + fn both_labels_empty_if_drawing_width_zero() { + let left = "foo.ext"; + let right = "right"; + assert_eq!( + ("".to_string(), ""), + factor_labels_for_entry(left, right, 0) + ); + } + + #[test] + fn nothing_changes_if_all_labels_fit_easily() { + let left = "foo.ext"; + let right = "right"; + assert_eq!( + (left.to_string(), right), + factor_labels_for_entry(left, right, 20) + ); + } + + #[test] + fn nothing_changes_if_all_labels_just_fit() { + let left = "foo.ext"; + let right = "right"; + assert_eq!( + (left.to_string(), right), + factor_labels_for_entry(left, right, 12) + ); + } + + #[test] + fn right_label_omitted_if_left_label_would_need_to_be_shortened_below_min_left_label_width() { + let left = "foobarbazfo.ext"; + let right = "right"; + assert!(left.chars().count() as i32 == MIN_LEFT_LABEL_WIDTH); + assert_eq!( + ("foobarbazfo.ext".to_string(), ""), + factor_labels_for_entry(left, right, MIN_LEFT_LABEL_WIDTH as usize) + ); + } + + #[test] + fn right_label_is_kept_if_left_label_is_not_shortened_below_min_left_label_width() { + let left = "foobarbazfoobarbaz.ext"; + let right = "right"; + assert!(left.chars().count() as i32 > MIN_LEFT_LABEL_WIDTH + right.chars().count() as i32); + assert_eq!( + ("foobarbazf….ext".to_string(), right), + factor_labels_for_entry( + left, + right, + MIN_LEFT_LABEL_WIDTH as usize + right.chars().count() + ) + ); + } + + #[test] + // regression + fn file_name_which_is_smaller_or_equal_drawing_width_does_not_cause_right_label_to_be_omitted() + { + let left = "foooooobaaaaaaarbaaaaaaaaaz"; + let right = "right"; + assert!(left.chars().count() as i32 > MIN_LEFT_LABEL_WIDTH); + assert_eq!( + ("foooooobaaaaaaarbaaaa…".to_string(), right), + factor_labels_for_entry(left, right, left.chars().count()) + ); + } +} + +#[cfg(test)] +mod test_trim_file_label { + use super::trim_file_label; + + #[test] + fn dotfiles_get_an_ellipsis_at_the_end_if_they_dont_fit() { + let label = ".joshuto"; + assert_eq!(".jos…".to_string(), trim_file_label(label, 5)); + } + + #[test] + fn dotless_files_get_an_ellipsis_at_the_end_if_they_dont_fit() { + let label = "Desktop"; + assert_eq!("Desk…".to_string(), trim_file_label(label, 5)); + } + + #[test] + fn if_the_extension_doesnt_fit_show_stem_with_double_ellipse() { + let label = "12345678.12345678910"; + assert_eq!("12345….…".to_string(), trim_file_label(label, 8)); + } + + #[test] + fn if_just_the_extension_fits_its_shown_with_an_ellipsis_instead_of_a_dot() { + let left = "foo.ext"; + assert_eq!("…ext".to_string(), trim_file_label(left, 4)); + } + + #[test] + fn if_the_extension_fits_the_stem_is_truncated_with_an_appended_ellipsis_1() { + let left = "foo.ext"; + assert_eq!("….ext".to_string(), trim_file_label(left, 5)); + } + + #[test] + fn if_the_extension_fits_the_stem_is_truncated_with_an_appended_ellipsis_2() { + let left = "foo.ext"; + assert_eq!("f….ext".to_string(), trim_file_label(left, 6)); + } + + #[test] + fn if_the_name_is_truncated_after_a_full_width_character_the_ellipsis_is_shown_correctly() { + let left = "🌕🌕🌕"; + assert_eq!("🌕…".to_string(), trim_file_label(left, 4)); + } + + #[test] + fn if_the_name_is_truncated_within_a_full_width_character_the_ellipsis_is_shown_correctly() { + let left = "🌕🌕🌕"; + assert_eq!("🌕🌕…".to_string(), trim_file_label(left, 5)); + } +} diff --git a/src/client/ui/widgets/tui_footer.rs b/src/client/ui/widgets/tui_footer.rs new file mode 100644 index 0000000..74ccab3 --- /dev/null +++ b/src/client/ui/widgets/tui_footer.rs @@ -0,0 +1,66 @@ +use tui::buffer::Buffer; +use tui::layout::Rect; +use tui::style::{Color, Style}; +use tui::text::{Span, Spans}; +use tui::widgets::{Paragraph, Widget}; + +use crate::fs::{DirList, LinkType}; +use crate::util::format; +use crate::util::unix; +use crate::THEME_T; + +pub struct TuiFooter<'a> { + dirlist: &'a DirList, +} + +impl<'a> TuiFooter<'a> { + pub fn new(dirlist: &'a DirList) -> Self { + Self { dirlist } + } +} + +impl<'a> Widget for TuiFooter<'a> { + fn render(self, area: Rect, buf: &mut Buffer) { + use std::os::unix::fs::PermissionsExt; + match self.dirlist.index { + Some(i) if i < self.dirlist.len() => { + let entry = &self.dirlist.contents[i]; + + let mode_style = Style::default().fg(Color::Cyan); + let mode_str = unix::mode_to_string(entry.metadata.permissions_ref().mode()); + + let mtime_str = format::mtime_to_string(entry.metadata.modified()); + let size_str = format::file_size_to_string(entry.metadata.len()); + + let mut text = vec![ + Span::styled(mode_str, mode_style), + Span::raw(" "), + Span::raw(format!("{}/{}", i + 1, self.dirlist.len())), + Span::raw(" "), + Span::raw(mtime_str), + Span::raw(" UTC "), + Span::raw(size_str), + ]; + + if let LinkType::Symlink(target, valid) = entry.metadata.link_type() { + let link_style = if *valid { + Style::default() + .fg(THEME_T.link.fg) + .bg(THEME_T.link.bg) + .add_modifier(THEME_T.link.modifier) + } else { + Style::default() + .fg(THEME_T.link_invalid.fg) + .bg(THEME_T.link_invalid.bg) + .add_modifier(THEME_T.link_invalid.modifier) + }; + text.push(Span::styled(" -> ", link_style)); + text.push(Span::styled(target, link_style)); + } + + Paragraph::new(Spans::from(text)).render(area, buf); + } + _ => {} + } + } +} diff --git a/src/client/ui/widgets/tui_menu.rs b/src/client/ui/widgets/tui_menu.rs new file mode 100644 index 0000000..0cee287 --- /dev/null +++ b/src/client/ui/widgets/tui_menu.rs @@ -0,0 +1,36 @@ +use tui::buffer::Buffer; +use tui::layout::Rect; +use tui::style::{Color, Style}; +use tui::widgets::{Block, Borders, Widget}; + +pub struct TuiMenu<'a> { + options: &'a [&'a str], +} + +impl<'a> TuiMenu<'a> { + pub fn new(options: &'a [&'a str]) -> Self { + Self { options } + } + + pub fn len(&self) -> usize { + self.options.len() + } +} + +impl<'a> Widget for TuiMenu<'a> { + fn render(self, area: Rect, buf: &mut Buffer) { + let style = Style::default().fg(Color::Reset).bg(Color::Reset); + + Block::default() + .style(style) + .borders(Borders::TOP) + .render(area, buf); + + let text_iter = self.options.iter().chain(&[" "]); + let area_x = area.x + 1; + + for (y, text) in (area.y + 1..area.y + area.height).zip(text_iter) { + buf.set_string(area_x, y, text, style); + } + } +} diff --git a/src/client/ui/widgets/tui_prompt.rs b/src/client/ui/widgets/tui_prompt.rs new file mode 100644 index 0000000..bc1b210 --- /dev/null +++ b/src/client/ui/widgets/tui_prompt.rs @@ -0,0 +1,70 @@ +use termion::event::{Event, Key}; +use tui::layout::Rect; +use tui::style::{Color, Style}; +use tui::text::Span; +use tui::widgets::{Clear, Paragraph, Wrap}; + +use crate::context::AppContext; +use crate::event::AppEvent; +use crate::ui::views::TuiView; +use crate::ui::TuiBackend; +use crate::util::input; + +pub struct TuiPrompt<'a> { + prompt: &'a str, +} + +impl<'a> TuiPrompt<'a> { + pub fn new(prompt: &'a str) -> Self { + Self { prompt } + } + + pub fn get_key(&mut self, backend: &mut TuiBackend, context: &mut AppContext) -> Key { + let terminal = backend.terminal_mut(); + + context.flush_event(); + loop { + let _ = terminal.draw(|frame| { + let f_size: Rect = frame.size(); + if f_size.height == 0 { + return; + } + + { + let mut view = TuiView::new(context); + view.show_bottom_status = false; + frame.render_widget(view, f_size); + } + + let prompt_style = Style::default().fg(Color::LightYellow); + + let text = Span::styled(self.prompt, prompt_style); + + let textfield_rect = Rect { + x: 0, + y: f_size.height - 1, + width: f_size.width, + height: 1, + }; + + frame.render_widget(Clear, textfield_rect); + frame.render_widget( + Paragraph::new(text).wrap(Wrap { trim: true }), + textfield_rect, + ); + }); + + if let Ok(event) = context.poll_event() { + match event { + AppEvent::Termion(Event::Key(key)) => { + return key; + } + AppEvent::Termion(_) => { + context.flush_event(); + } + event => input::process_noninteractive(event, context), + }; + } + } + } +} diff --git a/src/client/ui/widgets/tui_text.rs b/src/client/ui/widgets/tui_text.rs new file mode 100644 index 0000000..1a1064c --- /dev/null +++ b/src/client/ui/widgets/tui_text.rs @@ -0,0 +1,110 @@ +use tui::buffer::Buffer; +use tui::layout::Rect; +use tui::style::{Color, Style}; +use tui::widgets::Widget; +use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; + +#[derive(Clone, Debug)] +pub struct LineInfo { + pub start: usize, + pub end: usize, + pub width: usize, +} + +pub struct TuiMultilineText<'a> { + _s: &'a str, + _width: usize, + _lines: Vec, + _style: Style, +} + +impl<'a> TuiMultilineText<'a> { + pub fn new(s: &'a str, area_width: usize) -> Self { + // TODO: This is a very hacky way of doing it and I would like + // to clean this up more + + let default_style = Style::default().fg(Color::Reset).bg(Color::Reset); + + let s_width = s.width(); + if s_width < area_width { + return Self { + _s: s, + _lines: vec![LineInfo { + start: 0, + end: s.len(), + width: s_width, + }], + _width: area_width, + _style: default_style, + }; + } + + let filter = |(i, c): (usize, char)| { + let w = c.width()?; + Some((i, c, w)) + }; + + let mut lines = Vec::with_capacity(s.len() / area_width); + + let mut start = 0; + let mut line_width = 0; + for (i, _, w) in s.char_indices().filter_map(filter) { + if line_width + w < area_width { + line_width += w; + continue; + } + lines.push(LineInfo { + start, + end: i, + width: line_width, + }); + line_width = w; + start = i; + } + lines.push(LineInfo { + start, + end: s.len(), + width: s[start..s.len()].width(), + }); + + Self { + _s: s, + _lines: lines, + _width: area_width, + _style: default_style, + } + } + + pub fn width(&self) -> usize { + self._width + } + + pub fn height(&self) -> usize { + if self._lines[self._lines.len() - 1].width >= self.width() { + return self.len() + 1; + } + self.len() + } + pub fn len(&self) -> usize { + self._lines.len() + } + + pub fn iter(&self) -> impl Iterator { + self._lines.iter() + } +} + +impl<'a> Widget for TuiMultilineText<'a> { + fn render(self, area: Rect, buf: &mut Buffer) { + let area_left = area.left(); + let area_top = area.top(); + for (i, line_info) in self.iter().enumerate() { + buf.set_string( + area_left, + area_top + i as u16, + &self._s[line_info.start..line_info.end], + self._style, + ); + } + } +} diff --git a/src/client/ui/widgets/tui_topbar.rs b/src/client/ui/widgets/tui_topbar.rs new file mode 100644 index 0000000..09dfeb7 --- /dev/null +++ b/src/client/ui/widgets/tui_topbar.rs @@ -0,0 +1,50 @@ +use std::path::Path; + +use tui::buffer::Buffer; +use tui::layout::Rect; +use tui::style::{Color, Modifier, Style}; +use tui::text::{Span, Spans}; +use tui::widgets::{Paragraph, Widget}; + +use crate::context::AppContext; + +pub struct TuiTopBar<'a> { + pub context: &'a AppContext, + path: &'a Path, +} + +impl<'a> TuiTopBar<'a> { + pub fn new(context: &'a AppContext, path: &'a Path) -> Self { + Self { context, path } + } +} + +impl<'a> Widget for TuiTopBar<'a> { + fn render(self, area: Rect, buf: &mut Buffer) { + let path_style = Style::default() + .fg(Color::LightBlue) + .add_modifier(Modifier::BOLD); + + let mut ellipses = None; + let mut curr_path_str = self.path.to_string_lossy().into_owned(); + + if curr_path_str.len() > area.width as usize { + if let Some(s) = self.path.file_name() { + curr_path_str = s.to_string_lossy().into_owned(); + ellipses = Some(Span::styled("…", path_style)); + } + } + + let text = match ellipses { + Some(s) => Spans::from(vec![ + s, + Span::styled(curr_path_str, path_style), + ]), + None => Spans::from(vec![ + Span::styled(curr_path_str, path_style), + ]), + }; + + Paragraph::new(text).render(area, buf); + } +} diff --git a/src/client/util/display_option.rs b/src/client/util/display_option.rs new file mode 100644 index 0000000..79bf736 --- /dev/null +++ b/src/client/util/display_option.rs @@ -0,0 +1,91 @@ +use std::fs; + +use tui::layout::Constraint; + +use crate::util::sort_option::SortOption; + +pub const fn default_column_ratio() -> (usize, usize, usize) { + (1, 3, 4) +} + +#[derive(Clone, Debug)] +pub struct DisplayOption { + pub column_ratio: (usize, usize, usize), + pub _show_borders: bool, + pub _show_hidden: bool, + pub _sort_options: SortOption, + pub default_layout: [Constraint; 3], + pub no_preview_layout: [Constraint; 3], +} + +impl DisplayOption { + pub fn show_borders(&self) -> bool { + self._show_borders + } + + pub fn show_hidden(&self) -> bool { + self._show_hidden + } + + pub fn set_show_hidden(&mut self, show_hidden: bool) { + self._show_hidden = show_hidden; + } + + pub fn sort_options_ref(&self) -> &SortOption { + &self._sort_options + } + + pub fn sort_options_mut(&mut self) -> &mut SortOption { + &mut self._sort_options + } + + pub fn filter_func(&self) -> fn(&Result) -> bool { + if self.show_hidden() { + no_filter + } else { + filter_hidden + } + } +} + +impl std::default::Default for DisplayOption { + fn default() -> Self { + let column_ratio = default_column_ratio(); + + let total = (column_ratio.0 + column_ratio.1 + column_ratio.2) as u32; + let default_layout = [ + Constraint::Ratio(column_ratio.0 as u32, total), + Constraint::Ratio(column_ratio.1 as u32, total), + Constraint::Ratio(column_ratio.2 as u32, total), + ]; + let no_preview_layout = [ + Constraint::Ratio(column_ratio.0 as u32, total), + Constraint::Ratio(column_ratio.1 as u32 + column_ratio.2 as u32, total), + Constraint::Ratio(0, total), + ]; + + Self { + column_ratio, + _show_borders: true, + _show_hidden: false, + _sort_options: SortOption::default(), + default_layout, + no_preview_layout, + } + } +} + +const fn no_filter(_: &Result) -> bool { + true +} + +fn filter_hidden(result: &Result) -> bool { + match result { + Err(_) => true, + Ok(entry) => { + let file_name = entry.file_name(); + let lossy_string = file_name.as_os_str().to_string_lossy(); + !lossy_string.starts_with('.') + } + } +} diff --git a/src/client/util/format.rs b/src/client/util/format.rs new file mode 100644 index 0000000..cd73605 --- /dev/null +++ b/src/client/util/format.rs @@ -0,0 +1,28 @@ +use std::time; + +pub fn file_size_to_string(file_size: u64) -> String { + const FILE_UNITS: [&str; 6] = ["B", "K", "M", "G", "T", "E"]; + const CONV_RATE: f64 = 1024.0; + let mut file_size: f64 = file_size as f64; + + let mut index = 0; + while file_size > CONV_RATE { + file_size /= CONV_RATE; + index += 1; + } + + if file_size >= 100.0 { + format!("{:>4.0} {}", file_size, FILE_UNITS[index]) + } else if file_size >= 10.0 { + format!("{:>4.1} {}", file_size, FILE_UNITS[index]) + } else { + format!("{:>4.2} {}", file_size, FILE_UNITS[index]) + } +} + +pub fn mtime_to_string(mtime: time::SystemTime) -> String { + const MTIME_FORMATTING: &str = "%Y-%m-%d %H:%M"; + + let datetime: chrono::DateTime = mtime.into(); + datetime.format(MTIME_FORMATTING).to_string() +} diff --git a/src/client/util/input.rs b/src/client/util/input.rs new file mode 100644 index 0000000..bd03b95 --- /dev/null +++ b/src/client/util/input.rs @@ -0,0 +1,67 @@ +use std::io; +use std::path; + +use signal_hook::consts::signal; +use termion::event::{Event, Key, MouseButton, MouseEvent}; +use tui::layout::{Constraint, Direction, Layout}; + +use crate::commands::cursor_move; +use crate::config::AppKeyMapping; +use crate::context::AppContext; +use crate::event::AppEvent; +use crate::fs::DirList; +use crate::history::DirectoryHistory; +use crate::key_command::{AppExecute, CommandKeybind, Command}; +use crate::ui; +use crate::ui::views::TuiCommandMenu; +use crate::util::format; + +pub fn get_input_while_composite<'a>( + backend: &mut ui::TuiBackend, + context: &mut AppContext, + keymap: &'a AppKeyMapping, +) -> Option<&'a Command> { + let mut keymap = keymap; + + context.flush_event(); + + loop { + backend.render(TuiCommandMenu::new(context, keymap)); + + if let Ok(event) = context.poll_event() { + match event { + AppEvent::Termion(event) => { + match event { + Event::Key(Key::Esc) => return None, + event => match keymap.as_ref().get(&event) { + Some(CommandKeybind::SimpleKeybind(s)) => { + return Some(s); + } + Some(CommandKeybind::CompositeKeybind(m)) => { + keymap = m; + } + None => return None, + }, + } + context.flush_event(); + } + event => process_noninteractive(event, context), + } + } + } +} + +pub fn process_noninteractive(event: AppEvent, context: &mut AppContext) { + match event { + AppEvent::PreviewDir(Ok(dirlist)) => process_dir_preview(context, dirlist), + AppEvent::Signal(signal::SIGWINCH) => {} + _ => {} + } +} + +pub fn process_dir_preview(context: &mut AppContext, dirlist: DirList) { + let history = context.history_mut(); + + let dir_path = dirlist.file_path().to_path_buf(); + history.insert(dir_path, dirlist); +} diff --git a/src/client/util/keyparse.rs b/src/client/util/keyparse.rs new file mode 100644 index 0000000..fb39628 --- /dev/null +++ b/src/client/util/keyparse.rs @@ -0,0 +1,73 @@ +use termion::event::{Event, Key, MouseButton, MouseEvent}; + +pub fn str_to_event(s: &str) -> Option { + if let Some(k) = str_to_key(s) { + Some(Event::Key(k)) + } else if let Some(m) = str_to_mouse(s) { + Some(Event::Mouse(m)) + } else { + None + } +} + +pub fn str_to_key(s: &str) -> Option { + if s.is_empty() { + return None; + } + + let key = match s { + "backspace" => Some(Key::Backspace), + "backtab" => Some(Key::BackTab), + "arrow_left" => Some(Key::Left), + "arrow_right" => Some(Key::Right), + "arrow_up" => Some(Key::Up), + "arrow_down" => Some(Key::Down), + "home" => Some(Key::Home), + "end" => Some(Key::End), + "page_up" => Some(Key::PageUp), + "page_down" => Some(Key::PageDown), + "delete" => Some(Key::Delete), + "insert" => Some(Key::Insert), + "escape" => Some(Key::Esc), + "f1" => Some(Key::F(1)), + "f2" => Some(Key::F(2)), + "f3" => Some(Key::F(3)), + "f4" => Some(Key::F(4)), + "f5" => Some(Key::F(5)), + "f6" => Some(Key::F(6)), + "f7" => Some(Key::F(7)), + "f8" => Some(Key::F(8)), + "f9" => Some(Key::F(9)), + "f10" => Some(Key::F(10)), + "f11" => Some(Key::F(11)), + "f12" => Some(Key::F(12)), + _ => None, + }; + + if key.is_some() { + return key; + } + + if s.starts_with("ctrl+") { + let ch = s.chars().nth("ctrl+".len()); + let key = ch.map(Key::Ctrl); + return key; + } else if s.starts_with("alt+") { + let ch = s.chars().nth("alt+".len()); + let key = ch.map(Key::Alt); + return key; + } else if s.len() == 1 { + let ch = s.chars().next(); + let key = ch.map(Key::Char); + return key; + } + None +} + +pub fn str_to_mouse(s: &str) -> Option { + match s { + "scroll_up" => Some(MouseEvent::Press(MouseButton::WheelUp, 0, 0)), + "scroll_down" => Some(MouseEvent::Press(MouseButton::WheelDown, 0, 0)), + _ => None, + } +} diff --git a/src/client/util/mod.rs b/src/client/util/mod.rs new file mode 100644 index 0000000..68bcc16 --- /dev/null +++ b/src/client/util/mod.rs @@ -0,0 +1,12 @@ +pub mod display_option; +pub mod format; +pub mod input; +pub mod keyparse; +pub mod search; +pub mod select; +pub mod sort_option; +pub mod sort_type; +pub mod string; +pub mod style; +pub mod to_string; +pub mod unix; diff --git a/src/client/util/search.rs b/src/client/util/search.rs new file mode 100644 index 0000000..69d038c --- /dev/null +++ b/src/client/util/search.rs @@ -0,0 +1,7 @@ +use globset::GlobMatcher; + +#[derive(Clone, Debug)] +pub enum SearchPattern { + Glob(GlobMatcher), + String(String), +} diff --git a/src/client/util/select.rs b/src/client/util/select.rs new file mode 100644 index 0000000..8be7516 --- /dev/null +++ b/src/client/util/select.rs @@ -0,0 +1,26 @@ +#[derive(Clone, Copy, Debug)] +pub struct SelectOption { + pub toggle: bool, + pub all: bool, + pub reverse: bool, +} + +impl std::default::Default for SelectOption { + fn default() -> Self { + Self { + toggle: true, + all: false, + reverse: false, + } + } +} + +impl std::fmt::Display for SelectOption { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!( + f, + "--toggle={} --all={} --deselect={}", + self.toggle, self.all, self.reverse + ) + } +} diff --git a/src/client/util/sort_option.rs b/src/client/util/sort_option.rs new file mode 100644 index 0000000..3bdd1f2 --- /dev/null +++ b/src/client/util/sort_option.rs @@ -0,0 +1,58 @@ +use std::cmp; +use std::collections::VecDeque; +use std::fs; +use std::time; + +use serde_derive::Deserialize; + +use crate::fs::DirEntry; +use crate::util::sort_type::{SortType, SortTypes}; + +#[derive(Clone, Debug)] +pub struct SortOption { + pub directories_first: bool, + pub case_sensitive: bool, + pub reverse: bool, + pub sort_methods: SortTypes, +} + +impl SortOption { + pub fn set_sort_method(&mut self, method: SortType) { + self.sort_methods.reorganize(method); + } + + pub fn compare(&self, f1: &DirEntry, f2: &DirEntry) -> cmp::Ordering { + if self.directories_first { + let f1_isdir = f1.file_path().is_dir(); + let f2_isdir = f2.file_path().is_dir(); + + if f1_isdir && !f2_isdir { + return cmp::Ordering::Less; + } else if !f1_isdir && f2_isdir { + return cmp::Ordering::Greater; + } + } + + // let mut res = self.sort_method.cmp(f1, f2, &self); + let mut res = self.sort_methods.cmp(f1, f2, &self); + if self.reverse { + res = match res { + cmp::Ordering::Less => cmp::Ordering::Greater, + cmp::Ordering::Greater => cmp::Ordering::Less, + s => s, + }; + }; + res + } +} + +impl std::default::Default for SortOption { + fn default() -> Self { + SortOption { + directories_first: true, + case_sensitive: false, + reverse: false, + sort_methods: SortTypes::default(), + } + } +} diff --git a/src/client/util/sort_type.rs b/src/client/util/sort_type.rs new file mode 100644 index 0000000..9e09402 --- /dev/null +++ b/src/client/util/sort_type.rs @@ -0,0 +1,160 @@ +use std::cmp; +use std::collections::VecDeque; +use std::fs; +use std::time; + +use serde_derive::Deserialize; + +use crate::fs::DirEntry; +use crate::util::sort_option::SortOption; + +#[derive(Clone, Copy, Debug, Deserialize, PartialEq)] +pub enum SortType { + Lexical, + Mtime, + Natural, + Size, + Ext, +} + +impl SortType { + pub fn parse(s: &str) -> Option { + match s { + "lexical" => Some(SortType::Lexical), + "mtime" => Some(SortType::Mtime), + "natural" => Some(SortType::Natural), + "size" => Some(SortType::Size), + "ext" => Some(SortType::Ext), + _ => None, + } + } + pub const fn as_str(&self) -> &str { + match *self { + SortType::Lexical => "lexical", + SortType::Mtime => "mtime", + SortType::Natural => "natural", + SortType::Size => "size", + SortType::Ext => "ext", + } + } + pub fn cmp( + &self, + f1: &DirEntry, + f2: &DirEntry, + sort_option: &SortOption, + ) -> cmp::Ordering { + match &self { + SortType::Natural => natural_sort(f1, f2, sort_option), + SortType::Lexical => lexical_sort(f1, f2, sort_option), + SortType::Size => size_sort(f1, f2), + SortType::Mtime => mtime_sort(f1, f2), + SortType::Ext => ext_sort(f1, f2), + } + } +} + +impl std::fmt::Display for SortType { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", self.as_str()) + } +} + +#[derive(Clone, Debug)] +pub struct SortTypes { + pub list: VecDeque, +} + +impl SortTypes { + pub fn reorganize(&mut self, st: SortType) { + self.list.push_front(st); + self.list.pop_back(); + } + + pub fn cmp( + &self, + f1: &DirEntry, + f2: &DirEntry, + sort_option: &SortOption, + ) -> cmp::Ordering { + for st in &self.list { + let res = st.cmp(f1, f2, sort_option); + if res != cmp::Ordering::Equal { + return res; + } + } + cmp::Ordering::Equal + } +} + +impl std::default::Default for SortTypes { + fn default() -> Self { + let list: VecDeque = vec![ + SortType::Natural, + SortType::Lexical, + SortType::Size, + SortType::Ext, + SortType::Mtime, + ] + .into_iter() + .collect(); + + Self { list } + } +} + +fn mtime_sort(file1: &DirEntry, file2: &DirEntry) -> cmp::Ordering { + fn compare( + file1: &DirEntry, + file2: &DirEntry, + ) -> Result { + let f1_meta: fs::Metadata = std::fs::metadata(file1.file_path())?; + let f2_meta: fs::Metadata = std::fs::metadata(file2.file_path())?; + + let f1_mtime: time::SystemTime = f1_meta.modified()?; + let f2_mtime: time::SystemTime = f2_meta.modified()?; + Ok(f1_mtime.cmp(&f2_mtime)) + } + compare(file1, file2).unwrap_or(cmp::Ordering::Equal) +} + +fn size_sort(file1: &DirEntry, file2: &DirEntry) -> cmp::Ordering { + file1.metadata.len().cmp(&file2.metadata.len()) +} + +fn ext_sort(file1: &DirEntry, file2: &DirEntry) -> cmp::Ordering { + let f1_ext = file1.get_ext(); + let f2_ext = file2.get_ext(); + alphanumeric_sort::compare_str(&f1_ext, &f2_ext) +} + +fn lexical_sort( + f1: &DirEntry, + f2: &DirEntry, + sort_option: &SortOption, +) -> cmp::Ordering { + let f1_name = f1.file_name(); + let f2_name = f2.file_name(); + if sort_option.case_sensitive { + f1_name.cmp(f2_name) + } else { + let f1_name = f1_name.to_lowercase(); + let f2_name = f2_name.to_lowercase(); + f1_name.cmp(&f2_name) + } +} + +fn natural_sort( + f1: &DirEntry, + f2: &DirEntry, + sort_option: &SortOption, +) -> cmp::Ordering { + let f1_name = f1.file_name(); + let f2_name = f2.file_name(); + if sort_option.case_sensitive { + alphanumeric_sort::compare_str(&f1_name, &f2_name) + } else { + let f1_name = f1_name.to_lowercase(); + let f2_name = f2_name.to_lowercase(); + alphanumeric_sort::compare_str(&f1_name, &f2_name) + } +} diff --git a/src/client/util/string.rs b/src/client/util/string.rs new file mode 100644 index 0000000..e2e0002 --- /dev/null +++ b/src/client/util/string.rs @@ -0,0 +1,67 @@ +use unicode_segmentation::UnicodeSegmentation; +use unicode_width::UnicodeWidthStr; + +///Truncates a string to width, less or equal to the specified one. +/// +///In case the point of truncation falls into a full-width character, +///the returned string will be shorter than the given `width`. +///Otherwise, it will be equal. +pub trait UnicodeTruncate { + fn trunc(&self, width: usize) -> String; +} + +impl UnicodeTruncate for str { + #[inline] + fn trunc(&self, width: usize) -> String { + if self.width() <= width { + String::from(self) + } else { + let mut length: usize = 0; + let mut result = String::new(); + for grapheme in self.graphemes(true) { + let grapheme_length = grapheme.width(); + length += grapheme_length; + if length > width { + break; + }; + result.push_str(grapheme); + } + result + } + } +} + +#[cfg(test)] +mod tests_trunc { + use super::UnicodeTruncate; + + #[test] + fn truncate_correct_despite_several_multibyte_chars() { + assert_eq!(String::from("r͂o͒͜w̾").trunc(2), String::from("r͂o͒͜")); + } + + #[test] + fn truncate_at_end_returns_complete_string() { + assert_eq!(String::from("r͂o͒͜w̾").trunc(3), String::from("r͂o͒͜w̾")); + } + + #[test] + fn truncate_behind_end_returns_complete_string() { + assert_eq!(String::from("r͂o͒͜w̾").trunc(4), String::from("r͂o͒͜w̾")); + } + + #[test] + fn truncate_at_zero_returns_empty_string() { + assert_eq!(String::from("r͂o͒͜w̾").trunc(0), String::from("")); + } + + #[test] + fn truncate_correct_despite_fullwidth_character() { + assert_eq!(String::from("a🌕bc").trunc(4), String::from("a🌕b")); + } + + #[test] + fn truncate_within_fullwidth_character_truncates_before_the_character() { + assert_eq!(String::from("a🌕").trunc(2), String::from("a")); + } +} diff --git a/src/client/util/style.rs b/src/client/util/style.rs new file mode 100644 index 0000000..e24d1dc --- /dev/null +++ b/src/client/util/style.rs @@ -0,0 +1,51 @@ +use tui::style::Style; + +use crate::fs::{FileType, DirEntry, LinkType}; +use crate::util::unix; + +use crate::THEME_T; + +pub fn entry_style(entry: &DirEntry) -> Style { + let metadata = &entry.metadata; + let filetype = &metadata.file_type(); + let linktype = &metadata.link_type(); + + match linktype { + LinkType::Symlink(_, true) => Style::default() + .fg(THEME_T.link.fg) + .bg(THEME_T.link.bg) + .add_modifier(THEME_T.link.modifier), + LinkType::Symlink(_, false) => Style::default() + .fg(THEME_T.link_invalid.fg) + .bg(THEME_T.link_invalid.bg) + .add_modifier(THEME_T.link_invalid.modifier), + LinkType::Normal => match filetype { + FileType::Directory => Style::default() + .fg(THEME_T.directory.fg) + .bg(THEME_T.directory.bg) + .add_modifier(THEME_T.directory.modifier), + FileType::File => file_style(entry), + }, + } +} + +fn file_style(entry: &DirEntry) -> Style { + let metadata = &entry.metadata; + if unix::is_executable(metadata.mode) { + Style::default() + .fg(THEME_T.executable.fg) + .bg(THEME_T.executable.bg) + .add_modifier(THEME_T.executable.modifier) + } else { + match entry.file_path().extension() { + None => Style::default(), + Some(os_str) => match os_str.to_str() { + None => Style::default(), + Some(s) => match THEME_T.ext.get(s) { + None => Style::default(), + Some(t) => Style::default().fg(t.fg).bg(t.bg).add_modifier(t.modifier), + }, + }, + } + } +} diff --git a/src/client/util/to_string.rs b/src/client/util/to_string.rs new file mode 100644 index 0000000..cbfffb0 --- /dev/null +++ b/src/client/util/to_string.rs @@ -0,0 +1,46 @@ +use termion::event::{Event, Key, MouseEvent}; + +pub trait ToString { + fn to_string(&self) -> String; +} + +impl ToString for Key { + fn to_string(&self) -> String { + match *self { + Key::Char(c) => format!("{}", c), + Key::Ctrl(c) => format!("ctrl+{}", c), + Key::Left => "arrow_left".to_string(), + Key::Right => "arrow_right".to_string(), + Key::Up => "arrow_up".to_string(), + Key::Down => "arrow_down".to_string(), + Key::Backspace => "backspace".to_string(), + Key::Home => "home".to_string(), + Key::End => "end".to_string(), + Key::PageUp => "page_up".to_string(), + Key::PageDown => "page_down".to_string(), + Key::BackTab => "backtab".to_string(), + Key::Insert => "insert".to_string(), + Key::Delete => "delete".to_string(), + Key::Esc => "escape".to_string(), + Key::F(i) => format!("f{}", i), + k => format!("{:?}", k), + } + } +} + +impl ToString for MouseEvent { + fn to_string(&self) -> String { + let k = *self; + format!("{:?}", k) + } +} + +impl ToString for Event { + fn to_string(&self) -> String { + match self { + Event::Key(key) => key.to_string(), + Event::Mouse(mouse) => mouse.to_string(), + Event::Unsupported(v) => format!("{:?}", v), + } + } +} diff --git a/src/client/util/unix.rs b/src/client/util/unix.rs new file mode 100644 index 0000000..20c13b3 --- /dev/null +++ b/src/client/util/unix.rs @@ -0,0 +1,51 @@ +pub fn is_executable(mode: u32) -> bool { + const LIBC_PERMISSION_VALS: [libc::mode_t; 3] = [libc::S_IXUSR, libc::S_IXGRP, libc::S_IXOTH]; + + LIBC_PERMISSION_VALS + .iter() + .any(|val| mode & (*val as u32) != 0) +} + +pub fn mode_to_string(mode: u32) -> String { + const LIBC_FILE_VALS: [(libc::mode_t, char); 7] = [ + (libc::S_IFREG >> 9, '-'), + (libc::S_IFDIR >> 9, 'd'), + (libc::S_IFLNK >> 9, 'l'), + (libc::S_IFSOCK >> 9, 's'), + (libc::S_IFBLK >> 9, 'b'), + (libc::S_IFCHR >> 9, 'c'), + (libc::S_IFIFO >> 9, 'f'), + ]; + + const LIBC_PERMISSION_VALS: [(libc::mode_t, char); 9] = [ + (libc::S_IRUSR, 'r'), + (libc::S_IWUSR, 'w'), + (libc::S_IXUSR, 'x'), + (libc::S_IRGRP, 'r'), + (libc::S_IWGRP, 'w'), + (libc::S_IXGRP, 'x'), + (libc::S_IROTH, 'r'), + (libc::S_IWOTH, 'w'), + (libc::S_IXOTH, 'x'), + ]; + + let mut mode_str: String = String::with_capacity(10); + let mode_shifted = mode >> 9; + + for (val, ch) in LIBC_FILE_VALS.iter() { + if mode_shifted == *val { + mode_str.push(*ch); + break; + } + } + + for (val, ch) in LIBC_PERMISSION_VALS.iter() { + let val: u32 = (*val) as u32; + if mode & val != 0 { + mode_str.push(*ch); + } else { + mode_str.push('-'); + } + } + mode_str +} diff --git a/src/server/audio/mod.rs b/src/server/audio/mod.rs new file mode 100644 index 0000000..ff11891 --- /dev/null +++ b/src/server/audio/mod.rs @@ -0,0 +1,7 @@ +mod player; +mod playlist; +mod song; + +pub use player::*; +pub use playlist::*; +pub use song::*; diff --git a/src/server/audio/pipewire.rs b/src/server/audio/pipewire.rs new file mode 100644 index 0000000..b61d1c0 --- /dev/null +++ b/src/server/audio/pipewire.rs @@ -0,0 +1,36 @@ +use pipewire as pw; + +pub struct PipewireData { + mainloop: pw::MainLoop, + context: pw::Context, + core: pw::Core, + registry: pw::registry::Registry, + listener: pw::registry::Listener, + // stream: pw::stream::Stream, + // properties: pw::Properties, +} + +impl PipewireData { + pub fn new() -> Result> { + let mainloop = pw::MainLoop::new()?; + let context = pw::Context::new(&mainloop)?; + let core = context.connect(None)?; + let registry = core.get_registry()?; + + let listener = registry + .add_listener_local() + .global(|global| println!("New global: {:?}", global)) + .register(); + + mainloop.run(); + + Ok(Self { + mainloop, + context, + core, + registry, + listener, + }) + } +} + diff --git a/src/server/audio/player.rs b/src/server/audio/player.rs new file mode 100644 index 0000000..3bdc9f2 --- /dev/null +++ b/src/server/audio/player.rs @@ -0,0 +1,71 @@ +use std::fs::File; +use std::io::BufReader; +use std::path::{Path, PathBuf}; +use std::thread; + +use rodio::{Decoder, OutputStream, OutputStreamHandle}; +use rodio::source::Source; + +use dizi_commands::error::DiziResult; +use crate::audio::Song; + +#[derive(Clone, Debug)] +pub enum PlayerStatus { + Playing(Song), + Paused(Song), + Stopped, +} + +#[derive(Clone, Debug)] +pub struct Player { +// pipewire: PipewireData, + status: PlayerStatus, + shuffle: bool, + repeat: bool, + next: bool, +} + +impl Player { + pub fn new() -> Self { + Self { + status: PlayerStatus::Stopped, + shuffle: false, + repeat: true, + next: true, + } + } + + pub fn play(&mut self, path: &Path) -> DiziResult>> { + let song = Song::new(path)?; + + let path_clone = path.to_path_buf(); + + let handle = thread::spawn(move || { + let (_stream, stream_handle) = OutputStream::try_default()?; + let file = File::open(&path_clone)?; + let buffer = BufReader::new(file); + + let sink = stream_handle.play_once(buffer)?; + sink.sleep_until_end(); + Ok(()) + }); + + self.status = PlayerStatus::Playing(song); + Ok(handle) + } +} + +/* + pub fn new() -> Result> { + let pipewire = PipewireData::new()?; + + Ok(Self { + current_song: None, + pipewire, + }) + } + + pub fn play() -> Result<(), Box> { + Ok(()) + } +*/ diff --git a/src/server/audio/playlist.rs b/src/server/audio/playlist.rs new file mode 100644 index 0000000..991df0b --- /dev/null +++ b/src/server/audio/playlist.rs @@ -0,0 +1,48 @@ +use std::collections::HashSet; +use std::path::PathBuf; + +use crate::audio::Song; + +#[derive(Clone, Debug)] +pub struct Playlist { + _set: HashSet, + _list: Vec, +} + +impl Playlist { + pub fn new() -> Self { + Self::default() + } + + pub fn playlist(&self) -> &[Song] { + self._list.as_slice() + } + + pub fn add_song(&mut self, s: Song) { + self._set.insert(s.file_path().to_path_buf()); + self._list.push(s); + } + + pub fn remove_song(&mut self, index: usize) -> Song { + let song = self._list.remove(index); + self._set.remove(&song.file_path().to_path_buf()); + song + } + + pub fn len(&self) -> usize { + self._list.len() + } + + pub fn contains(&self, s: &PathBuf) -> bool { + self._set.contains(s) + } +} + +impl std::default::Default for Playlist { + fn default() -> Self { + Self { + _set: HashSet::new(), + _list: Vec::new(), + } + } +} diff --git a/src/server/audio/song.rs b/src/server/audio/song.rs new file mode 100644 index 0000000..8b7f005 --- /dev/null +++ b/src/server/audio/song.rs @@ -0,0 +1,29 @@ +use std::io; +use std::path::{Path, PathBuf}; + +use serde_derive::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Song { + #[serde(rename = "path")] + _path: PathBuf, + #[serde(rename = "metadata", default)] + _metadata: String, +} + +impl Song { + pub fn new(p: &Path) -> io::Result { + Ok(Self { + _path: p.to_path_buf(), + _metadata: "".to_string(), + }) + } + + pub fn file_path(&self) -> &Path { + self._path.as_path() + } + + pub fn metadata(&self) -> &String { + &self._metadata + } +} diff --git a/src/server/command.rs b/src/server/command.rs new file mode 100644 index 0000000..de15980 --- /dev/null +++ b/src/server/command.rs @@ -0,0 +1,22 @@ +use std::collections::HashMap; +use std::path::PathBuf; + +use dizi_commands::api_command::ApiCommand; +use dizi_commands::error::DiziResult; + +use crate::commands::*; +use crate::context::AppContext; + +pub fn run_command(context: &mut AppContext, command: ApiCommand, json_map: &HashMap) -> DiziResult<()> { + match command { + ApiCommand::Quit => {}, + ApiCommand::PlayerPlay => match json_map.get("path").map(|k| PathBuf::from(k)) { + Some(p) => { + player_play(context, &p)?; + } + None => {} + }, + _ => {}, + } + Ok(()) +} diff --git a/src/server/commands/mod.rs b/src/server/commands/mod.rs new file mode 100644 index 0000000..ce51016 --- /dev/null +++ b/src/server/commands/mod.rs @@ -0,0 +1,3 @@ +pub mod player; + +pub use self::player::*; diff --git a/src/server/commands/player.rs b/src/server/commands/player.rs new file mode 100644 index 0000000..360b079 --- /dev/null +++ b/src/server/commands/player.rs @@ -0,0 +1,15 @@ +use std::path::Path; + +use dizi_commands::error::DiziResult; + +use crate::context::AppContext; + +pub fn player_play(context: &mut AppContext, path: &Path) -> DiziResult<()> { + let res = context.player_context_mut().player_mut().play(path); + eprintln!("Playing {:?} res: {:?}", path, res); + match res { + Ok(handle) => handle.join(), + Err(e) => return Err(e), + }; + Ok(()) +} diff --git a/src/server/commands/playlist.rs b/src/server/commands/playlist.rs new file mode 100644 index 0000000..ba961bd --- /dev/null +++ b/src/server/commands/playlist.rs @@ -0,0 +1,42 @@ +use std::io; +use std::path::PathBuf; + +use serde_derive::{Deserialize, Serialize}; + +use crate::audio::Song; +use crate::error::AppError; +use crate::server::PLAYLIST; + +pub struct PlaylistAddSongResp { + pub song: Song, + pub index: usize, +} +#[post("/api/playlist/add")] +pub async fn add_to_playlist(data: web::Json) -> impl Responder { + { + let playlist = PLAYLIST.lock().unwrap(); + if playlist.contains(&data.path) { + let err = io::Error::new( + io::ErrorKind::AlreadyExists, + "This file is already in the playlist".to_string(), + ); + let err = AppError::from(err); + return HttpResponse::BadRequest().json(err); + } + } + + match Song::new(data.path.as_path()) { + Err(e) => HttpResponse::BadRequest().json(AppError::from(e)), + Ok(s) => { + let song = s.clone(); + let index = { + let mut playlist = PLAYLIST.lock().unwrap(); + (*playlist).add_song(s); + (*playlist).len() - 1 + }; + + let res = PlaylistAddSongResp { song, index }; + HttpResponse::Ok().json(res) + } + } +} diff --git a/src/server/config/default/default.rs b/src/server/config/default/default.rs new file mode 100644 index 0000000..5380887 --- /dev/null +++ b/src/server/config/default/default.rs @@ -0,0 +1,84 @@ +use std::path; + +use serde_derive::Deserialize; + +use crate::config::{parse_to_config_file, ConfigStructure, Flattenable}; + +#[derive(Clone, Debug, Deserialize)] +pub struct RawServerConfig { + #[serde(default)] + pub socket: path::PathBuf, +} + +impl Flattenable for RawServerConfig { + fn flatten(self) -> ServerConfig { + ServerConfig { + socket: path::PathBuf::from(self.socket), + } + } +} + +#[derive(Clone, Debug, Deserialize)] +pub struct ServerConfig { + #[serde(default)] + pub socket: path::PathBuf, +} + +impl std::default::Default for ServerConfig { + fn default() -> Self { + Self { + socket: path::PathBuf::from("."), + } + } +} + +impl ConfigStructure for ServerConfig { + fn get_config(file_name: &str) -> Self { + parse_to_config_file::(file_name).unwrap_or_else(Self::default) + } +} + +#[derive(Clone, Debug, Deserialize)] +pub struct RawAppConfig { + #[serde(default, rename = "server")] + _server: ServerConfig, +} + +impl Flattenable for RawAppConfig { + fn flatten(self) -> AppConfig { + AppConfig { + _server: self._server, + } + } +} + +#[derive(Debug, Clone)] +pub struct AppConfig { + _server: ServerConfig, +} + +impl AppConfig { + pub fn new(server: ServerConfig) -> Self { + Self { + _server: server, + } + } + + pub fn server_ref(&self) -> &ServerConfig { + &self._server + } +} + +impl ConfigStructure for AppConfig { + fn get_config(file_name: &str) -> Self { + parse_to_config_file::(file_name).unwrap_or_else(Self::default) + } +} + +impl std::default::Default for AppConfig { + fn default() -> Self { + Self { + _server: ServerConfig::default(), + } + } +} diff --git a/src/server/config/default/mod.rs b/src/server/config/default/mod.rs new file mode 100644 index 0000000..c83c745 --- /dev/null +++ b/src/server/config/default/mod.rs @@ -0,0 +1,3 @@ +pub mod default; + +pub use self::default::AppConfig; diff --git a/src/server/config/mod.rs b/src/server/config/mod.rs new file mode 100644 index 0000000..5bd7f81 --- /dev/null +++ b/src/server/config/mod.rs @@ -0,0 +1,56 @@ +pub mod default; + +pub use self::default::AppConfig; + +use std::fs; +use std::path::{Path, PathBuf}; + +use serde::de::DeserializeOwned; + +use crate::CONFIG_HIERARCHY; + +pub trait ConfigStructure { + fn get_config(file_name: &str) -> Self; +} + +// implemented by config file implementations to turn a RawConfig into a Config +trait Flattenable { + fn flatten(self) -> T; +} + +// searches a list of folders for a given file in order of preference +pub fn search_directories

(filename: &str, directories: &[P]) -> Option +where + P: AsRef, +{ + for path in directories.iter() { + let filepath = path.as_ref().join(filename); + if filepath.exists() { + return Some(filepath); + } + } + None +} + +// parses a config file into its appropriate format +fn parse_to_config_file(filename: &str) -> Option +where + T: DeserializeOwned + Flattenable, +{ + let file_path = search_directories(filename, &CONFIG_HIERARCHY)?; + let file_contents = match fs::read_to_string(&file_path) { + Ok(content) => content, + Err(e) => { + eprintln!("Error reading {} file: {}", filename, e); + return None; + } + }; + let config = match toml::from_str::(&file_contents) { + Ok(config) => config, + Err(e) => { + eprintln!("Error parsing {} file: {}", filename, e); + return None; + } + }; + Some(config.flatten()) +} diff --git a/src/server/context/app_context.rs b/src/server/context/app_context.rs new file mode 100644 index 0000000..7d87c6a --- /dev/null +++ b/src/server/context/app_context.rs @@ -0,0 +1,31 @@ +use dizi_commands::error::DiziResult; + +use crate::config; + +use super::PlayerContext; + +#[derive(Clone)] +pub struct AppContext { + config: config::AppConfig, + player_context: PlayerContext, +} + +impl AppContext { + pub fn new(config: config::AppConfig) -> DiziResult { + Ok(Self { + config, + player_context: PlayerContext::new()?, + }) + } + + pub fn config_ref(&self) -> &config::AppConfig { + &self.config + } + + pub fn player_context_ref(&self) -> &PlayerContext { + &self.player_context + } + pub fn player_context_mut(&mut self) -> &mut PlayerContext { + &mut self.player_context + } +} diff --git a/src/server/context/mod.rs b/src/server/context/mod.rs new file mode 100644 index 0000000..f0e4dc4 --- /dev/null +++ b/src/server/context/mod.rs @@ -0,0 +1,5 @@ +mod app_context; +mod player_context; + +pub use self::app_context::*; +pub use self::player_context::*; diff --git a/src/server/context/player_context.rs b/src/server/context/player_context.rs new file mode 100644 index 0000000..503d631 --- /dev/null +++ b/src/server/context/player_context.rs @@ -0,0 +1,26 @@ +use rodio::{Decoder, OutputStream, OutputStreamHandle}; +use rodio::source; + +use dizi_commands::error::DiziResult; +use crate::audio::Player; + +#[derive(Clone)] +pub struct PlayerContext { + player: Player, +} + +impl PlayerContext { + pub fn new() -> DiziResult { + let player = Player::new(); + Ok(Self { + player, + }) + } + + pub fn player_ref(&self) -> &Player { + &self.player + } + pub fn player_mut(&mut self) -> &mut Player { + &mut self.player + } +} diff --git a/src/server/error/error_type.rs b/src/server/error/error_type.rs new file mode 100644 index 0000000..2ddfcb7 --- /dev/null +++ b/src/server/error/error_type.rs @@ -0,0 +1,18 @@ +use std::io; + +use serde_derive::Serialize; + +#[derive(Clone, Debug, Serialize)] +pub struct AppError { + pub kind: String, + pub message: String, +} + +impl std::convert::From for AppError { + fn from(err: io::Error) -> Self { + AppError { + kind: "".to_string(), + message: err.to_string(), + } + } +} diff --git a/src/server/error/mod.rs b/src/server/error/mod.rs new file mode 100644 index 0000000..a7075fc --- /dev/null +++ b/src/server/error/mod.rs @@ -0,0 +1,3 @@ +mod error_type; + +pub use error_type::*; diff --git a/src/server/handlers/mod.rs b/src/server/handlers/mod.rs new file mode 100644 index 0000000..60043a4 --- /dev/null +++ b/src/server/handlers/mod.rs @@ -0,0 +1,5 @@ +pub mod player; +pub mod playlist; +pub mod websocket; + +pub use playlist::*; diff --git a/src/server/handlers/player.rs b/src/server/handlers/player.rs new file mode 100644 index 0000000..a0f7dbd --- /dev/null +++ b/src/server/handlers/player.rs @@ -0,0 +1,11 @@ +use actix_web::{get, post, HttpResponse, Responder}; + +#[get("/api/player")] +async fn get_player() -> impl Responder { + HttpResponse::Ok() +} + +#[post("/api/player/play")] +async fn post_player() -> impl Responder { + HttpResponse::Ok() +} diff --git a/src/server/handlers/playlist/add_song.rs b/src/server/handlers/playlist/add_song.rs new file mode 100644 index 0000000..94d9007 --- /dev/null +++ b/src/server/handlers/playlist/add_song.rs @@ -0,0 +1,49 @@ +use std::io; +use std::path::PathBuf; + +use actix_web::{post, web, HttpResponse, Responder}; +use serde_derive::{Deserialize, Serialize}; + +use crate::audio::Song; +use crate::error::AppError; +use crate::server::PLAYLIST; + +#[derive(Clone, Debug, Deserialize)] +pub struct PlaylistAddSongReq { + pub path: PathBuf, +} + +#[derive(Clone, Debug, Serialize)] +pub struct PlaylistAddSongResp { + pub song: Song, + pub index: usize, +} +#[post("/api/playlist/add")] +pub async fn add_to_playlist(data: web::Json) -> impl Responder { + { + let playlist = PLAYLIST.lock().unwrap(); + if playlist.contains(&data.path) { + let err = io::Error::new( + io::ErrorKind::AlreadyExists, + "This file is already in the playlist".to_string(), + ); + let err = AppError::from(err); + return HttpResponse::BadRequest().json(err); + } + } + + match Song::new(data.path.as_path()) { + Err(e) => HttpResponse::BadRequest().json(AppError::from(e)), + Ok(s) => { + let song = s.clone(); + let index = { + let mut playlist = PLAYLIST.lock().unwrap(); + (*playlist).add_song(s); + (*playlist).len() - 1 + }; + + let res = PlaylistAddSongResp { song, index }; + HttpResponse::Ok().json(res) + } + } +} diff --git a/src/server/handlers/playlist/get_playlist.rs b/src/server/handlers/playlist/get_playlist.rs new file mode 100644 index 0000000..445f354 --- /dev/null +++ b/src/server/handlers/playlist/get_playlist.rs @@ -0,0 +1,10 @@ +use actix_web::{get, HttpResponse, Responder}; + +use crate::server::PLAYLIST; + +#[get("/api/playlist")] +pub async fn get_playlist() -> impl Responder { + let playlist = PLAYLIST.lock().unwrap(); + let playlist_list = (*playlist).playlist().clone(); + HttpResponse::Ok().json(playlist_list) +} diff --git a/src/server/handlers/playlist/mod.rs b/src/server/handlers/playlist/mod.rs new file mode 100644 index 0000000..f262ecd --- /dev/null +++ b/src/server/handlers/playlist/mod.rs @@ -0,0 +1,7 @@ +mod add_song; +mod get_playlist; +mod remove_song; + +pub use add_song::*; +pub use get_playlist::*; +pub use remove_song::*; diff --git a/src/server/handlers/playlist/remove_song.rs b/src/server/handlers/playlist/remove_song.rs new file mode 100644 index 0000000..0f1c190 --- /dev/null +++ b/src/server/handlers/playlist/remove_song.rs @@ -0,0 +1,38 @@ +use std::io; + +use actix_web::{post, web, HttpResponse, Responder}; +use serde_derive::{Deserialize, Serialize}; + +use crate::audio::Song; +use crate::error::AppError; +use crate::server::PLAYLIST; + +#[derive(Clone, Debug, Deserialize)] +pub struct PlaylistRemoveSongReq { + pub index: usize, +} +#[derive(Clone, Debug, Serialize)] +pub struct PlaylistRemoveSongResp { + pub index: usize, + pub song: Song, +} +#[post("/api/playlist/remove")] +pub async fn remove_from_playlist(data: web::Json) -> impl Responder { + let mut playlist = PLAYLIST.lock().unwrap(); + if data.index >= playlist.len() { + let err = io::Error::new( + io::ErrorKind::InvalidInput, + "Index out of bounds".to_string(), + ); + let err = AppError::from(err); + return HttpResponse::BadRequest().json(err); + } + + let song = (*playlist).remove_song(data.index); + + let res = PlaylistRemoveSongResp { + index: data.index, + song, + }; + HttpResponse::Ok().json(res) +} diff --git a/src/server/handlers/websocket.rs b/src/server/handlers/websocket.rs new file mode 100644 index 0000000..c5322f5 --- /dev/null +++ b/src/server/handlers/websocket.rs @@ -0,0 +1,35 @@ +use actix::{Actor, StreamHandler}; +use actix_web::{get, post, web, Error, HttpRequest, HttpResponse}; +use actix_web_actors::ws; + +#[derive(Clone, Debug)] +struct WebSocketData {} + +impl Actor for WebSocketData { + type Context = ws::WebsocketContext; +} + +impl StreamHandler> for WebSocketData { + fn handle(&mut self, msg: Result, ctx: &mut Self::Context) { + match msg { + Ok(ws::Message::Ping(msg)) => ctx.pong(&msg), + Ok(ws::Message::Text(text)) => ctx.text(text), + Ok(ws::Message::Binary(bin)) => ctx.binary(bin), + _ => (), + } + } +} + +#[post("/api/ws/connect")] +pub async fn connect(req: HttpRequest, stream: web::Payload) -> Result { + let resp = ws::start(WebSocketData {}, &req, stream); + println!("{:?}", resp); + resp +} + +#[post("/api/ws/disconnect")] +pub async fn disconnect(req: HttpRequest, stream: web::Payload) -> Result { + let resp = ws::start(WebSocketData {}, &req, stream); + println!("{:?}", resp); + resp +} diff --git a/src/server/handlers/webtransport.rs b/src/server/handlers/webtransport.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/server/main.rs b/src/server/main.rs new file mode 100644 index 0000000..e227cb0 --- /dev/null +++ b/src/server/main.rs @@ -0,0 +1,85 @@ +mod audio; +mod command; +mod commands; +mod config; +mod context; +// mod handlers; +mod server; + +use std::io; +use std::path::PathBuf; + +use lazy_static::lazy_static; +use structopt::StructOpt; + +use dizi_commands::error::DiziResult; + +use crate::config::{AppConfig, ConfigStructure}; + +const PROGRAM_NAME: &str = "dizi"; +const CONFIG_FILE: &str = "server.toml"; + +lazy_static! { + // dynamically builds the config hierarchy + static ref CONFIG_HIERARCHY: Vec = { + let mut config_dirs = vec![]; + + if let Ok(p) = std::env::var("DIZI_CONFIG_HOME") { + let p = PathBuf::from(p); + if p.is_dir() { + config_dirs.push(p); + } + } + + if let Ok(dirs) = xdg::BaseDirectories::with_prefix(PROGRAM_NAME) { + config_dirs.push(dirs.get_config_home()); + } + + if let Ok(p) = std::env::var("HOME") { + let mut p = PathBuf::from(p); + p.push(format!(".config/{}", PROGRAM_NAME)); + if p.is_dir() { + config_dirs.push(p); + } + } + + // adds the default config files to the config hierarchy if running through cargo + if cfg!(debug_assertions) { + config_dirs.push(PathBuf::from("./config")); + } + config_dirs + }; +} + +#[derive(Clone, Debug, StructOpt)] +pub struct Args { + #[structopt(short = "v", long = "version")] + version: bool, +} + +fn run_server(args: Args) -> DiziResult<()> { + if args.version { + let version = env!("CARGO_PKG_VERSION"); + println!("{}", version); + return Ok(()); + } + + let config = AppConfig::get_config(CONFIG_FILE); + + eprintln!("{:#?}", config); + + server::serve(config) +} + +fn main() { + let args = Args::from_args(); + let res = run_server(args); + + match res { + Ok(_) => { + println!("TODO: saving playlist..."); + println!("Exiting server"); + } + Err(e) => eprintln!("Error: {}", e), + } +} diff --git a/src/server/server.rs b/src/server/server.rs new file mode 100644 index 0000000..1563b3e --- /dev/null +++ b/src/server/server.rs @@ -0,0 +1,70 @@ +use std::collections::HashMap; +use std::fs; +use std::io::{Cursor, BufRead, BufReader, Read, Write}; +use std::os::unix::net::{UnixStream, UnixListener}; +use std::path::Path; +use std::str; +use std::str::FromStr; +use std::sync::{Arc, Mutex}; +use std::thread; + +use lazy_static::lazy_static; + +use dizi_commands::api_command::ApiCommand; +use dizi_commands::error::DiziResult; + +use crate::audio::Playlist; +use crate::command::run_command; +use crate::config::default::AppConfig; +use crate::context::AppContext; + +lazy_static! { + pub static ref PLAYLIST: Mutex = Mutex::new(Playlist::default()); +} + +pub fn setup_socket(config: &AppConfig) -> DiziResult { + let socket = Path::new(config.server_ref().socket.as_path()); + + if socket.exists() { + fs::remove_file(&socket)?; + } + + let stream = UnixListener::bind(&socket)?; + Ok(stream) +} + +pub fn handle_client(stream: UnixStream, context: Arc>) { + let cursor = BufReader::new(stream); + for line in cursor.lines() { + if let Ok(line) = line { + // parse into json + let json_res: Result, serde_json::Error> = serde_json::from_str(&line); + + eprintln!("json_res: {:#?}", json_res); + + if let Ok(json_map) = json_res { + if let Some(s) = json_map.get("command") { + if let Ok(command) = ApiCommand::from_str(s) { + println!("{:#?}", command); + let mut context = context.lock().unwrap(); + run_command(&mut context, command, &json_map); + } + } + } + } + } +} + +pub fn serve(config: AppConfig) -> DiziResult<()> { + let context = AppContext::new(config)?; + let listener = setup_socket(context.config_ref())?; + let context_arc = Arc::new(Mutex::new(context)); + + for stream in listener.incoming() { + if let Ok(mut stream) = stream { + let context_arc_clone = context_arc.clone(); + thread::spawn(|| handle_client(stream, context_arc_clone)); + } + } + Ok(()) +}