├── .github └── workflows │ ├── rust-linux-main.yml │ └── rust-macos-main.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── config ├── client.toml ├── keymap.toml ├── layout.json ├── server.toml └── theme.toml ├── docs ├── README.md ├── configuration │ ├── README.md │ ├── client.toml.md │ ├── keymap.toml.md │ ├── layout.json.md │ ├── server.toml.md │ └── theme.toml.md └── query │ └── README.md ├── rustfmt.toml ├── screenshot.png └── src ├── bin ├── client │ ├── commands │ │ ├── change_directory.rs │ │ ├── command_line.rs │ │ ├── cursor_move.rs │ │ ├── goto.rs │ │ ├── mod.rs │ │ ├── open_file.rs │ │ ├── quit.rs │ │ ├── reload.rs │ │ ├── search.rs │ │ ├── search_glob.rs │ │ ├── search_skim.rs │ │ ├── search_string.rs │ │ ├── selection.rs │ │ ├── show_hidden.rs │ │ └── sort.rs │ ├── config │ │ ├── general │ │ │ ├── app.rs │ │ │ ├── client.rs │ │ │ ├── display_raw.rs │ │ │ ├── layout_raw.rs │ │ │ ├── mod.rs │ │ │ └── sort_raw.rs │ │ ├── keymap │ │ │ ├── default_keymap.rs │ │ │ ├── keymapping.rs │ │ │ └── mod.rs │ │ ├── mod.rs │ │ ├── option │ │ │ ├── display_option.rs │ │ │ ├── layout_option.rs │ │ │ ├── mod.rs │ │ │ ├── select_option.rs │ │ │ ├── sort_option.rs │ │ │ └── sort_type.rs │ │ └── theme │ │ │ ├── app_theme.rs │ │ │ ├── mod.rs │ │ │ └── style.rs │ ├── context │ │ ├── app_context.rs │ │ ├── commandline_context.rs │ │ ├── message_queue.rs │ │ ├── mod.rs │ │ ├── server_state.rs │ │ └── tab_context.rs │ ├── event │ │ ├── app_event.rs │ │ ├── mod.rs │ │ └── process_event.rs │ ├── fs │ │ ├── dirlist.rs │ │ ├── entry.rs │ │ ├── metadata.rs │ │ └── mod.rs │ ├── history.rs │ ├── key_command │ │ ├── commands.rs │ │ ├── constants.rs │ │ ├── impl_appcommand.rs │ │ ├── impl_appexecute.rs │ │ ├── impl_display.rs │ │ ├── impl_from_keymap.rs │ │ ├── impl_from_str.rs │ │ ├── keybind.rs │ │ ├── mod.rs │ │ └── traits.rs │ ├── main.rs │ ├── preview │ │ ├── mod.rs │ │ ├── preview_default.rs │ │ └── preview_dir.rs │ ├── run │ │ ├── mod.rs │ │ ├── run_control.rs │ │ ├── run_query.rs │ │ ├── run_query_all.rs │ │ └── run_ui.rs │ ├── tab.rs │ ├── traits │ │ ├── mod.rs │ │ └── to_string.rs │ ├── ui │ │ ├── backend.rs │ │ ├── mod.rs │ │ ├── views │ │ │ ├── mod.rs │ │ │ ├── tui_command_menu.rs │ │ │ ├── tui_folder_view.rs │ │ │ ├── tui_textfield.rs │ │ │ └── tui_view.rs │ │ └── widgets │ │ │ ├── mod.rs │ │ │ ├── tui_dirlist_detailed.rs │ │ │ ├── tui_footer.rs │ │ │ ├── tui_menu.rs │ │ │ ├── tui_player.rs │ │ │ ├── tui_playlist.rs │ │ │ ├── tui_prompt.rs │ │ │ ├── tui_text.rs │ │ │ └── tui_topbar.rs │ └── util │ │ ├── devicons.rs │ │ ├── format.rs │ │ ├── keyparse.rs │ │ ├── mod.rs │ │ ├── request.rs │ │ ├── search.rs │ │ ├── string.rs │ │ ├── style.rs │ │ └── unix.rs └── server │ ├── audio │ ├── device.rs │ ├── mod.rs │ ├── request.rs │ └── symphonia │ │ ├── decode.rs │ │ ├── mod.rs │ │ ├── player │ │ ├── impl_audio_player.rs │ │ └── mod.rs │ │ └── stream.rs │ ├── client.rs │ ├── config │ ├── general │ │ ├── app.rs │ │ ├── mod.rs │ │ ├── player.rs │ │ └── server.rs │ └── mod.rs │ ├── context │ ├── app_context.rs │ ├── mod.rs │ └── playlist_context.rs │ ├── error │ ├── error_type.rs │ └── mod.rs │ ├── events.rs │ ├── main.rs │ ├── playlist │ ├── impl_playlist.rs │ └── mod.rs │ ├── server.rs │ ├── server_commands │ ├── mod.rs │ ├── player.rs │ ├── playlist.rs │ └── server.rs │ ├── server_util.rs │ ├── traits │ ├── audio_player.rs │ ├── mod.rs │ └── playlist.rs │ └── util │ ├── mimetype.rs │ └── mod.rs ├── error ├── error_kind.rs ├── error_type.rs └── mod.rs ├── lib.rs ├── player.rs ├── playlist.rs ├── request ├── client.rs └── mod.rs ├── response ├── constants.rs ├── mod.rs └── server.rs ├── song.rs ├── traits.rs └── utils ├── mod.rs └── stream.rs /.github/workflows/rust-linux-main.yml: -------------------------------------------------------------------------------- 1 | name: Linux build 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | check: 11 | name: Rust Linux ${{ matrix.rust }} 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | rust: [stable] 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Update apt 19 | run: sudo apt update 20 | - name: Install alsa 21 | run: sudo apt-get install libasound2-dev 22 | - name: Install libjack 23 | run: sudo apt-get install libjack-jackd2-dev libjack-jackd2-0 24 | - uses: actions/checkout@v2 25 | - name: Install minimal ${{ matrix.rust }} rust 26 | uses: actions-rs/toolchain@v1 27 | with: 28 | override: true 29 | profile: minimal 30 | toolchain: ${{ matrix.rust }} 31 | - run: cargo -Vv && rustc -Vv 32 | - run: cargo check 33 | - run: cargo check --all-features 34 | if: ${{ matrix.rust == 'stable' }} 35 | - run: cargo fmt --all -- --check 36 | if: ${{ matrix.rust == 'stable' }} 37 | - run: cargo test 38 | if: ${{ matrix.rust == 'stable' }} 39 | -------------------------------------------------------------------------------- /.github/workflows/rust-macos-main.yml: -------------------------------------------------------------------------------- 1 | name: MacOS build 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | check: 11 | name: Rust MacOS ${{ matrix.rust }} 12 | runs-on: macos-latest 13 | strategy: 14 | matrix: 15 | rust: [stable] 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Install minimal ${{ matrix.rust }} rust 19 | uses: actions-rs/toolchain@v1 20 | with: 21 | override: true 22 | profile: minimal 23 | toolchain: ${{ matrix.rust }} 24 | - run: cargo -Vv && rustc -Vv 25 | - run: cargo check 26 | - run: cargo check --all-features 27 | if: ${{ matrix.rust == 'stable' }} 28 | - run: cargo fmt --all -- --check 29 | if: ${{ matrix.rust == 'stable' }} 30 | - run: cargo test 31 | if: ${{ matrix.rust == 'stable' }} 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dizi" 3 | version = "0.3.0" 4 | authors = ["Jiayi Zhao "] 5 | edition = "2021" 6 | description = "Terminal music player inspired by moc" 7 | homepage = "https://github.com/kamiyaa/dizi" 8 | repository = "https://github.com/kamiyaa/dizi" 9 | license = "LGPL-3.0" 10 | keywords = ["ratatui", "music-player"] 11 | categories = ['command-line-interface', 'command-line-utilities'] 12 | 13 | 14 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 15 | 16 | [[bin]] 17 | name = "dizi-server" 18 | path = "src/bin/server/main.rs" 19 | 20 | [[bin]] 21 | name = "dizi" 22 | path = "src/bin/client/main.rs" 23 | 24 | [dependencies] 25 | alphanumeric-sort = "^1" 26 | chrono = "^0" 27 | clap = { version = "^4", features = ["derive"] } 28 | dirs-next = "^2" 29 | globset = "^0" 30 | lazy_static = "^1" 31 | libc = "^0" 32 | memmap = "^0" 33 | m3u = "^1" 34 | phf = { version = "^0", features = ["macros"], optional = true } 35 | rand = "^0" 36 | ratatui = { version = "^0", default-features = false, features = ["termion"] } 37 | rustyline = "^4" 38 | serde = { version = "^1", features = ["derive"] } 39 | serde_json = "^1" 40 | shell-words = "^1" 41 | shellexpand = "^2" 42 | signal-hook = "^0" 43 | skim = "^0" 44 | strfmt = "^0" 45 | symphonia = { version = "^0", features = ["all"] } 46 | termion = "^1" 47 | tokio = { version = "^1", features = [ "macros", "rt", "rt-multi-thread" ] } 48 | toml = "^0" 49 | tracing = "^0" 50 | tracing-subscriber = { version = "^0", features = [ "std", "env-filter" ] } 51 | uuid = { version = "^0", features = ["v4"] } 52 | unicode-width = "^0" 53 | unicode-segmentation = "^1" 54 | xdg = "^2" 55 | cpal = "^0" 56 | # pipewire = { optional = true, version = "^0" } 57 | 58 | [features] 59 | default = [ "devicons" ] 60 | devicons = [ "phf" ] 61 | jack = [ "cpal/jack" ] 62 | mouse = [] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Linux build](https://github.com/kamiyaa/dizi/actions/workflows/rust-linux-main.yml/badge.svg)](https://github.com/kamiyaa/dizi/actions/workflows/rust-linux-main.yml) 2 | 3 | [![MacOS build](https://github.com/kamiyaa/dizi/actions/workflows/rust-macos-main.yml/badge.svg)](https://github.com/kamiyaa/dizi/actions/workflows/rust-macos-main.yml) 4 | 5 | # dizi 6 | Server-client music player written in Rust 7 | 8 | The goal of this project is to create a modern version of [mocp](http://moc.daper.net/) in Rust. 9 | 10 | ![Alt text](screenshot.png?raw=true "dizi") 11 | 12 | ## Motivation 13 | mocp currently interfaces with ALSA to play audio. 14 | This doesn't play well with [pipewire](https://pipewire.org/)'s pipewire-alsa plugin; 15 | whenever mocp is playing music, other audio/video apps stop working and vice versa. 16 | 17 | ## Dependencies 18 | - A system supporting UNIX sockets 19 | - [cargo](https://github.com/rust-lang/cargo/) 20 | - [rustc](https://www.rust-lang.org/) 21 | - Jack or Alsa or any other audio system [cpal](https://github.com/RustAudio/cpal) supports 22 | - `file` command for audio file detection 23 | 24 | ## Building 25 | ``` 26 | ~$ cargo build 27 | ``` 28 | 29 | ## Installation 30 | #### For single user 31 | ``` 32 | ~$ cargo install --path=. --force 33 | ``` 34 | 35 | #### System wide 36 | ``` 37 | ~# cargo install --path=. --force --root=/usr/local # /usr also works 38 | ``` 39 | 40 | ## Usage 41 | ``` 42 | ~ $ dizi-server # starts server 43 | ~ $ RUST_LOG=debug dizi-server # starts server with debug messages enabled 44 | ~ $ dizi # starts server if not already started, then starts frontend 45 | ``` 46 | 47 | ## Configuration 48 | 49 | Check out [docs](/docs) for details and [config](/config) for examples 50 | 51 | #### [client.toml](/config/client.toml) 52 | - client configurations 53 | 54 | #### [keymap.toml](/config/keymap.toml) 55 | - for keybindings for client 56 | 57 | #### [theme.toml](/config/theme.toml) 58 | - color customizations for client 59 | 60 | #### [server.toml](/config/server.toml) 61 | - server configurations 62 | 63 | ## Contributing 64 | See [docs](/docs) 65 | 66 | ## Features/Bugs 67 | 68 | Please create an issue :) 69 | 70 | ## TODOs 71 | 72 | ### Server-side 73 | - [x] play/pause support 74 | - [x] get audio duration (requires [rodio](https://github.com/RustAudio/rodio) and [symphonia](https://github.com/pdeljanov/Symphonia) to work together on this) 75 | - [x] volume support 76 | - [x] fast forward/rewind 77 | - [x] directory playing 78 | - [x] shuffle 79 | - [x] repeat 80 | - [x] next 81 | - [ ] sorting 82 | - [x] playlist support 83 | - [x] add/delete/update songs 84 | - [x] recursively add songs in a directory 85 | - [x] shuffle 86 | - [x] repeat 87 | - [x] next 88 | - [x] loading 89 | - [x] clearing 90 | - [x] save on exit 91 | - [x] show music progress 92 | - [x] configurable audio system 93 | - [x] ALSA support (current default) 94 | - [x] JACK support 95 | - [ ] Pulseaudio support (issue https://github.com/RustAudio/cpal/issues/259) 96 | - [ ] Pipewire support (issue https://github.com/RustAudio/cpal/issues/554) 97 | - [x] querying 98 | - [x] file name 99 | - [x] file path 100 | - [x] show audio metadata (title, artists, genre, album, etc) 101 | - [x] playlist index and length 102 | - [x] on song change hook 103 | 104 | ### Client-side 105 | - [x] show hidden files 106 | - [x] searching 107 | - [x] glob search 108 | - [x] case-insensitive search 109 | - [x] skim search (fzf) 110 | - [x] show player progression 111 | - [x] playlist support 112 | - [x] show playlist 113 | - [x] add/delete/update songs 114 | - [x] shuffle 115 | - [x] repeat 116 | - [x] next 117 | - [x] clearing 118 | - [ ] show audio metadata (artists, genre, album, etc) 119 | - [x] theming support 120 | - [x] custom layout support 121 | -------------------------------------------------------------------------------- /config/client.toml: -------------------------------------------------------------------------------- 1 | [client] 2 | socket = "~/dizi-server-socket" 3 | 4 | home_dir = "~/music" 5 | 6 | [client.display] 7 | show_borders = true 8 | show_hidden = false 9 | show_icons = false 10 | layout = "~/.config/dizi/layout.json" 11 | 12 | [client.display.sort] 13 | directories_first = true 14 | reverse = false 15 | sort_method = "natural" 16 | -------------------------------------------------------------------------------- /config/keymap.toml: -------------------------------------------------------------------------------- 1 | [[keymap]] 2 | keys = [ "q" ] 3 | command = "close" 4 | 5 | [[keymap]] 6 | keys = [ "r" ] 7 | command = "reload_dirlist" 8 | 9 | [[keymap]] 10 | keys = [ "z", "h" ] 11 | command = "reload_dirlist" 12 | 13 | [[keymap]] 14 | keys = [ "arrow_up" ] 15 | command = "cursor_move_up" 16 | 17 | [[keymap]] 18 | keys = [ "arrow_down" ] 19 | command = "cursor_move_down" 20 | 21 | [[keymap]] 22 | keys = [ "arrow_left" ] 23 | command = "cd .." 24 | 25 | [[keymap]] 26 | keys = [ "arrow_right" ] 27 | command = "open" 28 | 29 | [[keymap]] 30 | keys = [ "\n" ] 31 | command = "open" 32 | 33 | [[keymap]] 34 | keys = [ "end" ] 35 | command = "cursor_move_end" 36 | 37 | [[keymap]] 38 | keys = [ "home" ] 39 | command = "cursor_move_home" 40 | 41 | [[keymap]] 42 | keys = [ "page_up" ] 43 | command = "cursor_move_page_up" 44 | 45 | [[keymap]] 46 | keys = [ "page_down" ] 47 | command = "cursor_move_page_down" 48 | 49 | [[keymap]] 50 | keys = [ "=" ] 51 | command = "go_to_playing" 52 | 53 | [[keymap]] 54 | keys = [ "\t" ] 55 | command = "toggle_view" 56 | 57 | [[keymap]] 58 | keys = [ "c", "d" ] 59 | command = ":cd " 60 | 61 | [[keymap]] 62 | keys = [ "t" ] 63 | command = "select --all=true --toggle=true" 64 | 65 | [[keymap]] 66 | keys = [ ":" ] 67 | command = ":" 68 | 69 | [[keymap]] 70 | keys = [ ";" ] 71 | command = ":" 72 | 73 | [[keymap]] 74 | keys = [ "?" ] 75 | command = ":search " 76 | 77 | [[keymap]] 78 | keys = [ "\\" ] 79 | command = ":search_glob " 80 | 81 | [[keymap]] 82 | keys = [ "/" ] 83 | command = "search_skim" 84 | 85 | [[keymap]] 86 | keys = [ "[" ] 87 | command = "search_prev" 88 | 89 | [[keymap]] 90 | keys = [ "]" ] 91 | command = "search_next" 92 | 93 | [[keymap]] 94 | keys = [ "u", "r" ] 95 | command = "sort reverse" 96 | 97 | [[keymap]] 98 | keys = [ "u", "l" ] 99 | command = "sort lexical" 100 | 101 | [[keymap]] 102 | keys = [ "u", "m" ] 103 | command = "sort mtime" 104 | 105 | [[keymap]] 106 | keys = [ "u", "n" ] 107 | command = "sort natural" 108 | 109 | [[keymap]] 110 | keys = [ "u", "s" ] 111 | command = "sort size" 112 | 113 | [[keymap]] 114 | keys = [ "u", "e" ] 115 | command = "sort ext" 116 | 117 | [[keymap]] 118 | keys = [ "g", "r" ] 119 | command = "cd /" 120 | 121 | [[keymap]] 122 | keys = [ "g", "c" ] 123 | command = "cd ~/.config" 124 | 125 | [[keymap]] 126 | keys = [ "g", "d" ] 127 | command = "cd ~/Downloads" 128 | 129 | [[keymap]] 130 | keys = [ "g", "e" ] 131 | command = "cd /etc" 132 | 133 | [[keymap]] 134 | keys = [ "g", "h" ] 135 | command = "cd ~/" 136 | 137 | [[keymap]] 138 | keys = [ "Q" ] 139 | command = "server_request" 140 | request.api = "/server/quit" 141 | 142 | [[keymap]] 143 | keys = [ " " ] 144 | command = "server_request" 145 | request.api = "/player/toggle/play" 146 | 147 | [[keymap]] 148 | keys = [ "0" ] 149 | command = "server_request" 150 | request.api = "/player/volume/increase" 151 | request.amount = 1 152 | 153 | [[keymap]] 154 | keys = [ "9" ] 155 | command = "server_request" 156 | request.api = "/player/volume/decrease" 157 | request.amount = 1 158 | 159 | [[keymap]] 160 | keys = [ "," ] 161 | command = "server_request" 162 | request.api = "/player/rewind" 163 | request.amount = 10 164 | 165 | [[keymap]] 166 | keys = [ "." ] 167 | command = "server_request" 168 | request.api = "/player/fast_forward" 169 | request.amount = 10 170 | 171 | [[keymap]] 172 | keys = [ "S" ] 173 | command = "server_request" 174 | request.api = "/player/toggle/shuffle" 175 | 176 | [[keymap]] 177 | keys = [ "R" ] 178 | command = "server_request" 179 | request.api = "/player/toggle/repeat" 180 | 181 | [[keymap]] 182 | keys = [ "N" ] 183 | command = "server_request" 184 | request.api = "/player/toggle/next" 185 | 186 | [[keymap]] 187 | keys = [ "n" ] 188 | command = "server_request" 189 | request.api = "/player/play/next" 190 | 191 | [[keymap]] 192 | keys = [ "p" ] 193 | command = "server_request" 194 | request.api = "/player/play/previous" 195 | 196 | [[keymap]] 197 | keys = [ "a" ] 198 | command = "server_request" 199 | request.api = "/playlist/append" 200 | 201 | [[keymap]] 202 | keys = [ "d" ] 203 | command = "server_request" 204 | request.api = "/playlist/remove" 205 | 206 | [[keymap]] 207 | keys = [ "C" ] 208 | command = "server_request" 209 | request.api = "/playlist/clear" 210 | 211 | [[keymap]] 212 | keys = [ "w" ] 213 | command = "server_request" 214 | request.api = "/playlist/move_up" 215 | 216 | [[keymap]] 217 | keys = [ "s" ] 218 | command = "server_request" 219 | request.api = "/playlist/move_down" 220 | -------------------------------------------------------------------------------- /config/layout.json: -------------------------------------------------------------------------------- 1 | { 2 | "layout": { 3 | "type": "composite", 4 | "direction": "horizontal", 5 | "ratio": 1, 6 | "widgets": [ 7 | { 8 | "type": "simple", 9 | "widget": "file_browser", 10 | "ratio": 1, 11 | "border": true 12 | }, 13 | { 14 | "type": "composite", 15 | "direction": "vertical", 16 | "ratio": 1, 17 | "widgets": [ 18 | { 19 | "type": "simple", 20 | "widget": "music_player", 21 | "ratio": 2, 22 | "border": true 23 | }, 24 | { 25 | "type": "simple", 26 | "widget": "playlist", 27 | "ratio": 3, 28 | "border": true 29 | } 30 | ] 31 | } 32 | ] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /config/server.toml: -------------------------------------------------------------------------------- 1 | [server] 2 | socket = "~/dizi-server-socket" 3 | 4 | # Where to save playlist on exit 5 | playlist = "~/dizi_playlist.m3u" 6 | 7 | # supports alsa, jack 8 | audio_system = "alsa" 9 | 10 | # run a script whenever the song changes 11 | # on_song_change = "some_script" 12 | 13 | [server.player] 14 | 15 | shuffle = false 16 | repeat = true 17 | next = true 18 | volume = 50 19 | -------------------------------------------------------------------------------- /config/theme.toml: -------------------------------------------------------------------------------- 1 | [playing] 2 | fg = 'light_yellow' 3 | bg = 'black' 4 | 5 | [playlist] 6 | fg = 'magenta' 7 | 8 | [selection] 9 | fg = "light_yellow" 10 | bold = true 11 | 12 | [executable] 13 | fg = "light_green" 14 | bold = true 15 | 16 | [regular] 17 | fg = "white" 18 | 19 | [directory] 20 | fg = "light_blue" 21 | bold = true 22 | 23 | [link] 24 | fg = "cyan" 25 | bold = true 26 | 27 | [link_invalid] 28 | fg = "red" 29 | bold = true 30 | 31 | [socket] 32 | fg = "light_magenta" 33 | bold = true 34 | 35 | [ext] 36 | 37 | [ext.bmp] 38 | fg = "yellow" 39 | [ext.heic] 40 | fg = "yellow" 41 | [ext.jpg] 42 | fg = "yellow" 43 | [ext.jpeg] 44 | fg = "yellow" 45 | [ext.pgm] 46 | fg = "yellow" 47 | [ext.png] 48 | fg = "yellow" 49 | [ext.ppm] 50 | fg = "yellow" 51 | [ext.svg] 52 | fg = "yellow" 53 | [ext.gif] 54 | fg = "yellow" 55 | 56 | [ext.wav] 57 | fg = "magenta" 58 | [ext.flac] 59 | fg = "magenta" 60 | [ext.mp3] 61 | fg = "magenta" 62 | [ext.avi] 63 | fg = "magenta" 64 | [ext.m3u] 65 | fg = "red" 66 | [ext.mov] 67 | fg = "magenta" 68 | [ext.m4v] 69 | fg = "magenta" 70 | [ext.mp4] 71 | fg = "magenta" 72 | [ext.mkv] 73 | fg = "magenta" 74 | [ext.m4a] 75 | fg = "magenta" 76 | [ext.ts] 77 | fg = "magenta" 78 | [ext.webm] 79 | fg = "magenta" 80 | [ext.wmv] 81 | fg = "magenta" 82 | 83 | [ext.7z] 84 | fg = "red" 85 | [ext.zip] 86 | fg = "red" 87 | [ext.bz2] 88 | fg = "red" 89 | [ext.gz] 90 | fg = "red" 91 | [ext.tar] 92 | fg = "red" 93 | [ext.tgz] 94 | fg = "red" 95 | [ext.xz] 96 | fg = "red" 97 | [ext.rar] 98 | fg = "red" 99 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Docs 2 | 3 | - [configuration](/docs/configuration/) 4 | - [query](/docs/query/) 5 | - [contributing](/docs/contributing.md) 6 | -------------------------------------------------------------------------------- /docs/configuration/README.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | Dizi reads configurations from the following directories using environment variables (in order of precedence): 3 | - `$DIZI_CONFIG_HOME` 4 | - `$XDG_CONFIG_HOME/dizi` 5 | - `$HOME/.config/dizi` 6 | 7 | Dizi can currently be configured using the following files: 8 | 9 | ## Client Configuration 10 | - [client.toml](/docs/configuration/client.toml.md): configuring the client 11 | - [keymap.toml](/docs/configuration/keymap.toml.md): configuring the client's keymapping 12 | - [layout.json](/docs/configuration/layout.json.md): configuring the look of client 13 | - [theme.toml](/docs/configuration/theme.toml.md): theming configurations 14 | 15 | ## Server Configuration 16 | - [server.toml](/docs/configuration/server.toml.md): configuring the server 17 | -------------------------------------------------------------------------------- /docs/configuration/client.toml.md: -------------------------------------------------------------------------------- 1 | # client.toml 2 | 3 | This file is for configuring the client 4 | 5 | ```toml 6 | [client] 7 | # socket path for connecting to server 8 | socket = "/tmp/dizi-server-socket" 9 | 10 | # the directory to start the client in 11 | home_dir = "~/music" 12 | 13 | [client.display] 14 | # show borders around widgets 15 | show_borders = true 16 | 17 | # show hidden files 18 | show_hidden = false 19 | 20 | # layout file 21 | layout = "~/.config/dizi/layout.json" 22 | 23 | [client.display.sort] 24 | # list directory first 25 | directory_first = true 26 | # reverse directory 27 | reverse = false 28 | 29 | # Options include 30 | # - lexical (10.txt comes before 2.txt) 31 | # - natural (2.txt comes before 10.txt) 32 | # - mtime 33 | sort_method = "natural" 34 | ``` 35 | -------------------------------------------------------------------------------- /docs/configuration/layout.json.md: -------------------------------------------------------------------------------- 1 | # layout.json 2 | 3 | This file is for configuring the look of the client 4 | 5 | There are 2 types of widgets: 6 | - simple 7 | - composite 8 | 9 | `simple`: widgets are standalone widgets. 10 | - `ratio`: the ratio of how much space a widget takes up in a given composite widget 11 | - `border`: show borders or not 12 | - `widget`: currently supports `file_browser`, `music_player`, `playlist` 13 | 14 | `composite`: widgets are made up of more widgets. 15 | - `ratio`: the ratio of how much space a widget takes up in a given composite widget 16 | - ~~`border`: show borders or not~~ 17 | - `direction`: put widgets beside each other (horizontal) or above/below each other (vertical) 18 | - `widget`: list of widgets 19 | 20 | 21 | ```json 22 | { 23 | "layout": { 24 | "type": "composite", 25 | "direction": "horizontal", 26 | "ratio": 1, 27 | "widgets": [ 28 | { 29 | "type": "simple", 30 | "widget": "file_browser", 31 | "ratio": 1, 32 | "border": true 33 | }, 34 | { 35 | "type": "composite", 36 | "direction": "vertical", 37 | "ratio": 1, 38 | "widgets": [ 39 | { 40 | "type": "simple", 41 | "widget": "music_player", 42 | "ratio": 2, 43 | "border": true 44 | }, 45 | { 46 | "type": "simple", 47 | "widget": "playlist", 48 | "ratio": 3, 49 | "border": true 50 | } 51 | ] 52 | } 53 | ] 54 | } 55 | } 56 | ``` 57 | -------------------------------------------------------------------------------- /docs/configuration/server.toml.md: -------------------------------------------------------------------------------- 1 | # server.toml 2 | 3 | This file is for configuring the server 4 | 5 | ```toml 6 | [server] 7 | # socket path for clients to connect to 8 | socket = "/tmp/dizi-server-socket" 9 | 10 | # Where to save playlist on exit 11 | playlist = "~/.config/dizi/playlist.m3u" 12 | 13 | # How often to poll audio thread for updates in milliseconds (not implemented) 14 | # slower = less responsive player 15 | # faster = more cpu usage (from busy waiting) 16 | poll_rate = 200 17 | 18 | # path to run a script whenever the song changes 19 | # on_song_change = "some_script" 20 | 21 | [server.player] 22 | # supports alsa, jack on Linux 23 | # will use the default on other systems (MacOS, Windows) 24 | audio_system = "alsa" 25 | 26 | shuffle = false 27 | repeat = true 28 | next = true 29 | ``` 30 | -------------------------------------------------------------------------------- /docs/configuration/theme.toml.md: -------------------------------------------------------------------------------- 1 | # theme.toml 2 | 3 | This file is used to for theming the client 4 | 5 | ## Style 6 | Each style has the following fields: 7 | ```toml 8 | # background color 9 | bg = "light_blue" 10 | # foreground color 11 | fg = "blue" 12 | bold = false 13 | underline = false 14 | invert = false 15 | ``` 16 | 17 | ## Color 18 | Supports 16 colors as well as hex colors via `rgb(r,g,b)` 19 | ``` 20 | black 21 | red 22 | blue 23 | green 24 | yellow 25 | magenta 26 | cyan 27 | white 28 | gray 29 | dark_gray 30 | light_red 31 | light_green 32 | light_yellow 33 | light_blue 34 | light_magenta 35 | light_cyan 36 | _ 37 | ``` 38 | 39 | ## Theme 40 | 41 | supports theming via system file types as well as extensions 42 | 43 | System file types include: 44 | ```toml 45 | # for selected files 46 | [selection] 47 | fg = "light_yellow" 48 | bold = true 49 | 50 | # for executable files 51 | [executable] 52 | fg = "light_green" 53 | bold = true 54 | 55 | # default theme 56 | [regular] 57 | fg = "white" 58 | 59 | # for directories 60 | [directory] 61 | fg = "light_blue" 62 | bold = true 63 | 64 | # for symlinks 65 | [link] 66 | fg = "cyan" 67 | bold = true 68 | 69 | # for invalid symlinks 70 | [link_invalid] 71 | fg = "red" 72 | bold = true 73 | 74 | # for sockets 75 | [socket] 76 | fg = "light_magenta" 77 | bold = true 78 | ``` 79 | 80 | Via extensions 81 | ```toml 82 | [ext] 83 | [ext.jpg] 84 | fg = "yellow" 85 | [ext.jpeg] 86 | fg = "yellow" 87 | [ext.png] 88 | fg = "yellow" 89 | ``` 90 | -------------------------------------------------------------------------------- /docs/query/README.md: -------------------------------------------------------------------------------- 1 | # Client 2 | 3 | ## Queries 4 | 5 | You can query the server for information via 6 | 7 | ```sh 8 | ~$ dizi -q 'QUERY' 9 | ``` 10 | 11 | Available query strings include: 12 | 13 | ``` 14 | player_status # playing, paused, stopped 15 | player_volume # between 0 and 100 16 | player_next # boolean (true, false) if go to next song is enabled 17 | player_repeat # boolean (true, false) if repeat is enabled 18 | player_shuffle # boolean (true, false) if shuffle is enabled 19 | file_name # file name of current song 20 | file_path # file path of current song 21 | playlist_status # (file, directory) whether player is 22 | # playing a playlist file or from a directory 23 | 24 | playlist_index # index of the song being played in the file playlist 25 | playlist_length # length of playlist 26 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | # Using defaults as defined at https://github.com/rust-lang/rustfmt/blob/master/Configurations.md 2 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kamiyaa/dizi/d53ed35a7070111e938eeeac9751a4338801b8b0/screenshot.png -------------------------------------------------------------------------------- /src/bin/client/commands/change_directory.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use std::path; 3 | 4 | use dizi::error::DiziResult; 5 | 6 | use crate::commands::reload; 7 | use crate::config::option::WidgetType; 8 | use crate::context::AppContext; 9 | use crate::history::DirectoryHistory; 10 | 11 | pub fn cd(path: &path::Path, context: &mut AppContext) -> io::Result<()> { 12 | std::env::set_current_dir(path)?; 13 | context.tab_context_mut().curr_tab_mut().set_cwd(path); 14 | Ok(()) 15 | } 16 | 17 | pub fn change_directory(context: &mut AppContext, path: &path::Path) -> DiziResult { 18 | let new_cwd = if path.is_absolute() { 19 | path.canonicalize()? 20 | } else { 21 | let mut new_cwd = std::env::current_dir()?; 22 | new_cwd.push(path.canonicalize()?); 23 | new_cwd 24 | }; 25 | 26 | cd(new_cwd.as_path(), context)?; 27 | let options = context.config_ref().display_options_ref().clone(); 28 | let ui_context = context.ui_context_ref().clone(); 29 | context 30 | .tab_context_mut() 31 | .curr_tab_mut() 32 | .history_mut() 33 | .populate_to_root(new_cwd.as_path(), &ui_context, &options)?; 34 | Ok(()) 35 | } 36 | 37 | // ParentDirectory command 38 | pub fn parent_directory(context: &mut AppContext) -> DiziResult { 39 | if context.get_view_widget() != WidgetType::FileBrowser { 40 | return Ok(()); 41 | } 42 | 43 | if let Some(parent) = context 44 | .tab_context_ref() 45 | .curr_tab_ref() 46 | .cwd() 47 | .parent() 48 | .map(|p| p.to_path_buf()) 49 | { 50 | std::env::set_current_dir(&parent)?; 51 | context 52 | .tab_context_mut() 53 | .curr_tab_mut() 54 | .set_cwd(parent.as_path()); 55 | reload::soft_reload(context.tab_context_ref().index, context)?; 56 | } 57 | Ok(()) 58 | } 59 | -------------------------------------------------------------------------------- /src/bin/client/commands/command_line.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use dizi::error::DiziResult; 4 | 5 | use crate::config::AppKeyMapping; 6 | use crate::context::AppContext; 7 | use crate::key_command::{AppExecute, Command}; 8 | use crate::ui::views::TuiTextField; 9 | use crate::ui::AppBackend; 10 | 11 | pub fn read_and_execute( 12 | context: &mut AppContext, 13 | backend: &mut AppBackend, 14 | keymap_t: &AppKeyMapping, 15 | prefix: &str, 16 | suffix: &str, 17 | ) -> DiziResult { 18 | context.flush_event(); 19 | let user_input: Option = TuiTextField::default() 20 | .prompt(":") 21 | .prefix(prefix) 22 | .suffix(suffix) 23 | .get_input(backend, context); 24 | 25 | if let Some(s) = user_input { 26 | let trimmed = s.trim_start(); 27 | let command = Command::from_str(trimmed)?; 28 | command.execute(context, backend, keymap_t) 29 | } else { 30 | Ok(()) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/bin/client/commands/goto.rs: -------------------------------------------------------------------------------- 1 | use dizi::error::DiziResult; 2 | use dizi::playlist::PlaylistType; 3 | 4 | use crate::commands::change_directory; 5 | use crate::commands::cursor_move::set_playlist_index; 6 | use crate::commands::search_string; 7 | use crate::config::option::WidgetType; 8 | use crate::context::AppContext; 9 | 10 | fn _directory_goto_playing(context: &mut AppContext) -> DiziResult { 11 | let player_state = &context.server_state_ref().player; 12 | 13 | if let Some(song) = player_state.song.clone() { 14 | let file_path = song.file_path(); 15 | if let Some(parent) = file_path.parent() { 16 | change_directory::change_directory(context, parent)?; 17 | } 18 | let file_name = song.file_name(); 19 | search_string::search_exact(context, file_name)?; 20 | } 21 | Ok(()) 22 | } 23 | 24 | fn _playlist_goto_playing(context: &mut AppContext) -> DiziResult { 25 | let player_state = &context.server_state_ref().player; 26 | 27 | match player_state.playlist_status { 28 | PlaylistType::DirectoryListing => { 29 | if let Some(song) = player_state.song.clone() { 30 | let file_path = song.file_path(); 31 | if let Some((index, _)) = player_state 32 | .playlist 33 | .playlist() 34 | .iter() 35 | .enumerate() 36 | .find(|(_, song)| song.file_path() == file_path) 37 | { 38 | set_playlist_index(context, index); 39 | } 40 | } 41 | } 42 | PlaylistType::PlaylistFile => { 43 | if let Some(index) = player_state.playlist.playing_index { 44 | set_playlist_index(context, index); 45 | } 46 | } 47 | } 48 | Ok(()) 49 | } 50 | 51 | pub fn goto_playing(context: &mut AppContext) -> DiziResult { 52 | let widget = context.get_view_widget(); 53 | match widget { 54 | WidgetType::FileBrowser => _directory_goto_playing(context)?, 55 | WidgetType::Playlist => _playlist_goto_playing(context)?, 56 | _ => {} 57 | } 58 | Ok(()) 59 | } 60 | -------------------------------------------------------------------------------- /src/bin/client/commands/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod change_directory; 2 | pub mod command_line; 3 | pub mod cursor_move; 4 | pub mod goto; 5 | pub mod open_file; 6 | pub mod quit; 7 | pub mod reload; 8 | pub mod search; 9 | pub mod search_glob; 10 | pub mod search_skim; 11 | pub mod search_string; 12 | pub mod selection; 13 | pub mod show_hidden; 14 | pub mod sort; 15 | -------------------------------------------------------------------------------- /src/bin/client/commands/open_file.rs: -------------------------------------------------------------------------------- 1 | use dizi::error::DiziResult; 2 | use dizi::request::client::ClientRequest; 3 | 4 | use crate::config::option::WidgetType; 5 | use crate::context::AppContext; 6 | use crate::util::request::send_client_request; 7 | 8 | use super::change_directory; 9 | 10 | pub fn open(context: &mut AppContext) -> DiziResult { 11 | let widget = context.get_view_widget(); 12 | 13 | match widget { 14 | WidgetType::FileBrowser => file_browser_open(context)?, 15 | WidgetType::Playlist => playlist_open(context)?, 16 | _ => {} 17 | } 18 | Ok(()) 19 | } 20 | 21 | pub fn file_browser_open(context: &mut AppContext) -> DiziResult { 22 | if let Some(entry) = context 23 | .tab_context_ref() 24 | .curr_tab_ref() 25 | .curr_list_ref() 26 | .and_then(|s| s.curr_entry_ref()) 27 | { 28 | if entry.file_path().is_dir() { 29 | let path = entry.file_path().to_path_buf(); 30 | change_directory::cd(path.as_path(), context)?; 31 | } else { 32 | match entry.file_path().extension() { 33 | Some(s) => { 34 | let s = s.to_string_lossy(); 35 | if s.as_ref().starts_with("m3u") { 36 | let cwd = context.tab_context_ref().curr_tab_ref().cwd().to_path_buf(); 37 | let request = ClientRequest::PlaylistOpen { 38 | cwd: Some(cwd), 39 | path: Some(entry.file_path().to_path_buf()), 40 | }; 41 | send_client_request(context, &request)?; 42 | } else { 43 | let request = ClientRequest::PlayerFilePlay { 44 | path: Some(entry.file_path().to_path_buf()), 45 | }; 46 | send_client_request(context, &request)?; 47 | } 48 | } 49 | None => { 50 | let request = ClientRequest::PlayerFilePlay { 51 | path: Some(entry.file_path().to_path_buf()), 52 | }; 53 | send_client_request(context, &request)?; 54 | } 55 | } 56 | } 57 | } 58 | Ok(()) 59 | } 60 | 61 | pub fn playlist_open(context: &mut AppContext) -> DiziResult { 62 | if let Some(index) = context 63 | .server_state_ref() 64 | .player 65 | .playlist 66 | .get_cursor_index() 67 | { 68 | let request = ClientRequest::PlaylistPlay { index: Some(index) }; 69 | send_client_request(context, &request)?; 70 | } 71 | Ok(()) 72 | } 73 | -------------------------------------------------------------------------------- /src/bin/client/commands/quit.rs: -------------------------------------------------------------------------------- 1 | use dizi::error::DiziResult; 2 | use dizi::request::client::ClientRequest; 3 | 4 | use crate::context::{AppContext, QuitType}; 5 | use crate::util::request::send_client_request; 6 | 7 | pub fn close(context: &mut AppContext) -> DiziResult { 8 | context.quit = QuitType::Normal; 9 | Ok(()) 10 | } 11 | 12 | pub fn server_quit(context: &mut AppContext) -> DiziResult { 13 | let request = ClientRequest::ServerQuit; 14 | let _ = send_client_request(context, &request); 15 | context.quit = QuitType::Server; 16 | Ok(()) 17 | } 18 | -------------------------------------------------------------------------------- /src/bin/client/commands/reload.rs: -------------------------------------------------------------------------------- 1 | use dizi::error::DiziResult; 2 | 3 | use crate::context::AppContext; 4 | use crate::history::create_dirlist_with_history; 5 | 6 | // reload only if we have a queued reload 7 | pub fn soft_reload(index: usize, context: &mut AppContext) -> std::io::Result<()> { 8 | let mut paths = Vec::with_capacity(3); 9 | if let Some(curr_tab) = context.tab_context_ref().tab_ref(index) { 10 | if let Some(curr_list) = curr_tab.curr_list_ref() { 11 | if curr_list.need_update() { 12 | paths.push(curr_list.file_path().to_path_buf()); 13 | } 14 | } 15 | if let Some(curr_list) = curr_tab.parent_list_ref() { 16 | if curr_list.need_update() { 17 | paths.push(curr_list.file_path().to_path_buf()); 18 | } 19 | } 20 | if let Some(curr_list) = curr_tab.child_list_ref() { 21 | if curr_list.need_update() { 22 | paths.push(curr_list.file_path().to_path_buf()); 23 | } 24 | } 25 | } 26 | 27 | if !paths.is_empty() { 28 | let options = context.config_ref().display_options_ref().clone(); 29 | if let Some(history) = context 30 | .tab_context_mut() 31 | .tab_mut(index) 32 | .map(|t| t.history_mut()) 33 | { 34 | for path in paths { 35 | let new_dirlist = create_dirlist_with_history(history, path.as_path(), &options)?; 36 | history.insert(path, new_dirlist); 37 | } 38 | } 39 | } 40 | Ok(()) 41 | } 42 | 43 | pub fn reload(context: &mut AppContext, index: usize) -> std::io::Result<()> { 44 | let mut paths = Vec::with_capacity(3); 45 | if let Some(curr_tab) = context.tab_context_ref().tab_ref(index) { 46 | if let Some(curr_list) = curr_tab.curr_list_ref() { 47 | paths.push(curr_list.file_path().to_path_buf()); 48 | } 49 | if let Some(curr_list) = curr_tab.parent_list_ref() { 50 | paths.push(curr_list.file_path().to_path_buf()); 51 | } 52 | if let Some(curr_list) = curr_tab.child_list_ref() { 53 | paths.push(curr_list.file_path().to_path_buf()); 54 | } 55 | } 56 | 57 | if !paths.is_empty() { 58 | let options = context.config_ref().display_options_ref().clone(); 59 | if let Some(history) = context 60 | .tab_context_mut() 61 | .tab_mut(index) 62 | .map(|t| t.history_mut()) 63 | { 64 | for path in paths { 65 | let new_dirlist = create_dirlist_with_history(history, path.as_path(), &options)?; 66 | history.insert(path, new_dirlist); 67 | } 68 | } 69 | } 70 | context 71 | .message_queue_mut() 72 | .push_success("Directory listing reloaded!".to_string()); 73 | Ok(()) 74 | } 75 | 76 | pub fn reload_dirlist(context: &mut AppContext) -> DiziResult { 77 | reload(context, context.tab_context_ref().index)?; 78 | Ok(()) 79 | } 80 | -------------------------------------------------------------------------------- /src/bin/client/commands/search.rs: -------------------------------------------------------------------------------- 1 | use dizi::error::DiziResult; 2 | 3 | use crate::context::AppContext; 4 | use crate::util::search::SearchPattern; 5 | 6 | use super::cursor_move; 7 | use super::search_glob; 8 | use super::search_string; 9 | 10 | pub fn search_next(context: &mut AppContext) -> DiziResult { 11 | if let Some(search_context) = context.get_search_context() { 12 | let index = match search_context { 13 | SearchPattern::Glob(s) => { 14 | search_glob::search_glob_fwd(context.tab_context_ref().curr_tab_ref(), s) 15 | } 16 | SearchPattern::String(s) => { 17 | search_string::search_string_fwd(context.tab_context_ref().curr_tab_ref(), s) 18 | } 19 | }; 20 | if let Some(index) = index { 21 | cursor_move::cursor_move(context, index); 22 | } 23 | } 24 | Ok(()) 25 | } 26 | 27 | pub fn search_prev(context: &mut AppContext) -> DiziResult { 28 | if let Some(search_context) = context.get_search_context() { 29 | let index = match search_context { 30 | SearchPattern::Glob(s) => { 31 | search_glob::search_glob_rev(context.tab_context_ref().curr_tab_ref(), s) 32 | } 33 | SearchPattern::String(s) => { 34 | search_string::search_string_rev(context.tab_context_ref().curr_tab_ref(), s) 35 | } 36 | }; 37 | if let Some(index) = index { 38 | cursor_move::cursor_move(context, index); 39 | } 40 | } 41 | Ok(()) 42 | } 43 | -------------------------------------------------------------------------------- /src/bin/client/commands/search_glob.rs: -------------------------------------------------------------------------------- 1 | use globset::{GlobBuilder, GlobMatcher}; 2 | 3 | use dizi::error::DiziResult; 4 | 5 | use crate::context::AppContext; 6 | use crate::tab::JoshutoTab; 7 | use crate::util::search::SearchPattern; 8 | 9 | use super::cursor_move; 10 | 11 | pub fn search_glob_fwd(curr_tab: &JoshutoTab, glob: &GlobMatcher) -> Option { 12 | let curr_list = curr_tab.curr_list_ref()?; 13 | 14 | let offset = curr_list.get_index()? + 1; 15 | let contents_len = curr_list.len(); 16 | for i in 0..contents_len { 17 | let file_name = curr_list.contents[(offset + i) % contents_len].file_name(); 18 | if glob.is_match(file_name) { 19 | return Some((offset + i) % contents_len); 20 | } 21 | } 22 | None 23 | } 24 | pub fn search_glob_rev(curr_tab: &JoshutoTab, glob: &GlobMatcher) -> Option { 25 | let curr_list = curr_tab.curr_list_ref()?; 26 | 27 | let offset = curr_list.get_index()?; 28 | let contents_len = curr_list.len(); 29 | for i in (0..contents_len).rev() { 30 | let file_name = curr_list.contents[(offset + i) % contents_len].file_name(); 31 | if glob.is_match(file_name) { 32 | return Some((offset + i) % contents_len); 33 | } 34 | } 35 | None 36 | } 37 | 38 | pub fn search_glob(context: &mut AppContext, pattern: &str) -> DiziResult { 39 | let glob = GlobBuilder::new(pattern) 40 | .case_insensitive(true) 41 | .build()? 42 | .compile_matcher(); 43 | 44 | let index = search_glob_fwd(context.tab_context_ref().curr_tab_ref(), &glob); 45 | if let Some(index) = index { 46 | cursor_move::cursor_move(context, index); 47 | } 48 | context.set_search_context(SearchPattern::Glob(glob)); 49 | Ok(()) 50 | } 51 | -------------------------------------------------------------------------------- /src/bin/client/commands/search_string.rs: -------------------------------------------------------------------------------- 1 | use dizi::error::DiziResult; 2 | 3 | use crate::context::AppContext; 4 | use crate::tab::JoshutoTab; 5 | use crate::util::search::SearchPattern; 6 | 7 | use super::cursor_move; 8 | 9 | fn _search_exact(curr_tab: &JoshutoTab, pattern: &str) -> Option { 10 | let curr_list = curr_tab.curr_list_ref()?; 11 | 12 | let contents_len = curr_list.contents.len(); 13 | for i in 0..contents_len { 14 | let file_name = curr_list.contents[i].file_name(); 15 | if file_name == pattern { 16 | return Some(i); 17 | } 18 | } 19 | None 20 | } 21 | 22 | pub fn search_exact(context: &mut AppContext, pattern: &str) -> DiziResult { 23 | let index = _search_exact(context.tab_context_ref().curr_tab_ref(), pattern); 24 | if let Some(index) = index { 25 | cursor_move::cursor_move(context, index); 26 | } 27 | Ok(()) 28 | } 29 | 30 | pub fn search_string_fwd(curr_tab: &JoshutoTab, pattern: &str) -> Option { 31 | let curr_list = curr_tab.curr_list_ref()?; 32 | 33 | let offset = curr_list.get_index()? + 1; 34 | let contents_len = curr_list.contents.len(); 35 | for i in 0..contents_len { 36 | let file_name_lower = curr_list.contents[(offset + i) % contents_len] 37 | .file_name() 38 | .to_lowercase(); 39 | if file_name_lower.contains(pattern) { 40 | return Some((offset + i) % contents_len); 41 | } 42 | } 43 | None 44 | } 45 | pub fn search_string_rev(curr_tab: &JoshutoTab, pattern: &str) -> Option { 46 | let curr_list = curr_tab.curr_list_ref()?; 47 | 48 | let offset = curr_list.get_index()?; 49 | let contents_len = curr_list.contents.len(); 50 | for i in (0..contents_len).rev() { 51 | let file_name_lower = curr_list.contents[(offset + i) % contents_len] 52 | .file_name() 53 | .to_lowercase(); 54 | if file_name_lower.contains(pattern) { 55 | return Some((offset + i) % contents_len); 56 | } 57 | } 58 | None 59 | } 60 | 61 | pub fn search_string(context: &mut AppContext, pattern: &str) -> DiziResult { 62 | let pattern = pattern.to_lowercase(); 63 | let index = search_string_fwd(context.tab_context_ref().curr_tab_ref(), pattern.as_str()); 64 | if let Some(index) = index { 65 | cursor_move::cursor_move(context, index); 66 | } 67 | context.set_search_context(SearchPattern::String(pattern)); 68 | Ok(()) 69 | } 70 | -------------------------------------------------------------------------------- /src/bin/client/commands/selection.rs: -------------------------------------------------------------------------------- 1 | use dizi::error::DiziResult; 2 | 3 | use crate::config::option::SelectOption; 4 | use crate::context::AppContext; 5 | 6 | pub fn select_files(context: &mut AppContext, pattern: &str, options: &SelectOption) -> DiziResult { 7 | if pattern.is_empty() { 8 | select_without_pattern(context, options) 9 | } else { 10 | select_with_pattern(context, pattern, options) 11 | } 12 | } 13 | 14 | fn select_without_pattern(_context: &mut AppContext, _options: &SelectOption) -> DiziResult { 15 | Ok(()) 16 | } 17 | 18 | fn select_with_pattern( 19 | _context: &mut AppContext, 20 | _pattern: &str, 21 | _options: &SelectOption, 22 | ) -> DiziResult { 23 | Ok(()) 24 | } 25 | -------------------------------------------------------------------------------- /src/bin/client/commands/show_hidden.rs: -------------------------------------------------------------------------------- 1 | use dizi::error::DiziResult; 2 | 3 | use crate::context::AppContext; 4 | use crate::history::DirectoryHistory; 5 | 6 | use super::reload; 7 | 8 | pub fn _toggle_hidden(context: &mut AppContext) { 9 | let opposite = !context.config_ref().display_options_ref().show_hidden(); 10 | context 11 | .config_mut() 12 | .display_options_mut() 13 | .set_show_hidden(opposite); 14 | 15 | for tab in context.tab_context_mut().iter_mut() { 16 | tab.history_mut().depreciate_all_entries(); 17 | if let Some(s) = tab.curr_list_mut() { 18 | s.depreciate(); 19 | } 20 | } 21 | } 22 | 23 | pub fn toggle_hidden(context: &mut AppContext) -> DiziResult { 24 | _toggle_hidden(context); 25 | reload::reload_dirlist(context) 26 | } 27 | -------------------------------------------------------------------------------- /src/bin/client/commands/sort.rs: -------------------------------------------------------------------------------- 1 | use dizi::error::DiziResult; 2 | 3 | use crate::config::option::SortType; 4 | use crate::context::AppContext; 5 | use crate::history::DirectoryHistory; 6 | 7 | use super::reload; 8 | 9 | pub fn set_sort(context: &mut AppContext, method: SortType) -> DiziResult { 10 | context 11 | .config_mut() 12 | .sort_options_mut() 13 | .set_sort_method(method); 14 | for tab in context.tab_context_mut().iter_mut() { 15 | tab.history_mut().depreciate_all_entries(); 16 | } 17 | refresh(context) 18 | } 19 | 20 | pub fn toggle_reverse(context: &mut AppContext) -> DiziResult { 21 | let reversed = !context.config_ref().sort_options_ref().reverse; 22 | context.config_mut().sort_options_mut().reverse = reversed; 23 | 24 | for tab in context.tab_context_mut().iter_mut() { 25 | tab.history_mut().depreciate_all_entries(); 26 | } 27 | refresh(context) 28 | } 29 | 30 | fn refresh(context: &mut AppContext) -> DiziResult { 31 | reload::soft_reload(context.tab_context_ref().index, context)?; 32 | Ok(()) 33 | } 34 | -------------------------------------------------------------------------------- /src/bin/client/config/general/app.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | use crate::config::option::{DisplayOption, SortOption}; 4 | use crate::config::{parse_toml_to_config, TomlConfigFile}; 5 | 6 | use super::client::{ClientConfig, ClientConfigRaw}; 7 | 8 | #[derive(Clone, Debug, Deserialize)] 9 | pub struct AppConfigRaw { 10 | #[serde(default)] 11 | pub client: ClientConfigRaw, 12 | } 13 | 14 | impl From for AppConfig { 15 | fn from(raw: AppConfigRaw) -> Self { 16 | Self { 17 | _client: ClientConfig::from(raw.client), 18 | } 19 | } 20 | } 21 | 22 | #[derive(Debug, Default, Clone)] 23 | pub struct AppConfig { 24 | _client: ClientConfig, 25 | } 26 | 27 | impl AppConfig { 28 | #[allow(dead_code)] 29 | pub fn new(client: ClientConfig) -> Self { 30 | Self { _client: client } 31 | } 32 | 33 | pub fn client_ref(&self) -> &ClientConfig { 34 | &self._client 35 | } 36 | 37 | pub fn client_mut(&mut self) -> &mut ClientConfig { 38 | &mut self._client 39 | } 40 | 41 | pub fn display_options_ref(&self) -> &DisplayOption { 42 | &self.client_ref().display_options 43 | } 44 | pub fn display_options_mut(&mut self) -> &mut DisplayOption { 45 | &mut self.client_mut().display_options 46 | } 47 | 48 | pub fn sort_options_ref(&self) -> &SortOption { 49 | self.display_options_ref().sort_options_ref() 50 | } 51 | pub fn sort_options_mut(&mut self) -> &mut SortOption { 52 | self.display_options_mut().sort_options_mut() 53 | } 54 | } 55 | 56 | impl TomlConfigFile for AppConfig { 57 | fn get_config(file_name: &str) -> Self { 58 | match parse_toml_to_config::(file_name) { 59 | Ok(s) => s, 60 | Err(e) => { 61 | eprintln!("Failed to parse client config: {}", e); 62 | Self::default() 63 | } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/bin/client/config/general/client.rs: -------------------------------------------------------------------------------- 1 | use std::convert::From; 2 | use std::path::{Path, PathBuf}; 3 | 4 | use serde::Deserialize; 5 | use shellexpand::tilde_with_context; 6 | 7 | use crate::config::option::DisplayOption; 8 | 9 | use super::display_raw::DisplayOptionRaw; 10 | 11 | #[derive(Clone, Debug, Deserialize)] 12 | pub struct ClientConfigRaw { 13 | #[serde(default)] 14 | pub socket: String, 15 | #[serde(default)] 16 | pub home_dir: Option, 17 | 18 | #[serde(default, rename = "display")] 19 | pub display_options: DisplayOptionRaw, 20 | } 21 | 22 | impl std::default::Default for ClientConfigRaw { 23 | fn default() -> Self { 24 | Self { 25 | socket: "".to_string(), 26 | home_dir: None, 27 | display_options: DisplayOptionRaw::default(), 28 | } 29 | } 30 | } 31 | 32 | impl From for ClientConfig { 33 | fn from(raw: ClientConfigRaw) -> Self { 34 | let socket = PathBuf::from(tilde_with_context(&raw.socket, dirs_next::home_dir).as_ref()); 35 | let home_dir = raw.home_dir.map(|home_dir| { 36 | PathBuf::from(tilde_with_context(&home_dir, dirs_next::home_dir).as_ref()) 37 | }); 38 | 39 | Self { 40 | socket, 41 | home_dir, 42 | display_options: DisplayOption::from(raw.display_options), 43 | } 44 | } 45 | } 46 | 47 | #[derive(Clone, Debug)] 48 | pub struct ClientConfig { 49 | pub socket: PathBuf, 50 | pub home_dir: Option, 51 | pub display_options: DisplayOption, 52 | } 53 | 54 | impl ClientConfig { 55 | pub fn socket_ref(&self) -> &Path { 56 | self.socket.as_path() 57 | } 58 | pub fn display_options_ref(&self) -> &DisplayOption { 59 | &self.display_options 60 | } 61 | } 62 | 63 | impl std::default::Default for ClientConfig { 64 | fn default() -> Self { 65 | let socket = 66 | PathBuf::from(tilde_with_context("~/dizi-server-socket", dirs_next::home_dir).as_ref()); 67 | 68 | Self { 69 | socket, 70 | home_dir: None, 71 | display_options: DisplayOption::default(), 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/bin/client/config/general/display_raw.rs: -------------------------------------------------------------------------------- 1 | use std::convert::From; 2 | 3 | use serde::Deserialize; 4 | 5 | use crate::config::option::DisplayOption; 6 | 7 | use super::sort_raw::SortOptionRaw; 8 | 9 | const fn default_scroll_offset() -> usize { 10 | 4 11 | } 12 | 13 | #[derive(Clone, Debug, Deserialize)] 14 | pub struct DisplayOptionRaw { 15 | #[serde(default = "default_scroll_offset")] 16 | scroll_offset: usize, 17 | 18 | #[serde(default)] 19 | show_hidden: bool, 20 | 21 | #[serde(default)] 22 | show_icons: bool, 23 | 24 | #[serde(default, rename = "sort")] 25 | sort_options: SortOptionRaw, 26 | } 27 | 28 | impl From for DisplayOption { 29 | fn from(raw: DisplayOptionRaw) -> Self { 30 | Self { 31 | _show_hidden: raw.show_hidden, 32 | _show_icons: raw.show_icons, 33 | _sort_options: raw.sort_options.into(), 34 | _scroll_offset: raw.scroll_offset, 35 | } 36 | } 37 | } 38 | 39 | impl std::default::Default for DisplayOptionRaw { 40 | fn default() -> Self { 41 | Self { 42 | show_hidden: false, 43 | show_icons: false, 44 | sort_options: SortOptionRaw::default(), 45 | scroll_offset: default_scroll_offset(), 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/bin/client/config/general/layout_raw.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use std::convert::From; 3 | 4 | use crate::config::option::LayoutComposition; 5 | use crate::config::{parse_json_to_config, JsonConfigFile}; 6 | 7 | #[derive(Clone, Debug, Deserialize)] 8 | #[serde(tag = "type")] 9 | pub enum LayoutCompositionRaw { 10 | #[serde(rename = "simple")] 11 | Simple { 12 | widget: String, 13 | ratio: usize, 14 | #[serde(default)] 15 | border: bool, 16 | #[serde(default)] 17 | title: bool, 18 | }, 19 | #[serde(rename = "composite")] 20 | Composite { 21 | direction: String, 22 | widgets: Vec, 23 | ratio: usize, 24 | }, 25 | } 26 | 27 | #[derive(Clone, Debug, Deserialize)] 28 | pub struct AppLayoutRaw { 29 | pub layout: LayoutCompositionRaw, 30 | } 31 | 32 | #[derive(Clone, Debug)] 33 | pub struct AppLayout { 34 | pub layout: LayoutComposition, 35 | } 36 | 37 | impl std::default::Default for AppLayout { 38 | fn default() -> Self { 39 | let layout = LayoutComposition::default(); 40 | Self { layout } 41 | } 42 | } 43 | 44 | impl From for AppLayout { 45 | fn from(raw: AppLayoutRaw) -> Self { 46 | let res = LayoutComposition::from(&raw.layout); 47 | 48 | let layout = res.unwrap_or_else(|_| LayoutComposition::default()); 49 | Self { layout } 50 | } 51 | } 52 | 53 | impl JsonConfigFile for AppLayout { 54 | fn get_config(file_name: &str) -> Self { 55 | match parse_json_to_config::(file_name) { 56 | Ok(s) => s, 57 | Err(e) => { 58 | eprintln!("Failed to parse layout config: {}", e); 59 | Self::default() 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/bin/client/config/general/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod app; 2 | pub mod client; 3 | pub mod display_raw; 4 | pub mod layout_raw; 5 | pub mod sort_raw; 6 | 7 | pub use self::app::AppConfig; 8 | 9 | pub use self::layout_raw::*; 10 | -------------------------------------------------------------------------------- /src/bin/client/config/general/sort_raw.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | use crate::config::option::{SortOption, SortType, SortTypes}; 4 | 5 | const fn default_true() -> bool { 6 | true 7 | } 8 | 9 | #[derive(Clone, Debug, Deserialize)] 10 | pub struct SortOptionRaw { 11 | #[serde(default = "default_true")] 12 | pub directories_first: bool, 13 | #[serde(default)] 14 | pub case_sensitive: bool, 15 | #[serde(default = "default_true")] 16 | pub reverse: bool, 17 | #[serde(default)] 18 | pub sort_method: Option, 19 | } 20 | 21 | impl From for SortOption { 22 | fn from(raw: SortOptionRaw) -> Self { 23 | let sort_method = match raw.sort_method.as_ref() { 24 | Some(s) => SortType::parse(s).unwrap_or(SortType::Natural), 25 | None => SortType::Natural, 26 | }; 27 | 28 | let mut sort_methods = SortTypes::default(); 29 | sort_methods.reorganize(sort_method); 30 | 31 | Self { 32 | directories_first: raw.directories_first, 33 | case_sensitive: raw.case_sensitive, 34 | reverse: raw.reverse, 35 | sort_methods, 36 | } 37 | } 38 | } 39 | 40 | impl std::default::Default for SortOptionRaw { 41 | fn default() -> Self { 42 | Self { 43 | directories_first: true, 44 | case_sensitive: false, 45 | reverse: true, 46 | sort_method: None, 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/bin/client/config/keymap/default_keymap.rs: -------------------------------------------------------------------------------- 1 | pub const DEFAULT_KEYMAP: &str = include_str!("../../../../../config/keymap.toml"); 2 | -------------------------------------------------------------------------------- /src/bin/client/config/keymap/mod.rs: -------------------------------------------------------------------------------- 1 | mod default_keymap; 2 | mod keymapping; 3 | 4 | pub use self::keymapping::*; 5 | -------------------------------------------------------------------------------- /src/bin/client/config/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod general; 2 | pub mod keymap; 3 | pub mod option; 4 | pub mod theme; 5 | 6 | pub use self::general::*; 7 | pub use self::keymap::*; 8 | pub use self::theme::*; 9 | 10 | use dizi::error::{DiziError, DiziErrorKind, DiziResult}; 11 | use serde::de::DeserializeOwned; 12 | use std::fs; 13 | use std::io; 14 | use std::path::{Path, PathBuf}; 15 | 16 | use crate::CONFIG_HIERARCHY; 17 | 18 | pub trait TomlConfigFile { 19 | fn get_config(file_name: &str) -> Self; 20 | } 21 | 22 | pub trait JsonConfigFile { 23 | fn get_config(file_name: &str) -> Self; 24 | } 25 | 26 | // searches a list of folders for a given file in order of preference 27 | pub fn search_directories

(filename: &str, directories: &[P]) -> Option 28 | where 29 | P: AsRef, 30 | { 31 | for path in directories.iter() { 32 | let filepath = path.as_ref().join(filename); 33 | if filepath.exists() { 34 | return Some(filepath); 35 | } 36 | } 37 | None 38 | } 39 | 40 | // parses a config file into its appropriate format 41 | fn parse_toml_to_config(filename: &str) -> DiziResult 42 | where 43 | T: DeserializeOwned, 44 | S: From, 45 | { 46 | match search_directories(filename, &CONFIG_HIERARCHY) { 47 | Some(file_path) => { 48 | let file_contents = fs::read_to_string(&file_path)?; 49 | let config = toml::from_str::(&file_contents)?; 50 | Ok(S::from(config)) 51 | } 52 | None => { 53 | let error_kind = io::ErrorKind::NotFound; 54 | let error = DiziError::new( 55 | DiziErrorKind::IoError(error_kind), 56 | "No config directory found".to_string(), 57 | ); 58 | Err(error) 59 | } 60 | } 61 | } 62 | 63 | // parses a config file into its appropriate format 64 | fn parse_json_to_config(filename: &str) -> DiziResult 65 | where 66 | T: DeserializeOwned, 67 | S: From, 68 | { 69 | match search_directories(filename, &CONFIG_HIERARCHY) { 70 | Some(file_path) => { 71 | let file_contents = fs::read_to_string(&file_path)?; 72 | let config = serde_json::from_str::(&file_contents)?; 73 | Ok(S::from(config)) 74 | } 75 | None => { 76 | let error_kind = io::ErrorKind::NotFound; 77 | let error = DiziError::new( 78 | DiziErrorKind::IoError(error_kind), 79 | "No config directory found".to_string(), 80 | ); 81 | Err(error) 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/bin/client/config/option/display_option.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | 3 | use crate::config::option::SortOption; 4 | 5 | #[derive(Clone, Debug)] 6 | pub struct DisplayOption { 7 | pub _show_hidden: bool, 8 | pub _show_icons: bool, 9 | pub _sort_options: SortOption, 10 | pub _scroll_offset: usize, 11 | } 12 | 13 | impl DisplayOption { 14 | pub fn show_hidden(&self) -> bool { 15 | self._show_hidden 16 | } 17 | 18 | pub fn set_show_hidden(&mut self, show_hidden: bool) { 19 | self._show_hidden = show_hidden; 20 | } 21 | 22 | pub fn scroll_offset(&self) -> usize { 23 | self._scroll_offset 24 | } 25 | 26 | pub fn show_icons(&self) -> bool { 27 | self._show_icons 28 | } 29 | 30 | pub fn sort_options_ref(&self) -> &SortOption { 31 | &self._sort_options 32 | } 33 | 34 | pub fn sort_options_mut(&mut self) -> &mut SortOption { 35 | &mut self._sort_options 36 | } 37 | 38 | pub fn filter_func(&self) -> fn(&Result) -> bool { 39 | if self.show_hidden() { 40 | no_filter 41 | } else { 42 | filter_hidden 43 | } 44 | } 45 | } 46 | 47 | impl std::default::Default for DisplayOption { 48 | fn default() -> Self { 49 | Self { 50 | _show_hidden: false, 51 | _show_icons: false, 52 | _sort_options: SortOption::default(), 53 | _scroll_offset: 4, 54 | } 55 | } 56 | } 57 | 58 | const fn no_filter(_: &Result) -> bool { 59 | true 60 | } 61 | 62 | fn filter_hidden(result: &Result) -> bool { 63 | match result { 64 | Err(_) => true, 65 | Ok(entry) => { 66 | let file_name = entry.file_name(); 67 | let lossy_string = file_name.as_os_str().to_string_lossy(); 68 | !lossy_string.starts_with('.') 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/bin/client/config/option/layout_option.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use ratatui::layout::Direction; 4 | 5 | use dizi::error::{DiziError, DiziErrorKind, DiziResult}; 6 | 7 | use crate::config::general::LayoutCompositionRaw; 8 | 9 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 10 | pub enum WidgetType { 11 | FileBrowser, 12 | MusicPlayer, 13 | Playlist, 14 | } 15 | 16 | impl FromStr for WidgetType { 17 | type Err = DiziError; 18 | fn from_str(s: &str) -> Result { 19 | match s { 20 | "file_browser" => Ok(Self::FileBrowser), 21 | "music_player" => Ok(Self::MusicPlayer), 22 | "playlist" => Ok(Self::Playlist), 23 | s => Err(DiziError::new( 24 | DiziErrorKind::ParseError, 25 | format!("Unknown widget type: '{}'", s), 26 | )), 27 | } 28 | } 29 | } 30 | 31 | #[derive(Clone, Debug)] 32 | pub enum LayoutComposition { 33 | Simple { 34 | widget: WidgetType, 35 | ratio: usize, 36 | border: bool, 37 | title: bool, 38 | }, 39 | Composite { 40 | direction: Direction, 41 | widgets: Vec, 42 | ratio: usize, 43 | }, 44 | } 45 | 46 | impl LayoutComposition { 47 | pub fn ratio(&self) -> usize { 48 | match self { 49 | LayoutComposition::Simple { ratio, .. } => *ratio, 50 | LayoutComposition::Composite { ratio, .. } => *ratio, 51 | } 52 | } 53 | pub fn from(raw: &LayoutCompositionRaw) -> DiziResult { 54 | match raw { 55 | LayoutCompositionRaw::Simple { 56 | widget, 57 | ratio, 58 | border, 59 | title, 60 | } => { 61 | let widget = WidgetType::from_str(widget)?; 62 | Ok(Self::Simple { 63 | widget, 64 | ratio: *ratio, 65 | border: *border, 66 | title: *title, 67 | }) 68 | } 69 | LayoutCompositionRaw::Composite { 70 | direction, 71 | widgets, 72 | ratio, 73 | } => { 74 | let direction = str_to_direction(direction)?; 75 | let mut new_widgets: Vec = Vec::new(); 76 | for w in widgets { 77 | let widget = LayoutComposition::from(w)?; 78 | new_widgets.push(widget); 79 | } 80 | 81 | let ratio = *ratio; 82 | Ok(Self::Composite { 83 | direction, 84 | widgets: new_widgets, 85 | ratio, 86 | }) 87 | } 88 | } 89 | } 90 | } 91 | 92 | impl std::default::Default for LayoutComposition { 93 | fn default() -> Self { 94 | LayoutComposition::Composite { 95 | direction: Direction::Horizontal, 96 | ratio: 1, 97 | widgets: vec![ 98 | LayoutComposition::Simple { 99 | widget: WidgetType::FileBrowser, 100 | ratio: 1, 101 | border: true, 102 | title: true, 103 | }, 104 | LayoutComposition::Composite { 105 | direction: Direction::Vertical, 106 | ratio: 1, 107 | widgets: vec![ 108 | LayoutComposition::Simple { 109 | widget: WidgetType::MusicPlayer, 110 | ratio: 1, 111 | border: true, 112 | title: true, 113 | }, 114 | LayoutComposition::Simple { 115 | widget: WidgetType::Playlist, 116 | ratio: 1, 117 | border: true, 118 | title: true, 119 | }, 120 | ], 121 | }, 122 | ], 123 | } 124 | } 125 | } 126 | 127 | pub fn str_to_direction(s: &str) -> DiziResult { 128 | match s { 129 | "horizontal" => Ok(Direction::Horizontal), 130 | "vertical" => Ok(Direction::Vertical), 131 | s => Err(DiziError::new( 132 | DiziErrorKind::ParseError, 133 | format!("Unknown direction: '{}'", s), 134 | )), 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/bin/client/config/option/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod display_option; 2 | pub mod layout_option; 3 | pub mod select_option; 4 | pub mod sort_option; 5 | pub mod sort_type; 6 | 7 | pub use self::display_option::*; 8 | pub use self::layout_option::*; 9 | pub use self::select_option::*; 10 | pub use self::sort_option::*; 11 | pub use self::sort_type::*; 12 | -------------------------------------------------------------------------------- /src/bin/client/config/option/select_option.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Copy, Debug)] 2 | pub struct SelectOption { 3 | pub toggle: bool, 4 | pub all: bool, 5 | pub reverse: bool, 6 | } 7 | 8 | impl std::default::Default for SelectOption { 9 | fn default() -> Self { 10 | Self { 11 | toggle: true, 12 | all: false, 13 | reverse: false, 14 | } 15 | } 16 | } 17 | 18 | impl std::fmt::Display for SelectOption { 19 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 20 | write!( 21 | f, 22 | "--toggle={} --all={} --deselect={}", 23 | self.toggle, self.all, self.reverse 24 | ) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/bin/client/config/option/sort_option.rs: -------------------------------------------------------------------------------- 1 | use std::cmp; 2 | 3 | use crate::config::option::{SortType, SortTypes}; 4 | use crate::fs::JoshutoDirEntry; 5 | 6 | #[derive(Clone, Debug)] 7 | pub struct SortOption { 8 | pub directories_first: bool, 9 | pub case_sensitive: bool, 10 | pub reverse: bool, 11 | pub sort_methods: SortTypes, 12 | } 13 | 14 | impl SortOption { 15 | pub fn set_sort_method(&mut self, method: SortType) { 16 | self.sort_methods.reorganize(method); 17 | } 18 | 19 | pub fn compare(&self, f1: &JoshutoDirEntry, f2: &JoshutoDirEntry) -> cmp::Ordering { 20 | if self.directories_first { 21 | let f1_isdir = f1.file_path().is_dir(); 22 | let f2_isdir = f2.file_path().is_dir(); 23 | 24 | if f1_isdir && !f2_isdir { 25 | return cmp::Ordering::Less; 26 | } else if !f1_isdir && f2_isdir { 27 | return cmp::Ordering::Greater; 28 | } 29 | } 30 | 31 | // let mut res = self.sort_method.cmp(f1, f2, &self); 32 | let mut res = self.sort_methods.cmp(f1, f2, self); 33 | if self.reverse { 34 | res = match res { 35 | cmp::Ordering::Less => cmp::Ordering::Greater, 36 | cmp::Ordering::Greater => cmp::Ordering::Less, 37 | s => s, 38 | }; 39 | }; 40 | res 41 | } 42 | } 43 | 44 | impl std::default::Default for SortOption { 45 | fn default() -> Self { 46 | SortOption { 47 | directories_first: true, 48 | case_sensitive: false, 49 | reverse: false, 50 | sort_methods: SortTypes::default(), 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/bin/client/config/option/sort_type.rs: -------------------------------------------------------------------------------- 1 | use std::cmp; 2 | use std::collections::VecDeque; 3 | use std::fs; 4 | use std::time; 5 | 6 | use serde::Deserialize; 7 | 8 | use crate::config::option::SortOption; 9 | use crate::fs::JoshutoDirEntry; 10 | 11 | #[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)] 12 | pub enum SortType { 13 | Lexical, 14 | Mtime, 15 | Natural, 16 | Size, 17 | Ext, 18 | } 19 | 20 | impl SortType { 21 | pub fn parse(s: &str) -> Option { 22 | match s { 23 | "lexical" => Some(SortType::Lexical), 24 | "mtime" => Some(SortType::Mtime), 25 | "natural" => Some(SortType::Natural), 26 | "size" => Some(SortType::Size), 27 | "ext" => Some(SortType::Ext), 28 | _ => None, 29 | } 30 | } 31 | pub const fn as_str(&self) -> &str { 32 | match *self { 33 | SortType::Lexical => "lexical", 34 | SortType::Mtime => "mtime", 35 | SortType::Natural => "natural", 36 | SortType::Size => "size", 37 | SortType::Ext => "ext", 38 | } 39 | } 40 | pub fn cmp( 41 | &self, 42 | f1: &JoshutoDirEntry, 43 | f2: &JoshutoDirEntry, 44 | sort_option: &SortOption, 45 | ) -> cmp::Ordering { 46 | match &self { 47 | SortType::Natural => natural_sort(f1, f2, sort_option), 48 | SortType::Lexical => lexical_sort(f1, f2, sort_option), 49 | SortType::Size => size_sort(f1, f2), 50 | SortType::Mtime => mtime_sort(f1, f2), 51 | SortType::Ext => ext_sort(f1, f2), 52 | } 53 | } 54 | } 55 | 56 | impl std::fmt::Display for SortType { 57 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 58 | write!(f, "{}", self.as_str()) 59 | } 60 | } 61 | 62 | #[derive(Clone, Debug)] 63 | pub struct SortTypes { 64 | pub list: VecDeque, 65 | } 66 | 67 | impl SortTypes { 68 | pub fn reorganize(&mut self, st: SortType) { 69 | self.list.push_front(st); 70 | self.list.pop_back(); 71 | } 72 | 73 | pub fn cmp( 74 | &self, 75 | f1: &JoshutoDirEntry, 76 | f2: &JoshutoDirEntry, 77 | sort_option: &SortOption, 78 | ) -> cmp::Ordering { 79 | for st in &self.list { 80 | let res = st.cmp(f1, f2, sort_option); 81 | if res != cmp::Ordering::Equal { 82 | return res; 83 | } 84 | } 85 | cmp::Ordering::Equal 86 | } 87 | } 88 | 89 | impl std::default::Default for SortTypes { 90 | fn default() -> Self { 91 | let list: VecDeque = vec![ 92 | SortType::Natural, 93 | SortType::Lexical, 94 | SortType::Size, 95 | SortType::Ext, 96 | SortType::Mtime, 97 | ] 98 | .into_iter() 99 | .collect(); 100 | 101 | Self { list } 102 | } 103 | } 104 | 105 | fn mtime_sort(file1: &JoshutoDirEntry, file2: &JoshutoDirEntry) -> cmp::Ordering { 106 | fn compare( 107 | file1: &JoshutoDirEntry, 108 | file2: &JoshutoDirEntry, 109 | ) -> Result { 110 | let f1_meta: fs::Metadata = std::fs::metadata(file1.file_path())?; 111 | let f2_meta: fs::Metadata = std::fs::metadata(file2.file_path())?; 112 | 113 | let f1_mtime: time::SystemTime = f1_meta.modified()?; 114 | let f2_mtime: time::SystemTime = f2_meta.modified()?; 115 | Ok(f1_mtime.cmp(&f2_mtime)) 116 | } 117 | compare(file1, file2).unwrap_or(cmp::Ordering::Equal) 118 | } 119 | 120 | fn size_sort(file1: &JoshutoDirEntry, file2: &JoshutoDirEntry) -> cmp::Ordering { 121 | file1.metadata.len().cmp(&file2.metadata.len()) 122 | } 123 | 124 | fn ext_sort(file1: &JoshutoDirEntry, file2: &JoshutoDirEntry) -> cmp::Ordering { 125 | let f1_ext = file1.ext().unwrap_or_default(); 126 | let f2_ext = file2.ext().unwrap_or_default(); 127 | alphanumeric_sort::compare_str(f1_ext, f2_ext) 128 | } 129 | 130 | fn lexical_sort( 131 | f1: &JoshutoDirEntry, 132 | f2: &JoshutoDirEntry, 133 | sort_option: &SortOption, 134 | ) -> cmp::Ordering { 135 | let f1_name = f1.file_name(); 136 | let f2_name = f2.file_name(); 137 | if sort_option.case_sensitive { 138 | f1_name.cmp(f2_name) 139 | } else { 140 | let f1_name = f1_name.to_lowercase(); 141 | let f2_name = f2_name.to_lowercase(); 142 | f1_name.cmp(&f2_name) 143 | } 144 | } 145 | 146 | fn natural_sort( 147 | f1: &JoshutoDirEntry, 148 | f2: &JoshutoDirEntry, 149 | sort_option: &SortOption, 150 | ) -> cmp::Ordering { 151 | let f1_name = f1.file_name(); 152 | let f2_name = f2.file_name(); 153 | if sort_option.case_sensitive { 154 | alphanumeric_sort::compare_str(f1_name, f2_name) 155 | } else { 156 | let f1_name = f1_name.to_lowercase(); 157 | let f2_name = f2_name.to_lowercase(); 158 | alphanumeric_sort::compare_str(f1_name, f2_name) 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/bin/client/config/theme/app_theme.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use std::collections::HashMap; 3 | 4 | use dizi::error::DiziResult; 5 | 6 | use super::DEFAULT_CONFIG_FILE_PATH; 7 | use super::{AppStyle, AppStyleRaw}; 8 | use crate::config::{parse_toml_to_config, TomlConfigFile}; 9 | 10 | #[derive(Clone, Debug, Default, Deserialize)] 11 | pub struct AppThemeRaw { 12 | #[serde(default)] 13 | pub playing: AppStyleRaw, 14 | #[serde(default)] 15 | pub playlist: AppStyleRaw, 16 | 17 | #[serde(default)] 18 | pub regular: AppStyleRaw, 19 | #[serde(default)] 20 | pub directory: AppStyleRaw, 21 | #[serde(default)] 22 | pub executable: AppStyleRaw, 23 | #[serde(default)] 24 | pub link: AppStyleRaw, 25 | #[serde(default)] 26 | pub link_invalid: AppStyleRaw, 27 | #[serde(default)] 28 | pub socket: AppStyleRaw, 29 | #[serde(default)] 30 | pub ext: HashMap, 31 | } 32 | 33 | #[derive(Clone, Debug)] 34 | pub struct AppTheme { 35 | pub playing: AppStyle, 36 | pub playlist: AppStyle, 37 | 38 | pub regular: AppStyle, 39 | pub directory: AppStyle, 40 | pub executable: AppStyle, 41 | pub link: AppStyle, 42 | pub link_invalid: AppStyle, 43 | pub socket: AppStyle, 44 | pub ext: HashMap, 45 | } 46 | 47 | impl From for AppTheme { 48 | fn from(raw: AppThemeRaw) -> Self { 49 | let playing = raw.playing.to_style_theme(); 50 | let playlist = raw.playlist.to_style_theme(); 51 | 52 | let executable = raw.executable.to_style_theme(); 53 | let regular = raw.regular.to_style_theme(); 54 | let directory = raw.directory.to_style_theme(); 55 | let link = raw.link.to_style_theme(); 56 | let link_invalid = raw.link_invalid.to_style_theme(); 57 | let socket = raw.socket.to_style_theme(); 58 | let ext: HashMap = raw 59 | .ext 60 | .iter() 61 | .map(|(k, v)| { 62 | let style = v.to_style_theme(); 63 | (k.clone(), style) 64 | }) 65 | .collect(); 66 | 67 | Self { 68 | playing, 69 | playlist, 70 | 71 | executable, 72 | regular, 73 | directory, 74 | link, 75 | link_invalid, 76 | socket, 77 | ext, 78 | } 79 | } 80 | } 81 | 82 | impl AppTheme { 83 | pub fn default_res() -> DiziResult { 84 | let raw: AppThemeRaw = toml::from_str(DEFAULT_CONFIG_FILE_PATH)?; 85 | Ok(Self::from(raw)) 86 | } 87 | } 88 | 89 | impl TomlConfigFile for AppTheme { 90 | fn get_config(file_name: &str) -> Self { 91 | match parse_toml_to_config::(file_name) { 92 | Ok(s) => s, 93 | Err(e) => { 94 | eprintln!("Failed to parse theme config: {}", e); 95 | Self::default() 96 | } 97 | } 98 | } 99 | } 100 | 101 | impl std::default::Default for AppTheme { 102 | fn default() -> Self { 103 | // This should not fail. 104 | // If it fails then there is a (syntax) error in the default config file 105 | Self::default_res().unwrap() 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/bin/client/config/theme/mod.rs: -------------------------------------------------------------------------------- 1 | mod app_theme; 2 | mod style; 3 | 4 | pub use self::app_theme::AppTheme; 5 | pub use self::style::*; 6 | 7 | const DEFAULT_CONFIG_FILE_PATH: &str = include_str!("../../../../../config/theme.toml"); 8 | -------------------------------------------------------------------------------- /src/bin/client/config/theme/style.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | use ratatui::style; 4 | 5 | const fn default_color() -> style::Color { 6 | style::Color::Reset 7 | } 8 | 9 | #[derive(Clone, Debug, Deserialize)] 10 | pub struct AppStyleRaw { 11 | #[serde(default)] 12 | pub fg: String, 13 | #[serde(default)] 14 | pub bg: String, 15 | #[serde(default)] 16 | pub bold: bool, 17 | #[serde(default)] 18 | pub underline: bool, 19 | #[serde(default)] 20 | pub invert: bool, 21 | } 22 | 23 | impl AppStyleRaw { 24 | pub fn to_style_theme(&self) -> AppStyle { 25 | let bg = Self::str_to_color(self.bg.as_str()); 26 | let fg = Self::str_to_color(self.fg.as_str()); 27 | 28 | let mut modifier = style::Modifier::empty(); 29 | if self.bold { 30 | modifier.insert(style::Modifier::BOLD); 31 | } 32 | if self.underline { 33 | modifier.insert(style::Modifier::UNDERLINED); 34 | } 35 | if self.invert { 36 | modifier.insert(style::Modifier::REVERSED); 37 | } 38 | 39 | AppStyle::default().set_fg(fg).set_bg(bg).insert(modifier) 40 | } 41 | 42 | pub fn str_to_color(s: &str) -> style::Color { 43 | match s { 44 | "black" => style::Color::Black, 45 | "red" => style::Color::Red, 46 | "green" => style::Color::Green, 47 | "yellow" => style::Color::Yellow, 48 | "blue" => style::Color::Blue, 49 | "magenta" => style::Color::Magenta, 50 | "cyan" => style::Color::Cyan, 51 | "gray" => style::Color::Gray, 52 | "dark_gray" => style::Color::DarkGray, 53 | "light_red" => style::Color::LightRed, 54 | "light_green" => style::Color::LightGreen, 55 | "light_yellow" => style::Color::LightYellow, 56 | "light_blue" => style::Color::LightBlue, 57 | "light_magenta" => style::Color::LightMagenta, 58 | "light_cyan" => style::Color::LightCyan, 59 | "white" => style::Color::White, 60 | "reset" => style::Color::Reset, 61 | _s => style::Color::Reset, 62 | } 63 | } 64 | } 65 | 66 | impl std::default::Default for AppStyleRaw { 67 | fn default() -> Self { 68 | Self { 69 | bg: "".to_string(), 70 | fg: "".to_string(), 71 | bold: false, 72 | underline: false, 73 | invert: false, 74 | } 75 | } 76 | } 77 | 78 | #[derive(Clone, Debug)] 79 | pub struct AppStyle { 80 | pub fg: style::Color, 81 | pub bg: style::Color, 82 | pub modifier: style::Modifier, 83 | } 84 | 85 | impl AppStyle { 86 | pub fn set_bg(mut self, bg: style::Color) -> Self { 87 | self.bg = bg; 88 | self 89 | } 90 | pub fn set_fg(mut self, fg: style::Color) -> Self { 91 | self.fg = fg; 92 | self 93 | } 94 | 95 | pub fn insert(mut self, modifier: style::Modifier) -> Self { 96 | self.modifier.insert(modifier); 97 | self 98 | } 99 | } 100 | 101 | impl std::default::Default for AppStyle { 102 | fn default() -> Self { 103 | Self { 104 | fg: default_color(), 105 | bg: default_color(), 106 | modifier: style::Modifier::empty(), 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/bin/client/context/app_context.rs: -------------------------------------------------------------------------------- 1 | use ratatui::layout::Rect; 2 | use std::io; 3 | use std::os::unix::net::UnixStream; 4 | use std::path::PathBuf; 5 | use std::sync::mpsc; 6 | 7 | use dizi::utils; 8 | 9 | use crate::config; 10 | use crate::config::option::WidgetType; 11 | use crate::context::{CommandLineContext, MessageQueue, ServerState, TabContext}; 12 | use crate::event::{AppEvent, Events}; 13 | use crate::util::search::SearchPattern; 14 | 15 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 16 | pub enum QuitType { 17 | DoNot, 18 | Normal, 19 | Server, 20 | } 21 | 22 | #[derive(Clone, Debug, PartialEq, Eq)] 23 | pub struct UiContext { 24 | pub layout: Vec, 25 | } 26 | 27 | pub struct AppContext { 28 | pub quit: QuitType, 29 | // event loop querying 30 | pub events: Events, 31 | // server unix socket 32 | pub stream: UnixStream, 33 | pub view_widget: WidgetType, 34 | // app config 35 | config: config::AppConfig, 36 | 37 | // context related to tabs 38 | tab_context: TabContext, 39 | 40 | commandline_context: CommandLineContext, 41 | // user interface context; data which is input to both, the UI rendering and the app state 42 | ui_context: UiContext, 43 | // context related to searching 44 | search_context: Option, 45 | // message queue for displaying messages 46 | message_queue: MessageQueue, 47 | // server state 48 | server_state: ServerState, 49 | } 50 | 51 | impl AppContext { 52 | pub fn new(config: config::AppConfig, _cwd: PathBuf, stream: UnixStream) -> Self { 53 | let events = Events::new(); 54 | 55 | let mut commandline_context = CommandLineContext::new(); 56 | commandline_context.history_mut().set_max_len(20); 57 | 58 | Self { 59 | quit: QuitType::DoNot, 60 | config, 61 | stream, 62 | view_widget: WidgetType::FileBrowser, 63 | events, 64 | commandline_context, 65 | search_context: None, 66 | tab_context: TabContext::new(), 67 | ui_context: UiContext { layout: vec![] }, 68 | message_queue: MessageQueue::new(), 69 | server_state: ServerState::new(), 70 | } 71 | } 72 | 73 | pub fn clone_stream(&self) -> io::Result { 74 | self.stream.try_clone() 75 | } 76 | 77 | pub fn flush_stream(&mut self) -> io::Result<()> { 78 | utils::flush(&mut self.stream) 79 | } 80 | 81 | // event related 82 | pub fn poll_event(&self) -> Result { 83 | self.events.next() 84 | } 85 | pub fn flush_event(&self) { 86 | self.events.flush(); 87 | } 88 | pub fn clone_event_tx(&self) -> mpsc::Sender { 89 | self.events.event_tx.clone() 90 | } 91 | 92 | pub fn config_ref(&self) -> &config::AppConfig { 93 | &self.config 94 | } 95 | pub fn config_mut(&mut self) -> &mut config::AppConfig { 96 | &mut self.config 97 | } 98 | 99 | pub fn message_queue_ref(&self) -> &MessageQueue { 100 | &self.message_queue 101 | } 102 | pub fn message_queue_mut(&mut self) -> &mut MessageQueue { 103 | &mut self.message_queue 104 | } 105 | 106 | pub fn server_state_ref(&self) -> &ServerState { 107 | &self.server_state 108 | } 109 | pub fn server_state_mut(&mut self) -> &mut ServerState { 110 | &mut self.server_state 111 | } 112 | 113 | pub fn tab_context_ref(&self) -> &TabContext { 114 | &self.tab_context 115 | } 116 | pub fn tab_context_mut(&mut self) -> &mut TabContext { 117 | &mut self.tab_context 118 | } 119 | 120 | pub fn get_search_context(&self) -> Option<&SearchPattern> { 121 | self.search_context.as_ref() 122 | } 123 | pub fn set_search_context(&mut self, pattern: SearchPattern) { 124 | self.search_context = Some(pattern); 125 | } 126 | 127 | pub fn ui_context_ref(&self) -> &UiContext { 128 | &self.ui_context 129 | } 130 | pub fn ui_context_mut(&mut self) -> &mut UiContext { 131 | &mut self.ui_context 132 | } 133 | 134 | pub fn commandline_context_ref(&self) -> &CommandLineContext { 135 | &self.commandline_context 136 | } 137 | pub fn commandline_context_mut(&mut self) -> &mut CommandLineContext { 138 | &mut self.commandline_context 139 | } 140 | 141 | pub fn get_view_widget(&self) -> WidgetType { 142 | self.view_widget 143 | } 144 | pub fn set_view_widget(&mut self, widget: WidgetType) { 145 | self.view_widget = widget; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/bin/client/context/commandline_context.rs: -------------------------------------------------------------------------------- 1 | use rustyline::history; 2 | 3 | pub struct CommandLineContext { 4 | history: history::History, 5 | } 6 | 7 | impl std::default::Default for CommandLineContext { 8 | fn default() -> Self { 9 | Self { 10 | history: history::History::new(), 11 | } 12 | } 13 | } 14 | 15 | impl CommandLineContext { 16 | pub fn new() -> Self { 17 | Self::default() 18 | } 19 | 20 | pub fn history_ref(&self) -> &history::History { 21 | &self.history 22 | } 23 | pub fn history_mut(&mut self) -> &mut history::History { 24 | &mut self.history 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/bin/client/context/message_queue.rs: -------------------------------------------------------------------------------- 1 | use std::collections::VecDeque; 2 | 3 | use ratatui::style::{Color, Style}; 4 | 5 | #[derive(Clone, Debug, Default)] 6 | pub struct Message { 7 | pub content: String, 8 | pub style: Style, 9 | } 10 | 11 | impl Message { 12 | pub fn new(content: String, style: Style) -> Self { 13 | Self { content, style } 14 | } 15 | } 16 | 17 | #[derive(Clone, Debug, Default)] 18 | pub struct MessageQueue { 19 | contents: VecDeque, 20 | } 21 | 22 | impl MessageQueue { 23 | pub fn new() -> Self { 24 | Self::default() 25 | } 26 | 27 | pub fn push_success(&mut self, msg: String) { 28 | let message = Message::new(msg, Style::default().fg(Color::Green)); 29 | self.push_msg(message); 30 | } 31 | 32 | pub fn push_info(&mut self, msg: String) { 33 | let message = Message::new(msg, Style::default().fg(Color::Yellow)); 34 | self.push_msg(message); 35 | } 36 | 37 | pub fn push_error(&mut self, msg: String) { 38 | let message = Message::new(msg, Style::default().fg(Color::Red)); 39 | self.push_msg(message); 40 | } 41 | 42 | fn push_msg(&mut self, msg: Message) { 43 | self.contents.push_back(msg); 44 | } 45 | 46 | pub fn pop_front(&mut self) -> Option { 47 | self.contents.pop_front() 48 | } 49 | 50 | pub fn current_message(&self) -> Option<&Message> { 51 | self.contents.front() 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/bin/client/context/mod.rs: -------------------------------------------------------------------------------- 1 | mod app_context; 2 | mod commandline_context; 3 | mod message_queue; 4 | mod server_state; 5 | mod tab_context; 6 | 7 | pub use self::app_context::*; 8 | pub use self::commandline_context::*; 9 | pub use self::message_queue::*; 10 | pub use self::server_state::*; 11 | pub use self::tab_context::*; 12 | -------------------------------------------------------------------------------- /src/bin/client/context/server_state.rs: -------------------------------------------------------------------------------- 1 | use dizi::player::PlayerState; 2 | 3 | #[derive(Clone, Debug)] 4 | pub struct ServerState { 5 | pub player: PlayerState, 6 | } 7 | 8 | impl ServerState { 9 | pub fn new() -> Self { 10 | Self { 11 | player: PlayerState::new(), 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/bin/client/context/tab_context.rs: -------------------------------------------------------------------------------- 1 | use std::slice::IterMut; 2 | 3 | use crate::tab::JoshutoTab; 4 | 5 | #[derive(Debug, Default)] 6 | pub struct TabContext { 7 | pub index: usize, 8 | tabs: Vec, 9 | } 10 | 11 | impl TabContext { 12 | pub fn new() -> Self { 13 | Self::default() 14 | } 15 | 16 | pub fn len(&self) -> usize { 17 | self.tabs.len() 18 | } 19 | 20 | pub fn tab_ref(&self, i: usize) -> Option<&JoshutoTab> { 21 | if i >= self.tabs.len() { 22 | return None; 23 | } 24 | Some(&self.tabs[i]) 25 | } 26 | pub fn tab_mut(&mut self, i: usize) -> Option<&mut JoshutoTab> { 27 | if i >= self.tabs.len() { 28 | return None; 29 | } 30 | Some(&mut self.tabs[i]) 31 | } 32 | 33 | pub fn curr_tab_ref(&self) -> &JoshutoTab { 34 | &self.tabs[self.index] 35 | } 36 | pub fn curr_tab_mut(&mut self) -> &mut JoshutoTab { 37 | &mut self.tabs[self.index] 38 | } 39 | pub fn push_tab(&mut self, tab: JoshutoTab) { 40 | self.tabs.push(tab); 41 | self.index = self.tabs.len() - 1; 42 | } 43 | pub fn pop_tab(&mut self, index: usize) -> JoshutoTab { 44 | self.tabs.remove(index) 45 | } 46 | 47 | pub fn iter_mut(&mut self) -> IterMut { 48 | self.tabs.iter_mut() 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/bin/client/event/app_event.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use std::path; 3 | use std::sync::mpsc; 4 | use std::thread; 5 | 6 | use signal_hook::consts::signal; 7 | use signal_hook::iterator::exfiltrator::SignalOnly; 8 | use signal_hook::iterator::SignalsInfo; 9 | 10 | use termion::event::Event; 11 | use termion::input::TermRead; 12 | 13 | use crate::fs::JoshutoDirList; 14 | 15 | #[derive(Debug)] 16 | pub enum AppEvent { 17 | Termion(Event), 18 | // preview thread events 19 | PreviewDir { 20 | path: path::PathBuf, 21 | res: Box>, 22 | }, 23 | Signal(i32), 24 | Server(String), 25 | } 26 | 27 | #[derive(Debug, Default, Clone, Copy)] 28 | pub struct Config {} 29 | 30 | pub struct Events { 31 | pub event_tx: mpsc::Sender, 32 | event_rx: mpsc::Receiver, 33 | pub input_tx: mpsc::Sender<()>, 34 | } 35 | 36 | impl Events { 37 | pub fn new() -> Self { 38 | Events::with_config() 39 | } 40 | 41 | pub fn with_config() -> Self { 42 | let (input_tx, input_rx) = mpsc::channel(); 43 | let (event_tx, event_rx) = mpsc::channel(); 44 | 45 | // edge case that starts off the input thread 46 | let _ = input_tx.send(()); 47 | 48 | // signal thread 49 | let event_tx2 = event_tx.clone(); 50 | let _ = thread::spawn(move || { 51 | let sigs = vec![signal::SIGWINCH]; 52 | let mut signals = SignalsInfo::::new(&sigs).unwrap(); 53 | for signal in &mut signals { 54 | if let Err(e) = event_tx2.send(AppEvent::Signal(signal)) { 55 | eprintln!("Signal thread send err: {:#?}", e); 56 | return; 57 | } 58 | } 59 | }); 60 | 61 | // input thread 62 | let event_tx2 = event_tx.clone(); 63 | let _ = thread::spawn(move || { 64 | let stdin = io::stdin(); 65 | let mut events = stdin.events(); 66 | 67 | loop { 68 | let _ = input_rx.recv(); 69 | if let Some(Ok(event)) = events.next() { 70 | let _ = event_tx2.send(AppEvent::Termion(event)); 71 | } 72 | } 73 | }); 74 | 75 | Events { 76 | event_tx, 77 | event_rx, 78 | input_tx, 79 | } 80 | } 81 | 82 | // We need a next() and a flush() so we don't continuously consume 83 | // input from the console. Sometimes, other applications need to 84 | // read terminal inputs while joshuto is in the background 85 | pub fn next(&self) -> Result { 86 | let event = self.event_rx.recv()?; 87 | Ok(event) 88 | } 89 | 90 | pub fn flush(&self) { 91 | loop { 92 | if self.input_tx.send(()).is_ok() { 93 | break; 94 | } 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/bin/client/event/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod app_event; 2 | pub mod process_event; 3 | 4 | pub use self::app_event::*; 5 | -------------------------------------------------------------------------------- /src/bin/client/fs/entry.rs: -------------------------------------------------------------------------------- 1 | use std::{fs, io, path}; 2 | 3 | use crate::config::option::DisplayOption; 4 | use crate::fs::metadata::JoshutoMetadata; 5 | 6 | #[derive(Clone, Debug)] 7 | pub struct JoshutoDirEntry { 8 | pub name: String, 9 | pub ext: Option, 10 | pub path: path::PathBuf, 11 | pub metadata: JoshutoMetadata, 12 | /// Directly selected by the user, _not_ by a current visual mode selection 13 | permanent_selected: bool, 14 | /// Temporarily selected by the visual mode range 15 | visual_mode_selected: bool, 16 | _marked: bool, 17 | } 18 | 19 | impl JoshutoDirEntry { 20 | pub fn from( 21 | direntry: &fs::DirEntry, 22 | base: &path::Path, 23 | options: &DisplayOption, 24 | ) -> io::Result { 25 | let path = direntry.path().to_path_buf(); 26 | 27 | let name = direntry 28 | .path() 29 | .strip_prefix(base) 30 | .unwrap() 31 | .to_string_lossy() 32 | .to_string(); 33 | 34 | let ext = direntry 35 | .path() 36 | .extension() 37 | .and_then(|s| s.to_str()) 38 | .map(|s| s.to_string()); 39 | 40 | let metadata = JoshutoMetadata::from(&path)?; 41 | 42 | Ok(Self { 43 | name, 44 | ext, 45 | path, 46 | metadata, 47 | permanent_selected: false, 48 | visual_mode_selected: false, 49 | _marked: false, 50 | }) 51 | } 52 | 53 | pub fn file_name(&self) -> &str { 54 | self.name.as_str() 55 | } 56 | 57 | pub fn ext(&self) -> Option<&str> { 58 | self.ext.as_deref() 59 | } 60 | 61 | pub fn file_path(&self) -> &path::Path { 62 | self.path.as_path() 63 | } 64 | 65 | pub fn file_path_buf(&self) -> path::PathBuf { 66 | self.path.clone() 67 | } 68 | 69 | pub fn is_selected(&self) -> bool { 70 | self.permanent_selected || self.visual_mode_selected 71 | } 72 | 73 | pub fn is_permanent_selected(&self) -> bool { 74 | self.permanent_selected 75 | } 76 | 77 | pub fn is_visual_mode_selected(&self) -> bool { 78 | self.visual_mode_selected 79 | } 80 | 81 | pub fn set_permanent_selected(&mut self, selected: bool) { 82 | self.permanent_selected = selected; 83 | } 84 | 85 | pub fn set_visual_mode_selected(&mut self, visual_mode_selected: bool) { 86 | self.visual_mode_selected = visual_mode_selected; 87 | } 88 | } 89 | 90 | impl std::fmt::Display for JoshutoDirEntry { 91 | fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { 92 | write!(f, "{}", self.file_name()) 93 | } 94 | } 95 | 96 | impl std::convert::AsRef for JoshutoDirEntry { 97 | fn as_ref(&self) -> &str { 98 | self.file_name() 99 | } 100 | } 101 | 102 | impl std::cmp::PartialEq for JoshutoDirEntry { 103 | fn eq(&self, other: &Self) -> bool { 104 | self.file_path() == other.file_path() 105 | } 106 | } 107 | impl std::cmp::Eq for JoshutoDirEntry {} 108 | 109 | impl std::cmp::PartialOrd for JoshutoDirEntry { 110 | fn partial_cmp(&self, other: &Self) -> Option { 111 | Some(self.cmp(other)) 112 | } 113 | } 114 | 115 | impl std::cmp::Ord for JoshutoDirEntry { 116 | fn cmp(&self, other: &Self) -> std::cmp::Ordering { 117 | self.file_path().cmp(other.file_path()) 118 | } 119 | } 120 | 121 | fn get_directory_size(path: &path::Path) -> io::Result { 122 | fs::read_dir(path).map(|s| s.count()) 123 | } 124 | -------------------------------------------------------------------------------- /src/bin/client/fs/metadata.rs: -------------------------------------------------------------------------------- 1 | use std::{fs, io, path, time}; 2 | 3 | #[derive(Clone, Debug, PartialEq, Eq)] 4 | pub enum FileType { 5 | Directory, 6 | File, 7 | } 8 | 9 | impl FileType { 10 | pub fn is_dir(&self) -> bool { 11 | *self == Self::Directory 12 | } 13 | pub fn is_file(&self) -> bool { 14 | *self == Self::File 15 | } 16 | } 17 | 18 | #[derive(Clone, Debug)] 19 | pub enum LinkType { 20 | Normal, 21 | Symlink(String, bool), // link target, link validity 22 | } 23 | 24 | #[derive(Clone, Debug)] 25 | pub struct JoshutoMetadata { 26 | _len: u64, 27 | _directory_size: Option, 28 | _modified: time::SystemTime, 29 | _permissions: fs::Permissions, 30 | _file_type: FileType, 31 | _link_type: LinkType, 32 | #[cfg(unix)] 33 | pub uid: u32, 34 | #[cfg(unix)] 35 | pub gid: u32, 36 | #[cfg(unix)] 37 | pub mode: u32, 38 | } 39 | 40 | impl JoshutoMetadata { 41 | pub fn from(path: &path::Path) -> io::Result { 42 | #[cfg(unix)] 43 | use std::os::unix::fs::MetadataExt; 44 | 45 | let symlink_metadata = fs::symlink_metadata(path)?; 46 | let metadata = fs::metadata(path); 47 | let (_len, _modified, _permissions) = match metadata.as_ref() { 48 | Ok(m) => (m.len(), m.modified()?, m.permissions()), 49 | Err(_) => ( 50 | symlink_metadata.len(), 51 | symlink_metadata.modified()?, 52 | symlink_metadata.permissions(), 53 | ), 54 | }; 55 | 56 | let (_file_type, _directory_size) = match metadata.as_ref() { 57 | Ok(m) if m.file_type().is_dir() => (FileType::Directory, None), 58 | _ => (FileType::File, None), 59 | }; 60 | 61 | let _link_type = if symlink_metadata.file_type().is_symlink() { 62 | let mut link = "".to_string(); 63 | 64 | if let Ok(path) = fs::read_link(path) { 65 | if let Some(s) = path.to_str() { 66 | link = s.to_string(); 67 | } 68 | } 69 | 70 | let exists = path.exists(); 71 | LinkType::Symlink(link, exists) 72 | } else { 73 | LinkType::Normal 74 | }; 75 | 76 | #[cfg(unix)] 77 | let uid = symlink_metadata.uid(); 78 | #[cfg(unix)] 79 | let gid = symlink_metadata.gid(); 80 | #[cfg(unix)] 81 | let mode = symlink_metadata.mode(); 82 | 83 | Ok(Self { 84 | _len, 85 | _directory_size, 86 | _modified, 87 | _permissions, 88 | _file_type, 89 | _link_type, 90 | #[cfg(unix)] 91 | uid, 92 | #[cfg(unix)] 93 | gid, 94 | #[cfg(unix)] 95 | mode, 96 | }) 97 | } 98 | 99 | pub fn len(&self) -> u64 { 100 | self._len 101 | } 102 | 103 | pub fn directory_size(&self) -> Option { 104 | self._directory_size 105 | } 106 | 107 | pub fn update_directory_size(&mut self, size: usize) { 108 | self._directory_size = Some(size); 109 | } 110 | 111 | pub fn modified(&self) -> time::SystemTime { 112 | self._modified 113 | } 114 | 115 | pub fn file_type(&self) -> &FileType { 116 | &self._file_type 117 | } 118 | 119 | pub fn link_type(&self) -> &LinkType { 120 | &self._link_type 121 | } 122 | 123 | pub fn is_dir(&self) -> bool { 124 | self._file_type == FileType::Directory 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/bin/client/fs/mod.rs: -------------------------------------------------------------------------------- 1 | mod dirlist; 2 | mod entry; 3 | mod metadata; 4 | 5 | pub use self::dirlist::JoshutoDirList; 6 | pub use self::entry::JoshutoDirEntry; 7 | pub use self::metadata::{FileType, JoshutoMetadata, LinkType}; 8 | -------------------------------------------------------------------------------- /src/bin/client/key_command/commands.rs: -------------------------------------------------------------------------------- 1 | use std::path; 2 | 3 | use dizi::request::client::ClientRequest; 4 | 5 | use crate::config::option::SelectOption; 6 | use crate::config::option::SortType; 7 | 8 | #[derive(Clone, Debug)] 9 | pub enum Command { 10 | Close, 11 | 12 | ChangeDirectory(path::PathBuf), 13 | CommandLine(String, String), 14 | 15 | CursorMoveUp(usize), 16 | CursorMoveDown(usize), 17 | CursorMoveHome, 18 | CursorMoveEnd, 19 | CursorMovePageUp, 20 | CursorMovePageDown, 21 | 22 | GoToPlaying, 23 | 24 | OpenFile, 25 | ParentDirectory, 26 | 27 | ReloadDirList, 28 | 29 | SearchGlob(String), 30 | SearchString(String), 31 | SearchSkim, 32 | SearchNext, 33 | SearchPrev, 34 | 35 | ServerRequest(ClientRequest), 36 | 37 | SelectFiles(String, SelectOption), 38 | 39 | Sort(SortType), 40 | SortReverse, 41 | 42 | ToggleView, 43 | ToggleHiddenFiles, 44 | } 45 | -------------------------------------------------------------------------------- /src/bin/client/key_command/constants.rs: -------------------------------------------------------------------------------- 1 | use rustyline::completion::Pair; 2 | 3 | pub const CMD_COMMAND_LINE: &str = ":"; 4 | 5 | macro_rules! cmd_constants { 6 | ($( ($cmd_name:ident, $cmd_value:literal), )*) => { 7 | $( 8 | pub const $cmd_name: &str = $cmd_value; 9 | )* 10 | 11 | pub fn commands() -> Vec<&'static str> { 12 | vec![$($cmd_value,)*] 13 | } 14 | }; 15 | } 16 | 17 | cmd_constants![ 18 | (CMD_CLOSE, "close"), 19 | (CMD_CHANGE_DIRECTORY, "cd"), 20 | (CMD_CURSOR_MOVE_UP, "cursor_move_up"), 21 | (CMD_CURSOR_MOVE_DOWN, "cursor_move_down"), 22 | (CMD_CURSOR_MOVE_HOME, "cursor_move_home"), 23 | (CMD_CURSOR_MOVE_END, "cursor_move_end"), 24 | (CMD_CURSOR_MOVE_PAGEUP, "cursor_move_page_up"), 25 | (CMD_CURSOR_MOVE_PAGEDOWN, "cursor_move_page_down"), 26 | (CMD_GO_TO_PLAYING, "go_to_playing"), 27 | (CMD_OPEN_FILE, "open"), 28 | (CMD_PARENT_DIRECTORY, "cd .."), 29 | (CMD_RELOAD_DIRECTORY_LIST, "reload_dirlist"), 30 | (CMD_SEARCH_STRING, "search"), 31 | (CMD_SEARCH_GLOB, "search_glob"), 32 | (CMD_SEARCH_SKIM, "search_skim"), 33 | (CMD_SEARCH_NEXT, "search_next"), 34 | (CMD_SEARCH_PREV, "search_prev"), 35 | (CMD_SELECT_FILES, "select"), 36 | (CMD_SERVER_REQUEST, "server_request"), 37 | (CMD_SORT, "sort"), 38 | (CMD_SORT_REVERSE, "sort reverse"), 39 | (CMD_TOGGLE_HIDDEN, "toggle_hidden"), 40 | (CMD_TOGGLE_VIEW, "toggle_view"), 41 | ]; 42 | 43 | pub fn complete_command(partial_command: &str) -> Vec { 44 | commands() 45 | .into_iter() 46 | .filter(|command| command.starts_with(partial_command)) 47 | .map(|command| Pair { 48 | display: command.to_string(), 49 | replacement: command.to_string(), 50 | }) 51 | .collect() 52 | } 53 | -------------------------------------------------------------------------------- /src/bin/client/key_command/impl_appcommand.rs: -------------------------------------------------------------------------------- 1 | use super::constants::*; 2 | use super::{AppCommand, Command}; 3 | 4 | impl AppCommand for Command { 5 | fn command(&self) -> &'static str { 6 | match self { 7 | Self::Close => CMD_CLOSE, 8 | 9 | Self::ChangeDirectory(_) => CMD_CHANGE_DIRECTORY, 10 | Self::CommandLine(_, _) => CMD_COMMAND_LINE, 11 | 12 | Self::CursorMoveUp(_) => CMD_CURSOR_MOVE_UP, 13 | Self::CursorMoveDown(_) => CMD_CURSOR_MOVE_DOWN, 14 | Self::CursorMoveHome => CMD_CURSOR_MOVE_HOME, 15 | Self::CursorMoveEnd => CMD_CURSOR_MOVE_END, 16 | Self::CursorMovePageUp => CMD_CURSOR_MOVE_PAGEUP, 17 | Self::CursorMovePageDown => CMD_CURSOR_MOVE_PAGEDOWN, 18 | 19 | Self::GoToPlaying => CMD_GO_TO_PLAYING, 20 | 21 | Self::OpenFile => CMD_OPEN_FILE, 22 | Self::ParentDirectory => CMD_PARENT_DIRECTORY, 23 | 24 | Self::ReloadDirList => CMD_RELOAD_DIRECTORY_LIST, 25 | 26 | Self::SearchString(_) => CMD_SEARCH_STRING, 27 | Self::SearchGlob(_) => CMD_SEARCH_GLOB, 28 | Self::SearchSkim => CMD_SEARCH_SKIM, 29 | Self::SearchNext => CMD_SEARCH_NEXT, 30 | Self::SearchPrev => CMD_SEARCH_PREV, 31 | 32 | Self::SelectFiles(_, _) => CMD_SELECT_FILES, 33 | 34 | Self::Sort(_) => CMD_SORT, 35 | Self::SortReverse => CMD_SORT_REVERSE, 36 | 37 | Self::ToggleHiddenFiles => CMD_TOGGLE_HIDDEN, 38 | Self::ToggleView => CMD_TOGGLE_VIEW, 39 | 40 | Self::ServerRequest(request) => request.api_path(), 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/bin/client/key_command/impl_display.rs: -------------------------------------------------------------------------------- 1 | use super::{AppCommand, Command}; 2 | 3 | impl std::fmt::Display for Command { 4 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 5 | match self { 6 | Self::ChangeDirectory(p) => write!(f, "{} {:?}", self.command(), p), 7 | Self::CommandLine(s, p) => write!(f, "{} {} {}", self.command(), s, p), 8 | Self::CursorMoveUp(i) => write!(f, "{} {}", self.command(), i), 9 | Self::CursorMoveDown(i) => write!(f, "{} {}", self.command(), i), 10 | 11 | Self::SearchGlob(s) => write!(f, "{} {}", self.command(), s), 12 | Self::SearchString(s) => write!(f, "{} {}", self.command(), s), 13 | Self::SelectFiles(pattern, options) => { 14 | write!(f, "{} {} {}", self.command(), pattern, options) 15 | } 16 | Self::Sort(t) => write!(f, "{} {}", self.command(), t), 17 | Self::ServerRequest(request) => write!(f, "{} {}", self.command(), request.api_path()), 18 | _ => write!(f, "{}", self.command()), 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/bin/client/key_command/keybind.rs: -------------------------------------------------------------------------------- 1 | use crate::config::KeyMapping; 2 | 3 | use super::Command; 4 | 5 | #[derive(Debug)] 6 | pub enum CommandKeybind { 7 | SimpleKeybind(Command), 8 | CompositeKeybind(KeyMapping), 9 | } 10 | 11 | impl std::fmt::Display for CommandKeybind { 12 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 13 | match self { 14 | CommandKeybind::SimpleKeybind(s) => write!(f, "{}", s), 15 | CommandKeybind::CompositeKeybind(_) => write!(f, "..."), 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/bin/client/key_command/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod commands; 2 | pub mod constants; 3 | pub mod keybind; 4 | pub mod traits; 5 | 6 | mod impl_appcommand; 7 | mod impl_appexecute; 8 | mod impl_display; 9 | mod impl_from_keymap; 10 | mod impl_from_str; 11 | 12 | pub use self::commands::*; 13 | pub use self::constants::*; 14 | pub use self::keybind::*; 15 | pub use self::traits::*; 16 | -------------------------------------------------------------------------------- /src/bin/client/key_command/traits.rs: -------------------------------------------------------------------------------- 1 | use dizi::error::DiziResult; 2 | 3 | use crate::config::AppKeyMapping; 4 | use crate::context::AppContext; 5 | use crate::ui::AppBackend; 6 | 7 | pub trait AppExecute { 8 | fn execute( 9 | &self, 10 | context: &mut AppContext, 11 | backend: &mut AppBackend, 12 | keymap_t: &AppKeyMapping, 13 | ) -> DiziResult; 14 | } 15 | 16 | pub trait AppCommand: AppExecute + std::fmt::Display + std::fmt::Debug { 17 | fn command(&self) -> &'static str; 18 | } 19 | -------------------------------------------------------------------------------- /src/bin/client/preview/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod preview_default; 2 | pub mod preview_dir; 3 | -------------------------------------------------------------------------------- /src/bin/client/preview/preview_default.rs: -------------------------------------------------------------------------------- 1 | use std::path; 2 | 3 | use crate::context::AppContext; 4 | use crate::fs::JoshutoMetadata; 5 | use crate::preview::preview_dir; 6 | use crate::ui::AppBackend; 7 | 8 | pub fn load_preview_path( 9 | context: &mut AppContext, 10 | _backend: &mut AppBackend, 11 | p: path::PathBuf, 12 | metadata: JoshutoMetadata, 13 | ) { 14 | if metadata.is_dir() { 15 | let need_to_load = context 16 | .tab_context_ref() 17 | .curr_tab_ref() 18 | .history_ref() 19 | .get(p.as_path()) 20 | .map(|e| e.need_update()) 21 | .unwrap_or(true); 22 | 23 | if need_to_load { 24 | preview_dir::Background::load_preview(context, p); 25 | } 26 | } 27 | } 28 | 29 | pub fn load_preview(context: &mut AppContext, backend: &mut AppBackend) { 30 | let mut load_list = Vec::with_capacity(2); 31 | 32 | match context.tab_context_ref().curr_tab_ref().curr_list_ref() { 33 | Some(curr_list) => { 34 | if let Some(index) = curr_list.get_index() { 35 | let entry = &curr_list.contents[index]; 36 | load_list.push((entry.file_path().to_path_buf(), entry.metadata.clone())); 37 | } 38 | } 39 | None => { 40 | let cwd = context.tab_context_mut().curr_tab_mut().cwd(); 41 | if let Ok(metadata) = JoshutoMetadata::from(cwd) { 42 | load_list.push((cwd.to_path_buf(), metadata)); 43 | } 44 | } 45 | } 46 | 47 | for (path, metadata) in load_list { 48 | load_preview_path(context, backend, path, metadata); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/bin/client/preview/preview_dir.rs: -------------------------------------------------------------------------------- 1 | use std::path; 2 | use std::thread; 3 | 4 | use crate::context::AppContext; 5 | use crate::event::AppEvent; 6 | use crate::fs::JoshutoDirList; 7 | 8 | pub struct Background {} 9 | 10 | impl Background { 11 | pub fn load_preview(context: &mut AppContext, p: path::PathBuf) -> thread::JoinHandle<()> { 12 | let event_tx = context.clone_event_tx(); 13 | let options = context.config_ref().display_options_ref().clone(); 14 | 15 | thread::spawn(move || { 16 | let path_clone = p.clone(); 17 | let dir_res = JoshutoDirList::from_path(p, &options); 18 | let res = AppEvent::PreviewDir { 19 | path: path_clone, 20 | res: Box::new(dir_res), 21 | }; 22 | let _ = event_tx.send(res); 23 | }) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/bin/client/run/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod run_control; 2 | pub mod run_query; 3 | pub mod run_query_all; 4 | pub mod run_ui; 5 | 6 | pub use self::run_control::*; 7 | pub use self::run_query::*; 8 | pub use self::run_query_all::*; 9 | pub use self::run_ui::*; 10 | -------------------------------------------------------------------------------- /src/bin/client/run/run_control.rs: -------------------------------------------------------------------------------- 1 | use dizi::error::DiziResult; 2 | use dizi::request::client::ClientRequest; 3 | 4 | use crate::context::AppContext; 5 | use crate::util::request::send_client_request; 6 | use crate::CommandArgs; 7 | 8 | pub fn run_control(context: &mut AppContext, args: &CommandArgs) -> DiziResult { 9 | let request = if args.exit { 10 | Some(ClientRequest::ServerQuit) 11 | } else if args.next { 12 | Some(ClientRequest::PlayerPlayNext) 13 | } else if args.previous { 14 | Some(ClientRequest::PlayerPlayPrevious) 15 | } else if args.pause { 16 | Some(ClientRequest::PlayerPause) 17 | } else if args.resume { 18 | Some(ClientRequest::PlayerResume) 19 | } else if args.toggle_play { 20 | Some(ClientRequest::PlayerTogglePlay) 21 | } else { 22 | None 23 | }; 24 | if let Some(request) = request { 25 | send_client_request(context, &request)?; 26 | } 27 | Ok(()) 28 | } 29 | -------------------------------------------------------------------------------- /src/bin/client/run/run_query.rs: -------------------------------------------------------------------------------- 1 | use std::io::{BufRead, BufReader}; 2 | use std::thread; 3 | 4 | use dizi::error::DiziResult; 5 | use dizi::request::client::ClientRequest; 6 | use dizi::response::server::ServerBroadcastEvent; 7 | 8 | use crate::context::AppContext; 9 | use crate::event::AppEvent; 10 | use crate::util::request::send_client_request; 11 | 12 | pub fn run_query(context: &mut AppContext, query: String) -> DiziResult { 13 | // server listener 14 | { 15 | let stream = context.clone_stream()?; 16 | let event_tx = context.events.event_tx.clone(); 17 | 18 | let _ = thread::spawn(move || { 19 | let cursor = BufReader::new(stream); 20 | for line in cursor.lines().flatten() { 21 | let _ = event_tx.send(AppEvent::Server(line)); 22 | } 23 | }); 24 | 25 | // request for server state 26 | let request = ClientRequest::ServerQuery { 27 | query: query.clone(), 28 | }; 29 | send_client_request(context, &request)?; 30 | 31 | // request for server state 32 | let request = ClientRequest::PlayerState; 33 | send_client_request(context, &request)?; 34 | } 35 | 36 | loop { 37 | let event = match context.poll_event() { 38 | Ok(event) => event, 39 | Err(_) => return Ok(()), // TODO 40 | }; 41 | 42 | if let AppEvent::Server(message) = event { 43 | let server_broadcast_event: ServerBroadcastEvent = serde_json::from_str(&message)?; 44 | match server_broadcast_event { 45 | ServerBroadcastEvent::ServerQuery { query } => { 46 | println!("{}", query); 47 | break; 48 | } 49 | ServerBroadcastEvent::PlayerState { mut state } => { 50 | if !state.playlist.is_empty() { 51 | state.playlist.set_cursor_index(Some(0)); 52 | } 53 | let res = state.query(&query)?; 54 | println!("{}", res); 55 | break; 56 | } 57 | ServerBroadcastEvent::ServerError { msg } => { 58 | println!("{}", msg); 59 | break; 60 | } 61 | _ => {} 62 | } 63 | } 64 | } 65 | Ok(()) 66 | } 67 | -------------------------------------------------------------------------------- /src/bin/client/run/run_query_all.rs: -------------------------------------------------------------------------------- 1 | use std::io::{BufRead, BufReader}; 2 | use std::thread; 3 | 4 | use dizi::error::DiziResult; 5 | use dizi::request::client::ClientRequest; 6 | use dizi::response::server::ServerBroadcastEvent; 7 | 8 | use crate::context::AppContext; 9 | use crate::event::AppEvent; 10 | use crate::util::request::send_client_request; 11 | 12 | pub fn run_query_all(context: &mut AppContext) -> DiziResult { 13 | // server listener 14 | { 15 | let stream = context.clone_stream()?; 16 | let event_tx = context.clone_event_tx(); 17 | 18 | let _ = thread::spawn(move || { 19 | let cursor = BufReader::new(stream); 20 | for line in cursor.lines().flatten() { 21 | let _ = event_tx.send(AppEvent::Server(line)); 22 | } 23 | }); 24 | 25 | // request for server state 26 | let request = ClientRequest::ServerQueryAll; 27 | send_client_request(context, &request)?; 28 | 29 | // request for server state 30 | let request = ClientRequest::PlayerState; 31 | send_client_request(context, &request)?; 32 | } 33 | 34 | loop { 35 | let event = match context.poll_event() { 36 | Ok(event) => event, 37 | Err(_) => return Ok(()), // TODO 38 | }; 39 | 40 | if let AppEvent::Server(message) = event { 41 | let server_broadcast_event: ServerBroadcastEvent = serde_json::from_str(&message)?; 42 | match server_broadcast_event { 43 | ServerBroadcastEvent::ServerQueryAll { mut query_items } => { 44 | let mut items_sorted: Vec<(String, String)> = query_items.drain().collect(); 45 | items_sorted.sort(); 46 | for (key, val) in items_sorted { 47 | println!("{} = {}", key, val); 48 | } 49 | break; 50 | } 51 | ServerBroadcastEvent::PlayerState { mut state } => { 52 | if !state.playlist.is_empty() { 53 | state.playlist.set_cursor_index(Some(0)); 54 | } 55 | let mut query_items = state.query_all(); 56 | let mut items_sorted: Vec<(String, String)> = query_items.drain().collect(); 57 | items_sorted.sort(); 58 | for (key, val) in items_sorted { 59 | println!("{} = {}", key, val); 60 | } 61 | break; 62 | } 63 | ServerBroadcastEvent::ServerError { msg } => { 64 | println!("{}", msg); 65 | break; 66 | } 67 | _ => {} 68 | } 69 | } 70 | } 71 | Ok(()) 72 | } 73 | -------------------------------------------------------------------------------- /src/bin/client/tab.rs: -------------------------------------------------------------------------------- 1 | use std::path; 2 | 3 | use crate::config::option::DisplayOption; 4 | use crate::context::UiContext; 5 | use crate::fs::JoshutoDirList; 6 | use crate::history::{DirectoryHistory, JoshutoHistory}; 7 | 8 | #[derive(Debug)] 9 | pub struct JoshutoTab { 10 | history: JoshutoHistory, 11 | _cwd: path::PathBuf, 12 | // history is just a HashMap, so we have this property to store last workdir 13 | _previous_dir: Option, 14 | } 15 | 16 | impl JoshutoTab { 17 | pub fn new( 18 | cwd: path::PathBuf, 19 | ui_context: &UiContext, 20 | options: &DisplayOption, 21 | ) -> std::io::Result { 22 | let mut history = JoshutoHistory::new(); 23 | history.populate_to_root(cwd.as_path(), ui_context, options)?; 24 | 25 | Ok(Self { 26 | history, 27 | _cwd: cwd, 28 | _previous_dir: None, 29 | }) 30 | } 31 | 32 | pub fn cwd(&self) -> &path::Path { 33 | self._cwd.as_path() 34 | } 35 | 36 | pub fn set_cwd(&mut self, cwd: &path::Path) { 37 | self._previous_dir = Some(self._cwd.to_path_buf()); 38 | self._cwd = cwd.to_path_buf(); 39 | } 40 | 41 | pub fn previous_dir(&self) -> Option<&path::Path> { 42 | // This converts PathBuf to Path 43 | match &self._previous_dir { 44 | Some(path) => Some(path), 45 | None => None, 46 | } 47 | } 48 | 49 | pub fn history_ref(&self) -> &JoshutoHistory { 50 | &self.history 51 | } 52 | 53 | pub fn history_mut(&mut self) -> &mut JoshutoHistory { 54 | &mut self.history 55 | } 56 | 57 | pub fn curr_list_ref(&self) -> Option<&JoshutoDirList> { 58 | self.history.get(self.cwd()) 59 | } 60 | 61 | pub fn parent_list_ref(&self) -> Option<&JoshutoDirList> { 62 | let parent = self.cwd().parent()?; 63 | self.history.get(parent) 64 | } 65 | 66 | pub fn child_list_ref(&self) -> Option<&JoshutoDirList> { 67 | let curr_list = self.curr_list_ref()?; 68 | let index = curr_list.get_index()?; 69 | let path = curr_list.contents[index].file_path(); 70 | self.history.get(path) 71 | } 72 | 73 | pub fn curr_list_mut(&mut self) -> Option<&mut JoshutoDirList> { 74 | self.history.get_mut(self._cwd.as_path()) 75 | } 76 | 77 | pub fn parent_list_mut(&mut self) -> Option<&mut JoshutoDirList> { 78 | let parent = self._cwd.parent()?; 79 | self.history.get_mut(parent) 80 | } 81 | 82 | #[allow(dead_code)] 83 | pub fn child_list_mut(&mut self) -> Option<&mut JoshutoDirList> { 84 | let child_path = { 85 | let curr_list = self.curr_list_ref()?; 86 | let index = curr_list.get_index()?; 87 | curr_list.contents[index].file_path().to_path_buf() 88 | }; 89 | 90 | self.history.get_mut(child_path.as_path()) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/bin/client/traits/mod.rs: -------------------------------------------------------------------------------- 1 | mod to_string; 2 | 3 | pub use to_string::*; 4 | -------------------------------------------------------------------------------- /src/bin/client/traits/to_string.rs: -------------------------------------------------------------------------------- 1 | use termion::event::{Event, Key, MouseEvent}; 2 | 3 | pub trait ToString { 4 | fn to_string(&self) -> String; 5 | } 6 | 7 | impl ToString for Key { 8 | fn to_string(&self) -> String { 9 | match *self { 10 | Key::Char(c) => format!("{}", c), 11 | Key::Ctrl(c) => format!("ctrl+{}", c), 12 | Key::Left => "arrow_left".to_string(), 13 | Key::Right => "arrow_right".to_string(), 14 | Key::Up => "arrow_up".to_string(), 15 | Key::Down => "arrow_down".to_string(), 16 | Key::Backspace => "backspace".to_string(), 17 | Key::Home => "home".to_string(), 18 | Key::End => "end".to_string(), 19 | Key::PageUp => "page_up".to_string(), 20 | Key::PageDown => "page_down".to_string(), 21 | Key::BackTab => "backtab".to_string(), 22 | Key::Insert => "insert".to_string(), 23 | Key::Delete => "delete".to_string(), 24 | Key::Esc => "escape".to_string(), 25 | Key::F(i) => format!("f{}", i), 26 | k => format!("{:?}", k), 27 | } 28 | } 29 | } 30 | 31 | impl ToString for MouseEvent { 32 | fn to_string(&self) -> String { 33 | let k = *self; 34 | format!("{:?}", k) 35 | } 36 | } 37 | 38 | impl ToString for Event { 39 | fn to_string(&self) -> String { 40 | match self { 41 | Event::Key(key) => key.to_string(), 42 | Event::Mouse(mouse) => mouse.to_string(), 43 | Event::Unsupported(v) => format!("{:?}", v), 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/bin/client/ui/backend.rs: -------------------------------------------------------------------------------- 1 | use std::io::stdout; 2 | use std::io::Write; 3 | 4 | use ratatui::backend::TermionBackend; 5 | use termion::raw::{IntoRawMode, RawTerminal}; 6 | use termion::screen::AlternateScreen; 7 | 8 | use ratatui::widgets::Widget; 9 | 10 | #[cfg(feature = "mouse")] 11 | use termion::input::MouseTerminal; 12 | 13 | trait New { 14 | fn new() -> std::io::Result 15 | where 16 | Self: Sized; 17 | } 18 | 19 | #[cfg(feature = "mouse")] 20 | type Screen = MouseTerminal>>; 21 | #[cfg(feature = "mouse")] 22 | impl New for Screen { 23 | fn new() -> std::io::Result { 24 | let stdout = std::io::stdout().into_raw_mode()?; 25 | let alt_screen = MouseTerminal::from(AlternateScreen::from(stdout)); 26 | return Ok(alt_screen); 27 | } 28 | } 29 | #[cfg(not(feature = "mouse"))] 30 | type Screen = AlternateScreen>; 31 | #[cfg(not(feature = "mouse"))] 32 | impl New for Screen { 33 | fn new() -> std::io::Result { 34 | let stdout = std::io::stdout().into_raw_mode()?; 35 | let alt_screen = AlternateScreen::from(stdout); 36 | Ok(alt_screen) 37 | } 38 | } 39 | 40 | pub type AppTerminal = ratatui::Terminal>; 41 | 42 | pub struct AppBackend { 43 | pub terminal: Option, 44 | } 45 | 46 | impl AppBackend { 47 | pub fn new() -> std::io::Result { 48 | let mut alt_screen = Screen::new()?; 49 | // clears the screen of artifacts 50 | write!(alt_screen, "{}", termion::clear::All)?; 51 | 52 | let backend = TermionBackend::new(alt_screen); 53 | let mut terminal = ratatui::Terminal::new(backend)?; 54 | terminal.hide_cursor()?; 55 | Ok(Self { 56 | terminal: Some(terminal), 57 | }) 58 | } 59 | 60 | pub fn render(&mut self, widget: W) 61 | where 62 | W: Widget, 63 | { 64 | let _ = self.terminal_mut().draw(|frame| { 65 | let rect = frame.area(); 66 | frame.render_widget(widget, rect); 67 | }); 68 | } 69 | 70 | pub fn terminal_ref(&self) -> &AppTerminal { 71 | self.terminal.as_ref().unwrap() 72 | } 73 | 74 | pub fn terminal_mut(&mut self) -> &mut AppTerminal { 75 | self.terminal.as_mut().unwrap() 76 | } 77 | 78 | pub fn terminal_drop(&mut self) { 79 | let _ = self.terminal.take(); 80 | let _ = stdout().flush(); 81 | } 82 | 83 | pub fn terminal_restore(&mut self) -> std::io::Result<()> { 84 | let mut new_backend = AppBackend::new()?; 85 | std::mem::swap(&mut self.terminal, &mut new_backend.terminal); 86 | Ok(()) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/bin/client/ui/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod backend; 2 | pub mod views; 3 | pub mod widgets; 4 | 5 | pub use backend::*; 6 | 7 | #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] 8 | pub struct Rect { 9 | pub x: u16, 10 | pub y: u16, 11 | pub width: u16, 12 | pub height: u16, 13 | } 14 | -------------------------------------------------------------------------------- /src/bin/client/ui/views/mod.rs: -------------------------------------------------------------------------------- 1 | mod tui_command_menu; 2 | mod tui_folder_view; 3 | mod tui_textfield; 4 | mod tui_view; 5 | 6 | pub use self::tui_command_menu::*; 7 | pub use self::tui_folder_view::*; 8 | pub use self::tui_textfield::*; 9 | pub use self::tui_view::*; 10 | -------------------------------------------------------------------------------- /src/bin/client/ui/views/tui_command_menu.rs: -------------------------------------------------------------------------------- 1 | use std::iter::Iterator; 2 | 3 | use ratatui::buffer::Buffer; 4 | use ratatui::layout::Rect; 5 | use ratatui::widgets::{Clear, Widget}; 6 | 7 | use crate::config::KeyMapping; 8 | use crate::context::AppContext; 9 | use crate::traits::ToString; 10 | use crate::ui::views::TuiView; 11 | use crate::ui::widgets::TuiMenu; 12 | 13 | const BORDER_HEIGHT: usize = 1; 14 | const BOTTOM_MARGIN: usize = 1; 15 | 16 | pub struct TuiCommandMenu<'a> { 17 | context: &'a AppContext, 18 | keymap: &'a KeyMapping, 19 | } 20 | 21 | impl<'a> TuiCommandMenu<'a> { 22 | pub fn new(context: &'a AppContext, keymap: &'a KeyMapping) -> Self { 23 | Self { context, keymap } 24 | } 25 | } 26 | 27 | impl<'a> Widget for TuiCommandMenu<'a> { 28 | fn render(self, area: Rect, buf: &mut Buffer) { 29 | TuiView::new(self.context).render(area, buf); 30 | 31 | // draw menu 32 | let mut display_vec: Vec = self 33 | .keymap 34 | .iter() 35 | .map(|(k, v)| format!(" {} {}", k.to_string(), v)) 36 | .collect(); 37 | display_vec.sort(); 38 | let display_str: Vec<&str> = display_vec.iter().map(|v| v.as_str()).collect(); 39 | let display_str_len = display_str.len(); 40 | 41 | let y = if (area.height as usize) < display_str_len + BORDER_HEIGHT + BOTTOM_MARGIN { 42 | 0 43 | } else { 44 | area.height - (BORDER_HEIGHT + BOTTOM_MARGIN) as u16 - display_str_len as u16 45 | }; 46 | 47 | let menu_height = if display_str_len + BORDER_HEIGHT > area.height as usize { 48 | area.height 49 | } else { 50 | (display_str_len + BORDER_HEIGHT) as u16 51 | }; 52 | 53 | let menu_rect = Rect { 54 | x: 0, 55 | y, 56 | width: area.width, 57 | height: menu_height, 58 | }; 59 | 60 | Clear.render(menu_rect, buf); 61 | TuiMenu::new(&display_str).render(menu_rect, buf); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/bin/client/ui/views/tui_folder_view.rs: -------------------------------------------------------------------------------- 1 | use ratatui::buffer::Buffer; 2 | use ratatui::layout::Rect; 3 | 4 | use ratatui::widgets::Widget; 5 | 6 | use crate::context::AppContext; 7 | use crate::ui::widgets::TuiDirListDetailed; 8 | 9 | pub struct TuiFolderView<'a> { 10 | pub context: &'a AppContext, 11 | pub focused: bool, 12 | } 13 | 14 | impl<'a> TuiFolderView<'a> { 15 | pub fn new(context: &'a AppContext, focused: bool) -> Self { 16 | Self { context, focused } 17 | } 18 | } 19 | 20 | impl<'a> Widget for TuiFolderView<'a> { 21 | fn render(self, area: Rect, buf: &mut Buffer) { 22 | let curr_list = self 23 | .context 24 | .tab_context_ref() 25 | .curr_tab_ref() 26 | .curr_list_ref(); 27 | let _curr_entry = curr_list.and_then(|c| c.curr_entry_ref()); 28 | 29 | let config = self.context.config_ref(); 30 | let display_options = config.display_options_ref(); 31 | let currently_playing = self.context.server_state_ref().player.song.as_ref(); 32 | 33 | // render current view 34 | if let Some(list) = curr_list.as_ref() { 35 | TuiDirListDetailed::new(list, display_options, currently_playing, self.focused) 36 | .render(area, buf); 37 | let _rect = Rect { 38 | x: 0, 39 | y: area.height - 1, 40 | width: area.width, 41 | height: 1, 42 | }; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/bin/client/ui/widgets/mod.rs: -------------------------------------------------------------------------------- 1 | mod tui_dirlist_detailed; 2 | mod tui_footer; 3 | mod tui_menu; 4 | mod tui_player; 5 | mod tui_playlist; 6 | mod tui_prompt; 7 | mod tui_text; 8 | mod tui_topbar; 9 | 10 | pub use self::tui_dirlist_detailed::*; 11 | pub use self::tui_footer::*; 12 | pub use self::tui_menu::*; 13 | pub use self::tui_player::*; 14 | pub use self::tui_playlist::*; 15 | pub use self::tui_prompt::*; 16 | pub use self::tui_text::*; 17 | pub use self::tui_topbar::*; 18 | -------------------------------------------------------------------------------- /src/bin/client/ui/widgets/tui_footer.rs: -------------------------------------------------------------------------------- 1 | use ratatui::buffer::Buffer; 2 | use ratatui::layout::Rect; 3 | use ratatui::style::{Color, Style}; 4 | use ratatui::text::{Line, Span}; 5 | use ratatui::widgets::{Paragraph, Widget}; 6 | 7 | use dizi::player::PlayerState; 8 | 9 | pub struct TuiFooter<'a> { 10 | player_state: &'a PlayerState, 11 | } 12 | 13 | impl<'a> TuiFooter<'a> { 14 | pub fn new(player_state: &'a PlayerState) -> Self { 15 | Self { player_state } 16 | } 17 | } 18 | 19 | impl<'a> Widget for TuiFooter<'a> { 20 | fn render(self, area: Rect, buf: &mut Buffer) { 21 | let text = vec![ 22 | Span::styled( 23 | format!("Audio system: {}", self.player_state.audio_host), 24 | Style::default().fg(Color::Green), 25 | ), 26 | Span::raw(" "), 27 | Span::raw(format!( 28 | "Channels: {}", 29 | self.player_state 30 | .song 31 | .as_ref() 32 | .map(|song| song.audio_metadata()) 33 | .and_then(|metadata| metadata.channels) 34 | .map(|s| s.to_string()) 35 | .unwrap_or_else(|| "UNKNOWN".to_string()) 36 | )), 37 | Span::raw(" "), 38 | Span::raw(format!( 39 | "Sample Rate: {} Hz", 40 | self.player_state 41 | .song 42 | .as_ref() 43 | .map(|song| song.audio_metadata()) 44 | .and_then(|metadata| metadata.sample_rate) 45 | .map(|s| s.to_string()) 46 | .unwrap_or_else(|| "UNKNOWN".to_string()) 47 | )), 48 | ]; 49 | 50 | Paragraph::new(Line::from(text)).render(area, buf); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/bin/client/ui/widgets/tui_menu.rs: -------------------------------------------------------------------------------- 1 | use ratatui::buffer::Buffer; 2 | use ratatui::layout::Rect; 3 | use ratatui::style::{Color, Style}; 4 | use ratatui::widgets::{Block, Borders, Widget}; 5 | 6 | pub struct TuiMenu<'a> { 7 | options: &'a [&'a str], 8 | } 9 | 10 | impl<'a> TuiMenu<'a> { 11 | pub fn new(options: &'a [&'a str]) -> Self { 12 | Self { options } 13 | } 14 | 15 | pub fn len(&self) -> usize { 16 | self.options.len() 17 | } 18 | } 19 | 20 | impl<'a> Widget for TuiMenu<'a> { 21 | fn render(self, area: Rect, buf: &mut Buffer) { 22 | let style = Style::default().fg(Color::Reset).bg(Color::Reset); 23 | 24 | Block::default() 25 | .style(style) 26 | .borders(Borders::TOP) 27 | .render(area, buf); 28 | 29 | let text_iter = self.options.iter().chain(&[" "]); 30 | let area_x = area.x + 1; 31 | 32 | for (y, text) in (area.y + 1..area.y + area.height).zip(text_iter) { 33 | buf.set_string(area_x, y, text, style); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/bin/client/ui/widgets/tui_prompt.rs: -------------------------------------------------------------------------------- 1 | use ratatui::layout::Rect; 2 | use ratatui::style::{Color, Style}; 3 | use ratatui::text::Span; 4 | use ratatui::widgets::{Clear, Paragraph, Wrap}; 5 | use termion::event::{Event, Key}; 6 | 7 | use crate::context::AppContext; 8 | use crate::event::process_event; 9 | use crate::event::AppEvent; 10 | use crate::ui::views::TuiView; 11 | use crate::ui::AppBackend; 12 | 13 | pub struct TuiPrompt<'a> { 14 | prompt: &'a str, 15 | } 16 | 17 | impl<'a> TuiPrompt<'a> { 18 | pub fn new(prompt: &'a str) -> Self { 19 | Self { prompt } 20 | } 21 | 22 | pub fn get_key(&mut self, backend: &mut AppBackend, context: &mut AppContext) -> Key { 23 | let terminal = backend.terminal_mut(); 24 | 25 | context.flush_event(); 26 | loop { 27 | let _ = terminal.draw(|frame| { 28 | let f_size: Rect = frame.area(); 29 | if f_size.height == 0 { 30 | return; 31 | } 32 | 33 | { 34 | let mut view = TuiView::new(context); 35 | view.show_bottom_status = false; 36 | frame.render_widget(view, f_size); 37 | } 38 | 39 | let prompt_style = Style::default().fg(Color::LightYellow); 40 | 41 | let text = Span::styled(self.prompt, prompt_style); 42 | 43 | let textfield_rect = Rect { 44 | x: 0, 45 | y: f_size.height - 1, 46 | width: f_size.width, 47 | height: 1, 48 | }; 49 | 50 | frame.render_widget(Clear, textfield_rect); 51 | frame.render_widget( 52 | Paragraph::new(text).wrap(Wrap { trim: true }), 53 | textfield_rect, 54 | ); 55 | }); 56 | 57 | if let Ok(event) = context.poll_event() { 58 | match event { 59 | AppEvent::Termion(Event::Key(key)) => { 60 | return key; 61 | } 62 | AppEvent::Termion(_) => { 63 | context.flush_event(); 64 | } 65 | event => process_event::process_noninteractive(event, context), 66 | }; 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/bin/client/ui/widgets/tui_text.rs: -------------------------------------------------------------------------------- 1 | use ratatui::buffer::Buffer; 2 | use ratatui::layout::Rect; 3 | use ratatui::style::{Color, Style}; 4 | use ratatui::widgets::Widget; 5 | use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; 6 | 7 | #[derive(Clone, Debug)] 8 | pub struct LineInfo { 9 | pub start: usize, 10 | pub end: usize, 11 | pub width: usize, 12 | } 13 | 14 | pub struct TuiMultilineText<'a> { 15 | _s: &'a str, 16 | _width: usize, 17 | _lines: Vec, 18 | _style: Style, 19 | } 20 | 21 | impl<'a> TuiMultilineText<'a> { 22 | pub fn new(s: &'a str, area_width: usize) -> Self { 23 | // TODO: This is a very hacky way of doing it and I would like 24 | // to clean this up more 25 | 26 | let default_style = Style::default().fg(Color::Reset).bg(Color::Reset); 27 | 28 | let s_width = s.width(); 29 | if s_width < area_width { 30 | return Self { 31 | _s: s, 32 | _lines: vec![LineInfo { 33 | start: 0, 34 | end: s.len(), 35 | width: s_width, 36 | }], 37 | _width: area_width, 38 | _style: default_style, 39 | }; 40 | } 41 | 42 | let filter = |(i, c): (usize, char)| { 43 | let w = c.width()?; 44 | Some((i, c, w)) 45 | }; 46 | 47 | let mut lines = Vec::with_capacity(s.len() / area_width); 48 | 49 | let mut start = 0; 50 | let mut line_width = 0; 51 | for (i, _, w) in s.char_indices().filter_map(filter) { 52 | if line_width + w < area_width { 53 | line_width += w; 54 | continue; 55 | } 56 | lines.push(LineInfo { 57 | start, 58 | end: i, 59 | width: line_width, 60 | }); 61 | line_width = w; 62 | start = i; 63 | } 64 | lines.push(LineInfo { 65 | start, 66 | end: s.len(), 67 | width: s[start..s.len()].width(), 68 | }); 69 | 70 | Self { 71 | _s: s, 72 | _lines: lines, 73 | _width: area_width, 74 | _style: default_style, 75 | } 76 | } 77 | 78 | pub fn width(&self) -> usize { 79 | self._width 80 | } 81 | 82 | pub fn height(&self) -> usize { 83 | if self._lines[self._lines.len() - 1].width >= self.width() { 84 | return self.len() + 1; 85 | } 86 | self.len() 87 | } 88 | pub fn len(&self) -> usize { 89 | self._lines.len() 90 | } 91 | 92 | pub fn iter(&self) -> impl Iterator { 93 | self._lines.iter() 94 | } 95 | } 96 | 97 | impl<'a> Widget for TuiMultilineText<'a> { 98 | fn render(self, area: Rect, buf: &mut Buffer) { 99 | let area_left = area.left(); 100 | let area_top = area.top(); 101 | for (i, line_info) in self.iter().enumerate() { 102 | buf.set_string( 103 | area_left, 104 | area_top + i as u16, 105 | &self._s[line_info.start..line_info.end], 106 | self._style, 107 | ); 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/bin/client/ui/widgets/tui_topbar.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use ratatui::buffer::Buffer; 4 | use ratatui::layout::Rect; 5 | use ratatui::style::{Color, Modifier, Style}; 6 | use ratatui::text::{Line, Span}; 7 | use ratatui::widgets::{Paragraph, Widget}; 8 | 9 | pub struct TuiTopBar<'a> { 10 | path: &'a Path, 11 | } 12 | 13 | impl<'a> TuiTopBar<'a> { 14 | pub fn new(path: &'a Path) -> Self { 15 | Self { path } 16 | } 17 | } 18 | 19 | impl<'a> Widget for TuiTopBar<'a> { 20 | fn render(self, area: Rect, buf: &mut Buffer) { 21 | let path_style = Style::default() 22 | .fg(Color::LightBlue) 23 | .add_modifier(Modifier::BOLD); 24 | 25 | let mut ellipses = None; 26 | let mut curr_path_str = self.path.to_string_lossy().into_owned(); 27 | 28 | if curr_path_str.len() > area.width as usize { 29 | if let Some(s) = self.path.file_name() { 30 | curr_path_str = s.to_string_lossy().into_owned(); 31 | ellipses = Some(Span::styled("…", path_style)); 32 | } 33 | } 34 | 35 | let text = match ellipses { 36 | Some(s) => Line::from(vec![s, Span::styled(curr_path_str, path_style)]), 37 | None => Line::from(vec![Span::styled(curr_path_str, path_style)]), 38 | }; 39 | 40 | Paragraph::new(text).render(area, buf); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/bin/client/util/format.rs: -------------------------------------------------------------------------------- 1 | pub fn file_size_to_string(file_size: u64) -> String { 2 | const FILE_UNITS: [&str; 6] = ["B", "K", "M", "G", "T", "E"]; 3 | const CONV_RATE: f64 = 1024.0; 4 | let mut file_size: f64 = file_size as f64; 5 | 6 | let mut index = 0; 7 | while file_size > CONV_RATE { 8 | file_size /= CONV_RATE; 9 | index += 1; 10 | } 11 | 12 | if file_size >= 100.0 { 13 | format!("{:>4.0} {}", file_size, FILE_UNITS[index]) 14 | } else if file_size >= 10.0 { 15 | format!("{:>4.1} {}", file_size, FILE_UNITS[index]) 16 | } else { 17 | format!("{:>4.2} {}", file_size, FILE_UNITS[index]) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/bin/client/util/keyparse.rs: -------------------------------------------------------------------------------- 1 | use termion::event::{Event, Key, MouseButton, MouseEvent}; 2 | 3 | pub fn str_to_event(s: &str) -> Option { 4 | if let Some(k) = str_to_key(s) { 5 | Some(Event::Key(k)) 6 | } else { 7 | str_to_mouse(s).map(Event::Mouse) 8 | } 9 | } 10 | 11 | pub fn str_to_key(s: &str) -> Option { 12 | if s.is_empty() { 13 | return None; 14 | } 15 | 16 | let key = match s { 17 | "backspace" => Some(Key::Backspace), 18 | "backtab" => Some(Key::BackTab), 19 | "arrow_left" => Some(Key::Left), 20 | "arrow_right" => Some(Key::Right), 21 | "arrow_up" => Some(Key::Up), 22 | "arrow_down" => Some(Key::Down), 23 | "home" => Some(Key::Home), 24 | "end" => Some(Key::End), 25 | "page_up" => Some(Key::PageUp), 26 | "page_down" => Some(Key::PageDown), 27 | "delete" => Some(Key::Delete), 28 | "insert" => Some(Key::Insert), 29 | "escape" => Some(Key::Esc), 30 | "f1" => Some(Key::F(1)), 31 | "f2" => Some(Key::F(2)), 32 | "f3" => Some(Key::F(3)), 33 | "f4" => Some(Key::F(4)), 34 | "f5" => Some(Key::F(5)), 35 | "f6" => Some(Key::F(6)), 36 | "f7" => Some(Key::F(7)), 37 | "f8" => Some(Key::F(8)), 38 | "f9" => Some(Key::F(9)), 39 | "f10" => Some(Key::F(10)), 40 | "f11" => Some(Key::F(11)), 41 | "f12" => Some(Key::F(12)), 42 | _ => None, 43 | }; 44 | 45 | if key.is_some() { 46 | return key; 47 | } 48 | 49 | if s.starts_with("ctrl+") { 50 | let ch = s.chars().nth("ctrl+".len()); 51 | let key = ch.map(Key::Ctrl); 52 | return key; 53 | } else if s.starts_with("alt+") { 54 | let ch = s.chars().nth("alt+".len()); 55 | let key = ch.map(Key::Alt); 56 | return key; 57 | } else if s.len() == 1 { 58 | let ch = s.chars().next(); 59 | let key = ch.map(Key::Char); 60 | return key; 61 | } 62 | None 63 | } 64 | 65 | pub fn str_to_mouse(s: &str) -> Option { 66 | match s { 67 | "scroll_up" => Some(MouseEvent::Press(MouseButton::WheelUp, 0, 0)), 68 | "scroll_down" => Some(MouseEvent::Press(MouseButton::WheelDown, 0, 0)), 69 | _ => None, 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/bin/client/util/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "devicons")] 2 | pub mod devicons; 3 | 4 | pub mod format; 5 | pub mod keyparse; 6 | pub mod request; 7 | pub mod search; 8 | pub mod string; 9 | pub mod style; 10 | pub mod unix; 11 | -------------------------------------------------------------------------------- /src/bin/client/util/request.rs: -------------------------------------------------------------------------------- 1 | use std::io::Write; 2 | 3 | use dizi::error::DiziResult; 4 | use dizi::request::client::ClientRequest; 5 | 6 | use crate::context::AppContext; 7 | 8 | pub fn send_client_request(context: &mut AppContext, request: &ClientRequest) -> DiziResult { 9 | let json = serde_json::to_string(&request)?; 10 | 11 | context.stream.write_all(json.as_bytes())?; 12 | context.flush_stream()?; 13 | Ok(()) 14 | } 15 | -------------------------------------------------------------------------------- /src/bin/client/util/search.rs: -------------------------------------------------------------------------------- 1 | use globset::GlobMatcher; 2 | 3 | #[derive(Clone, Debug)] 4 | pub enum SearchPattern { 5 | Glob(GlobMatcher), 6 | String(String), 7 | } 8 | -------------------------------------------------------------------------------- /src/bin/client/util/string.rs: -------------------------------------------------------------------------------- 1 | use unicode_segmentation::UnicodeSegmentation; 2 | use unicode_width::UnicodeWidthStr; 3 | 4 | ///Truncates a string to width, less or equal to the specified one. 5 | /// 6 | ///In case the point of truncation falls into a full-width character, 7 | ///the returned string will be shorter than the given `width`. 8 | ///Otherwise, it will be equal. 9 | pub trait UnicodeTruncate { 10 | fn trunc(&self, width: usize) -> String; 11 | } 12 | 13 | impl UnicodeTruncate for str { 14 | #[inline] 15 | fn trunc(&self, width: usize) -> String { 16 | if self.width() <= width { 17 | String::from(self) 18 | } else { 19 | let mut length: usize = 0; 20 | let mut result = String::new(); 21 | for grapheme in self.graphemes(true) { 22 | let grapheme_length = grapheme.width(); 23 | length += grapheme_length; 24 | if length > width { 25 | break; 26 | }; 27 | result.push_str(grapheme); 28 | } 29 | result 30 | } 31 | } 32 | } 33 | 34 | #[cfg(test)] 35 | mod tests_trunc { 36 | use super::UnicodeTruncate; 37 | 38 | #[test] 39 | fn truncate_correct_despite_several_multibyte_chars() { 40 | assert_eq!(String::from("r͂o͒͜w̾").trunc(2), String::from("r͂o͒͜")); 41 | } 42 | 43 | #[test] 44 | fn truncate_at_end_returns_complete_string() { 45 | assert_eq!(String::from("r͂o͒͜w̾").trunc(3), String::from("r͂o͒͜w̾")); 46 | } 47 | 48 | #[test] 49 | fn truncate_behind_end_returns_complete_string() { 50 | assert_eq!(String::from("r͂o͒͜w̾").trunc(4), String::from("r͂o͒͜w̾")); 51 | } 52 | 53 | #[test] 54 | fn truncate_at_zero_returns_empty_string() { 55 | assert_eq!(String::from("r͂o͒͜w̾").trunc(0), String::from("")); 56 | } 57 | 58 | #[test] 59 | fn truncate_correct_despite_fullwidth_character() { 60 | assert_eq!(String::from("a🌕bc").trunc(4), String::from("a🌕b")); 61 | } 62 | 63 | #[test] 64 | fn truncate_within_fullwidth_character_truncates_before_the_character() { 65 | assert_eq!(String::from("a🌕").trunc(2), String::from("a")); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/bin/client/util/style.rs: -------------------------------------------------------------------------------- 1 | use ratatui::style::Style; 2 | 3 | use crate::fs::{FileType, JoshutoDirEntry, LinkType}; 4 | use crate::util::unix; 5 | 6 | use crate::THEME_T; 7 | 8 | pub fn playing_style() -> Style { 9 | Style::default() 10 | .fg(THEME_T.playing.fg) 11 | .bg(THEME_T.playing.bg) 12 | .add_modifier(THEME_T.playing.modifier) 13 | } 14 | 15 | pub fn playlist_style() -> Style { 16 | Style::default() 17 | .fg(THEME_T.playlist.fg) 18 | .bg(THEME_T.playlist.bg) 19 | .add_modifier(THEME_T.playlist.modifier) 20 | } 21 | 22 | pub fn entry_style(entry: &JoshutoDirEntry) -> Style { 23 | let metadata = &entry.metadata; 24 | let filetype = &metadata.file_type(); 25 | let linktype = &metadata.link_type(); 26 | 27 | match linktype { 28 | LinkType::Symlink(_, true) => Style::default() 29 | .fg(THEME_T.link.fg) 30 | .bg(THEME_T.link.bg) 31 | .add_modifier(THEME_T.link.modifier), 32 | LinkType::Symlink(_, false) => Style::default() 33 | .fg(THEME_T.link_invalid.fg) 34 | .bg(THEME_T.link_invalid.bg) 35 | .add_modifier(THEME_T.link_invalid.modifier), 36 | LinkType::Normal => match filetype { 37 | FileType::Directory => Style::default() 38 | .fg(THEME_T.directory.fg) 39 | .bg(THEME_T.directory.bg) 40 | .add_modifier(THEME_T.directory.modifier), 41 | FileType::File => file_style(entry), 42 | }, 43 | } 44 | } 45 | 46 | fn file_style(entry: &JoshutoDirEntry) -> Style { 47 | let metadata = &entry.metadata; 48 | if unix::is_executable(metadata.mode) { 49 | Style::default() 50 | .fg(THEME_T.executable.fg) 51 | .bg(THEME_T.executable.bg) 52 | .add_modifier(THEME_T.executable.modifier) 53 | } else { 54 | match entry.file_path().extension() { 55 | None => Style::default(), 56 | Some(os_str) => match os_str.to_str() { 57 | None => Style::default(), 58 | Some(s) => match THEME_T.ext.get(s) { 59 | None => Style::default(), 60 | Some(t) => Style::default().fg(t.fg).bg(t.bg).add_modifier(t.modifier), 61 | }, 62 | }, 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/bin/client/util/unix.rs: -------------------------------------------------------------------------------- 1 | pub fn is_executable(mode: u32) -> bool { 2 | const LIBC_PERMISSION_VALS: [u32; 3] = [ 3 | libc::S_IXUSR as u32, 4 | libc::S_IXGRP as u32, 5 | libc::S_IXOTH as u32, 6 | ]; 7 | 8 | LIBC_PERMISSION_VALS.iter().any(|val| mode & *val != 0) 9 | } 10 | -------------------------------------------------------------------------------- /src/bin/server/audio/device.rs: -------------------------------------------------------------------------------- 1 | pub fn get_default_host(host_id: cpal::HostId) -> cpal::Host { 2 | tracing::debug!("Available audio systems:"); 3 | for host in cpal::available_hosts() { 4 | tracing::debug!("host: {:?}", host); 5 | } 6 | cpal::host_from_id( 7 | cpal::available_hosts() 8 | .into_iter() 9 | .find(|id| *id == host_id) 10 | .unwrap(), 11 | ) 12 | .unwrap_or_else(|_| cpal::default_host()) 13 | } 14 | -------------------------------------------------------------------------------- /src/bin/server/audio/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod device; 2 | pub mod request; 3 | pub mod symphonia; 4 | -------------------------------------------------------------------------------- /src/bin/server/audio/request.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use dizi::song::DiziAudioFile; 4 | 5 | #[derive(Clone, Debug)] 6 | pub enum PlayerRequest { 7 | Play { song: DiziAudioFile, volume: f32 }, 8 | Pause, 9 | Resume, 10 | Stop, 11 | SetVolume { volume: f32 }, 12 | FastForward { offset: Duration }, 13 | Rewind { offset: Duration }, 14 | // AddListener(ServerEventSender), 15 | // ClearListeners, 16 | } 17 | -------------------------------------------------------------------------------- /src/bin/server/audio/symphonia/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod decode; 2 | pub mod player; 3 | pub mod stream; 4 | -------------------------------------------------------------------------------- /src/bin/server/audio/symphonia/player/mod.rs: -------------------------------------------------------------------------------- 1 | mod impl_audio_player; 2 | 3 | use std::path::PathBuf; 4 | use std::sync::mpsc; 5 | use std::thread::{self, JoinHandle}; 6 | 7 | use cpal::traits::HostTrait; 8 | 9 | use dizi::error::{DiziError, DiziErrorKind, DiziResult}; 10 | use dizi::player::{PlayerState, PlayerStatus}; 11 | use dizi::playlist::PlaylistType; 12 | use dizi::song::DiziAudioFile; 13 | 14 | use crate::audio::device::get_default_host; 15 | use crate::audio::request::PlayerRequest; 16 | use crate::audio::symphonia::stream::PlayerStream; 17 | use crate::config; 18 | use crate::context::PlaylistContext; 19 | use crate::events::ServerEventSender; 20 | use crate::playlist::DiziPlaylist; 21 | use crate::traits::AudioPlayer; 22 | 23 | #[derive(Debug)] 24 | pub struct SymphoniaPlayer { 25 | pub state: PlayerState, 26 | pub playlist_context: PlaylistContext, 27 | 28 | pub player_req_tx: mpsc::Sender, 29 | pub player_res_rx: mpsc::Receiver, 30 | 31 | pub _stream_handle: JoinHandle, 32 | } 33 | 34 | impl SymphoniaPlayer { 35 | pub fn new(config_t: &config::AppConfig, event_tx: ServerEventSender) -> DiziResult { 36 | let audio_host = get_default_host(config_t.server_ref().audio_system); 37 | let audio_device = audio_host.default_output_device().ok_or_else(|| { 38 | let error_msg = "Failed to get default output device"; 39 | tracing::error!("{error_msg}"); 40 | DiziError::new(DiziErrorKind::Symphonia, error_msg.to_string()) 41 | })?; 42 | 43 | let (player_req_tx, player_req_rx) = mpsc::channel(); 44 | let (player_res_tx, player_res_rx) = mpsc::channel(); 45 | 46 | let stream_handle: JoinHandle = thread::spawn(move || { 47 | let mut stream = 48 | PlayerStream::new(event_tx, player_res_tx, player_req_rx, audio_device)?; 49 | stream.listen_for_events()?; 50 | Ok(()) 51 | }); 52 | 53 | let server_config = config_t.server_ref(); 54 | let player_config = server_config.player_ref(); 55 | 56 | let playlist_context = PlaylistContext { 57 | file_playlist: DiziPlaylist::from_file( 58 | &PathBuf::from("/"), 59 | server_config.playlist_ref(), 60 | ) 61 | .unwrap_or_default(), 62 | ..Default::default() 63 | }; 64 | let state = PlayerState { 65 | next: player_config.next, 66 | repeat: player_config.repeat, 67 | shuffle: player_config.shuffle, 68 | volume: config_t.server_ref().player_ref().volume, 69 | audio_host: audio_host.id().name().to_lowercase(), 70 | ..PlayerState::default() 71 | }; 72 | 73 | Ok(Self { 74 | state, 75 | playlist_context, 76 | player_req_tx, 77 | player_res_rx, 78 | _stream_handle: stream_handle, 79 | }) 80 | } 81 | 82 | fn player_stream_req(&self) -> &mpsc::Sender { 83 | &self.player_req_tx 84 | } 85 | fn player_stream_res(&self) -> &mpsc::Receiver { 86 | &self.player_res_rx 87 | } 88 | 89 | fn play(&mut self, song: &DiziAudioFile) -> DiziResult { 90 | tracing::debug!("Song: {:#?}", song); 91 | 92 | self.player_stream_req().send(PlayerRequest::Play { 93 | song: song.clone(), 94 | volume: self.get_volume() as f32 / 100.0, 95 | })?; 96 | 97 | self.player_stream_res().recv()??; 98 | 99 | self.state.status = PlayerStatus::Playing; 100 | self.state.song = Some(song.clone()); 101 | Ok(()) 102 | } 103 | 104 | fn set_playlist_type(&mut self, playlist_type: PlaylistType) { 105 | self.playlist_context.current_playlist_type = playlist_type; 106 | self.state.playlist_status = playlist_type; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/bin/server/client.rs: -------------------------------------------------------------------------------- 1 | use std::io::{BufRead, BufReader, Write}; 2 | use std::os::unix::net::UnixStream; 3 | use std::sync::mpsc; 4 | use std::thread; 5 | 6 | use dizi::error::DiziResult; 7 | use dizi::request::client::ClientRequest; 8 | use dizi::response::server::ServerBroadcastEvent; 9 | use dizi::utils; 10 | 11 | use crate::events::{ClientRequestSender, ServerBroadcastEventReceiver}; 12 | 13 | #[derive(Clone, Debug)] 14 | pub enum ClientMessage { 15 | Client(String), 16 | Server(Box), 17 | } 18 | 19 | pub fn handle_client( 20 | uuid: uuid::Uuid, 21 | mut stream: UnixStream, 22 | client_request_tx: ClientRequestSender, 23 | server_event_rx: ServerBroadcastEventReceiver, 24 | ) -> DiziResult { 25 | let (event_tx, event_rx) = mpsc::channel(); 26 | 27 | // listen for events broadcasted by the server 28 | let event_tx_clone = event_tx.clone(); 29 | let _ = thread::spawn(move || { 30 | while let Ok(server_event) = server_event_rx.recv() { 31 | if event_tx_clone 32 | .send(ClientMessage::Server(Box::new(server_event))) 33 | .is_err() 34 | { 35 | return; 36 | } 37 | } 38 | }); 39 | 40 | let uuid_string = uuid.to_string(); 41 | 42 | // listen for requests sent by client 43 | let event_tx_clone = event_tx; 44 | let stream_clone = stream.try_clone().expect("Failed to clone UnixStream"); 45 | let _ = thread::spawn(move || { 46 | let cursor = BufReader::new(stream_clone); 47 | // keep listening for client requests 48 | for line in cursor.lines().flatten() { 49 | if event_tx_clone.send(ClientMessage::Client(line)).is_err() { 50 | return; 51 | } 52 | } 53 | 54 | let response = ClientRequest::ClientLeave { 55 | uuid: uuid.to_string(), 56 | }; 57 | let json = serde_json::to_string(&response).expect("Failed to serialize ClientRequest"); 58 | let _ = event_tx_clone.send(ClientMessage::Client(json)); 59 | }); 60 | 61 | // process events 62 | while let Ok(event) = event_rx.recv() { 63 | match event { 64 | ClientMessage::Server(event) => { 65 | process_server_event(&mut stream, &event)?; 66 | } 67 | ClientMessage::Client(line) => { 68 | if line.is_empty() { 69 | continue; 70 | } 71 | forward_client_request(&client_request_tx, &uuid_string, &line)?; 72 | } 73 | } 74 | } 75 | Ok(()) 76 | } 77 | 78 | /// Forwards client requests to the server via `ClientRequestSender` 79 | pub fn forward_client_request( 80 | client_request_tx: &ClientRequestSender, 81 | uuid: &str, 82 | line: &str, 83 | ) -> DiziResult { 84 | let request: ClientRequest = serde_json::from_str(line)?; 85 | client_request_tx.send((uuid.to_string(), request))?; 86 | Ok(()) 87 | } 88 | 89 | pub fn process_server_event(stream: &mut UnixStream, event: &ServerBroadcastEvent) -> DiziResult { 90 | let json = serde_json::to_string(&event)?; 91 | stream.write_all(json.as_bytes())?; 92 | utils::flush(stream)?; 93 | Ok(()) 94 | } 95 | -------------------------------------------------------------------------------- /src/bin/server/config/general/app.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | use crate::config::{parse_toml_to_config, TomlConfigFile}; 4 | 5 | use super::{ServerConfig, ServerConfigRaw}; 6 | 7 | #[derive(Clone, Debug, Deserialize)] 8 | pub struct AppConfigRaw { 9 | #[serde(default)] 10 | pub server: ServerConfigRaw, 11 | } 12 | 13 | #[derive(Clone, Debug, Default)] 14 | pub struct AppConfig { 15 | server: ServerConfig, 16 | } 17 | 18 | impl From for AppConfig { 19 | fn from(crude: AppConfigRaw) -> Self { 20 | Self { 21 | server: ServerConfig::from(crude.server), 22 | } 23 | } 24 | } 25 | 26 | impl AppConfig { 27 | pub fn server_ref(&self) -> &ServerConfig { 28 | &self.server 29 | } 30 | } 31 | 32 | impl TomlConfigFile for AppConfig { 33 | fn get_config(file_name: &str) -> Self { 34 | match parse_toml_to_config::(file_name) { 35 | Ok(s) => s, 36 | Err(e) => { 37 | eprintln!("Failed to parse server config: {}", e); 38 | Self::default() 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/bin/server/config/general/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod app; 2 | pub mod player; 3 | pub mod server; 4 | 5 | pub use self::app::*; 6 | pub use self::player::*; 7 | pub use self::server::*; 8 | -------------------------------------------------------------------------------- /src/bin/server/config/general/player.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | const fn default_true() -> bool { 4 | true 5 | } 6 | 7 | const fn default_volume() -> usize { 8 | 50 9 | } 10 | 11 | #[derive(Clone, Debug, Deserialize)] 12 | pub struct PlayerOptionRaw { 13 | #[serde(default)] 14 | pub shuffle: bool, 15 | #[serde(default = "default_true")] 16 | pub repeat: bool, 17 | #[serde(default = "default_true")] 18 | pub next: bool, 19 | #[serde(default = "default_volume")] 20 | pub volume: usize, 21 | } 22 | 23 | impl std::default::Default for PlayerOptionRaw { 24 | fn default() -> Self { 25 | Self { 26 | shuffle: false, 27 | repeat: true, 28 | next: true, 29 | volume: default_volume(), 30 | } 31 | } 32 | } 33 | 34 | impl From for PlayerOption { 35 | fn from(crude: PlayerOptionRaw) -> Self { 36 | Self { 37 | shuffle: crude.shuffle, 38 | repeat: crude.repeat, 39 | next: crude.next, 40 | volume: crude.volume, 41 | } 42 | } 43 | } 44 | 45 | #[derive(Clone, Debug)] 46 | pub struct PlayerOption { 47 | pub shuffle: bool, 48 | pub repeat: bool, 49 | pub next: bool, 50 | pub volume: usize, 51 | } 52 | 53 | impl std::default::Default for PlayerOption { 54 | fn default() -> Self { 55 | Self { 56 | shuffle: false, 57 | repeat: true, 58 | next: true, 59 | volume: default_volume(), 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/bin/server/config/general/server.rs: -------------------------------------------------------------------------------- 1 | use std::convert::From; 2 | use std::path::{Path, PathBuf}; 3 | 4 | use serde::Deserialize; 5 | use shellexpand::tilde_with_context; 6 | 7 | use super::{PlayerOption, PlayerOptionRaw}; 8 | 9 | fn default_socket_string() -> String { 10 | "~/dizi-server-socket".to_string() 11 | } 12 | 13 | fn default_playlist_string() -> String { 14 | "~/dizi-playlist.m3u".to_string() 15 | } 16 | 17 | fn default_socket_path() -> PathBuf { 18 | let s = default_socket_string(); 19 | PathBuf::from(tilde_with_context(&s, dirs_next::home_dir).as_ref()) 20 | } 21 | 22 | fn default_playlist_path() -> PathBuf { 23 | let s = default_playlist_string(); 24 | PathBuf::from(tilde_with_context(&s, dirs_next::home_dir).as_ref()) 25 | } 26 | 27 | fn default_audio_system() -> cpal::HostId { 28 | #[cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd"))] 29 | { 30 | cpal::HostId::Alsa 31 | } 32 | #[cfg(any(target_os = "macos", target_os = "ios"))] 33 | { 34 | cpal::HostId::CoreAudio 35 | } 36 | #[cfg(target_os = "windows")] 37 | { 38 | cpal::HostId::Asio 39 | } 40 | } 41 | 42 | fn default_audio_system_string() -> String { 43 | "alsa".to_string() 44 | } 45 | 46 | #[cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd"))] 47 | fn str_to_cpal_hostid(s: &str) -> Option { 48 | match s { 49 | "alsa" => Some(cpal::HostId::Alsa), 50 | #[cfg(feature = "jack")] 51 | "jack" => Some(cpal::HostId::Jack), 52 | _ => None, 53 | } 54 | } 55 | 56 | #[cfg(any(target_os = "macos", target_os = "ios"))] 57 | fn str_to_cpal_hostid(s: &str) -> Option { 58 | Some(cpal::HostId::CoreAudio) 59 | } 60 | 61 | #[cfg(target_os = "windows")] 62 | fn str_to_cpal_hostid(s: &str) -> Option { 63 | Some(cpal::HostId::Asio) 64 | } 65 | 66 | #[derive(Clone, Debug, Deserialize)] 67 | pub struct ServerConfigRaw { 68 | #[serde(default = "default_socket_string")] 69 | pub socket: String, 70 | #[serde(default = "default_playlist_string")] 71 | pub playlist: String, 72 | #[serde(default = "default_audio_system_string")] 73 | pub audio_system: String, 74 | #[serde(default)] 75 | pub on_song_change: Option, 76 | #[serde(default)] 77 | pub player: PlayerOptionRaw, 78 | } 79 | 80 | impl std::default::Default for ServerConfigRaw { 81 | fn default() -> Self { 82 | Self { 83 | socket: default_socket_string(), 84 | playlist: default_playlist_string(), 85 | audio_system: default_audio_system_string(), 86 | on_song_change: None, 87 | player: PlayerOptionRaw::default(), 88 | } 89 | } 90 | } 91 | 92 | #[derive(Clone, Debug)] 93 | pub struct ServerConfig { 94 | pub socket: PathBuf, 95 | pub playlist: PathBuf, 96 | pub audio_system: cpal::HostId, 97 | pub on_song_change: Option, 98 | pub player: PlayerOption, 99 | } 100 | 101 | impl ServerConfig { 102 | pub fn socket_ref(&self) -> &Path { 103 | self.socket.as_path() 104 | } 105 | pub fn playlist_ref(&self) -> &Path { 106 | self.playlist.as_path() 107 | } 108 | pub fn player_ref(&self) -> &PlayerOption { 109 | &self.player 110 | } 111 | } 112 | 113 | impl std::default::Default for ServerConfig { 114 | fn default() -> Self { 115 | Self { 116 | socket: default_socket_path(), 117 | playlist: default_playlist_path(), 118 | audio_system: default_audio_system(), 119 | on_song_change: None, 120 | player: PlayerOption::default(), 121 | } 122 | } 123 | } 124 | 125 | impl From for ServerConfig { 126 | fn from(raw: ServerConfigRaw) -> Self { 127 | let audio_system = str_to_cpal_hostid(&raw.audio_system.to_lowercase()) 128 | .unwrap_or_else(default_audio_system); 129 | 130 | let socket = tilde_with_context(&raw.socket, dirs_next::home_dir); 131 | let playlist = tilde_with_context(&raw.playlist, dirs_next::home_dir); 132 | let on_song_change = raw 133 | .on_song_change 134 | .map(|path| PathBuf::from(tilde_with_context(&path, dirs_next::home_dir).as_ref())); 135 | 136 | Self { 137 | socket: PathBuf::from(socket.as_ref()), 138 | playlist: PathBuf::from(playlist.as_ref()), 139 | audio_system, 140 | on_song_change, 141 | player: PlayerOption::from(raw.player), 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/bin/server/config/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod general; 2 | 3 | pub use self::general::*; 4 | 5 | use dizi::error::{DiziError, DiziErrorKind, DiziResult}; 6 | use serde::de::DeserializeOwned; 7 | use std::fs; 8 | use std::io; 9 | use std::path::{Path, PathBuf}; 10 | 11 | use crate::CONFIG_HIERARCHY; 12 | 13 | pub trait TomlConfigFile { 14 | fn get_config(file_name: &str) -> Self; 15 | } 16 | 17 | // searches a list of folders for a given file in order of preference 18 | pub fn search_directories

(filename: &str, directories: &[P]) -> Option 19 | where 20 | P: AsRef, 21 | { 22 | for path in directories.iter() { 23 | let filepath = path.as_ref().join(filename); 24 | if filepath.exists() { 25 | return Some(filepath); 26 | } 27 | } 28 | None 29 | } 30 | 31 | // parses a config file into its appropriate format 32 | fn parse_toml_to_config(filename: &str) -> DiziResult 33 | where 34 | T: DeserializeOwned, 35 | S: From, 36 | { 37 | match search_directories(filename, &CONFIG_HIERARCHY) { 38 | Some(file_path) => { 39 | let file_contents = fs::read_to_string(&file_path)?; 40 | let config = toml::from_str::(&file_contents)?; 41 | Ok(S::from(config)) 42 | } 43 | None => { 44 | let error_kind = io::ErrorKind::NotFound; 45 | let error = DiziError::new( 46 | DiziErrorKind::IoError(error_kind), 47 | "No config directory found".to_string(), 48 | ); 49 | Err(error) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/bin/server/context/app_context.rs: -------------------------------------------------------------------------------- 1 | use crate::audio::symphonia::player::SymphoniaPlayer; 2 | use crate::config; 3 | use crate::events::Events; 4 | 5 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 6 | pub enum QuitType { 7 | DoNot, 8 | Server, 9 | } 10 | 11 | #[derive(Debug)] 12 | pub struct AppContext { 13 | pub config: config::AppConfig, 14 | pub events: Events, 15 | pub quit: QuitType, 16 | pub player: SymphoniaPlayer, 17 | } 18 | 19 | impl AppContext { 20 | pub fn config_ref(&self) -> &config::AppConfig { 21 | &self.config 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/bin/server/context/mod.rs: -------------------------------------------------------------------------------- 1 | mod app_context; 2 | mod playlist_context; 3 | 4 | pub use app_context::*; 5 | pub use playlist_context::*; 6 | -------------------------------------------------------------------------------- /src/bin/server/context/playlist_context.rs: -------------------------------------------------------------------------------- 1 | use dizi::playlist::PlaylistType; 2 | 3 | use crate::{ 4 | playlist::DiziPlaylist, 5 | traits::{DiziPlaylistEntry, DiziPlaylistTrait}, 6 | }; 7 | 8 | #[derive(Clone, Debug)] 9 | pub struct PlaylistContext { 10 | pub file_playlist: DiziPlaylist, 11 | pub directory_playlist: DiziPlaylist, 12 | pub current_playlist_type: PlaylistType, 13 | } 14 | 15 | impl PlaylistContext { 16 | pub fn current_playlist_ref(&self) -> &DiziPlaylist { 17 | match self.current_playlist_type { 18 | PlaylistType::DirectoryListing => &self.directory_playlist, 19 | PlaylistType::PlaylistFile => &self.file_playlist, 20 | } 21 | } 22 | pub fn current_playlist_mut(&mut self) -> &mut DiziPlaylist { 23 | match self.current_playlist_type { 24 | PlaylistType::DirectoryListing => &mut self.directory_playlist, 25 | PlaylistType::PlaylistFile => &mut self.file_playlist, 26 | } 27 | } 28 | 29 | pub fn current_song(&self) -> Option { 30 | match self.current_playlist_type { 31 | PlaylistType::PlaylistFile => self.file_playlist.current_entry(), 32 | PlaylistType::DirectoryListing => self.directory_playlist.current_entry(), 33 | } 34 | } 35 | 36 | pub fn next_song_peak(&self) -> Option { 37 | match self.current_playlist_type { 38 | PlaylistType::PlaylistFile => self.file_playlist.next_song_peak(), 39 | PlaylistType::DirectoryListing => self.directory_playlist.next_song_peak(), 40 | } 41 | } 42 | 43 | pub fn previous_song_peak(&self) -> Option { 44 | match self.current_playlist_type { 45 | PlaylistType::PlaylistFile => self.file_playlist.previous_song_peak(), 46 | PlaylistType::DirectoryListing => self.directory_playlist.previous_song_peak(), 47 | } 48 | } 49 | 50 | pub fn is_end(&self) -> bool { 51 | match self.current_playlist_type { 52 | PlaylistType::PlaylistFile => self.file_playlist.is_end(), 53 | PlaylistType::DirectoryListing => self.directory_playlist.is_end(), 54 | } 55 | } 56 | } 57 | 58 | impl std::default::Default for PlaylistContext { 59 | fn default() -> Self { 60 | Self { 61 | file_playlist: DiziPlaylist::default(), 62 | directory_playlist: DiziPlaylist::default(), 63 | current_playlist_type: PlaylistType::PlaylistFile, 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/bin/server/error/error_type.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | use serde::Serialize; 4 | 5 | #[derive(Clone, Debug, Serialize)] 6 | pub struct AppError { 7 | pub kind: String, 8 | pub message: String, 9 | } 10 | 11 | impl std::convert::From for AppError { 12 | fn from(err: io::Error) -> Self { 13 | AppError { 14 | kind: "".to_string(), 15 | message: err.to_string(), 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/bin/server/error/mod.rs: -------------------------------------------------------------------------------- 1 | mod error_type; 2 | 3 | pub use error_type::*; 4 | -------------------------------------------------------------------------------- /src/bin/server/events.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::os::unix::net::UnixStream; 3 | use std::sync::mpsc; 4 | use std::thread; 5 | use std::time; 6 | 7 | use dizi::request::client::ClientRequest; 8 | use dizi::response::server::ServerBroadcastEvent; 9 | 10 | #[derive(Debug)] 11 | pub enum ServerEvent { 12 | // new client 13 | NewClient(UnixStream), 14 | 15 | PlayerProgressUpdate(time::Duration), 16 | PlayerDone, 17 | } 18 | 19 | #[derive(Debug)] 20 | pub enum AppEvent { 21 | Server(ServerEvent), 22 | Client { 23 | uuid: String, 24 | request: ClientRequest, 25 | }, 26 | } 27 | 28 | pub type AppEventReceiver = mpsc::Receiver; 29 | 30 | /// Send client requests for the server to process 31 | pub type ClientRequestSender = mpsc::Sender<(String, ClientRequest)>; 32 | // pub type ClientRequestReceiver = mpsc::Receiver<(String, ClientRequest)>; 33 | 34 | pub type ServerEventSender = mpsc::Sender; 35 | // pub type ServerEventReceiver = mpsc::Receiver; 36 | 37 | pub type ServerBroadcastEventSender = mpsc::Sender; 38 | pub type ServerBroadcastEventReceiver = mpsc::Receiver; 39 | 40 | /// A small event handler that wrap termion input and tick events. Each event 41 | /// type is handled in its own thread and returned to a common `Receiver` 42 | 43 | #[derive(Debug)] 44 | pub struct Events { 45 | // use if you want to send client requests 46 | pub client_request_tx: ClientRequestSender, 47 | // use if you want to send server events 48 | pub server_event_tx: ServerEventSender, 49 | // main listening loop 50 | pub app_event_rx: AppEventReceiver, 51 | 52 | pub server_broadcast_listeners: HashMap, 53 | } 54 | 55 | impl Events { 56 | pub fn new() -> Self { 57 | Events::_new() 58 | } 59 | 60 | fn _new() -> Self { 61 | let (client_request_tx, client_request_rx) = mpsc::channel(); 62 | let (server_event_tx, server_event_rx) = mpsc::channel(); 63 | 64 | let (app_event_tx, app_event_rx) = mpsc::channel(); 65 | 66 | // listen to client requests 67 | { 68 | let event_tx = app_event_tx.clone(); 69 | let _ = thread::spawn(move || loop { 70 | if let Ok((uuid, request)) = client_request_rx.recv() { 71 | let _ = event_tx.send(AppEvent::Client { uuid, request }); 72 | } 73 | }); 74 | } 75 | 76 | // listen to server requests 77 | { 78 | let event_tx = app_event_tx.clone(); 79 | let _ = thread::spawn(move || loop { 80 | if let Ok(msg) = server_event_rx.recv() { 81 | let _ = event_tx.send(AppEvent::Server(msg)); 82 | } 83 | }); 84 | } 85 | 86 | Events { 87 | client_request_tx, 88 | server_event_tx, 89 | app_event_rx, 90 | server_broadcast_listeners: HashMap::new(), 91 | } 92 | } 93 | 94 | pub fn client_request_sender(&self) -> &ClientRequestSender { 95 | &self.client_request_tx 96 | } 97 | 98 | pub fn server_event_sender(&self) -> &ServerEventSender { 99 | &self.server_event_tx 100 | } 101 | 102 | pub fn next(&self) -> Result { 103 | self.app_event_rx.recv() 104 | } 105 | 106 | pub fn add_broadcast_listener(&mut self, uuid: String, server_tx: ServerBroadcastEventSender) { 107 | self.server_broadcast_listeners.insert(uuid, server_tx); 108 | } 109 | 110 | pub fn broadcast_event(&mut self, event: ServerBroadcastEvent) { 111 | match &event { 112 | ServerBroadcastEvent::PlayerState { .. } => {} 113 | event => { 114 | tracing::debug!( 115 | "Server broadcast: {:#?} to {} clients", 116 | event, 117 | self.server_broadcast_listeners.len() 118 | ); 119 | } 120 | } 121 | for (_, server_tx) in self.server_broadcast_listeners.iter() { 122 | let _ = server_tx.send(event.clone()); 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/bin/server/main.rs: -------------------------------------------------------------------------------- 1 | mod audio; 2 | mod client; 3 | mod config; 4 | mod context; 5 | mod events; 6 | mod playlist; 7 | mod server; 8 | mod server_commands; 9 | mod server_util; 10 | mod traits; 11 | mod util; 12 | 13 | use std::path::PathBuf; 14 | 15 | use clap::Parser; 16 | 17 | use dizi::error::DiziResult; 18 | use lazy_static::lazy_static; 19 | use tracing_subscriber::{prelude::*, EnvFilter}; 20 | 21 | use crate::config::{AppConfig, TomlConfigFile}; 22 | 23 | const PROGRAM_NAME: &str = "dizi"; 24 | const CONFIG_HOME: &str = "DIZI_CONFIG_HOME"; 25 | const CONFIG_FILE: &str = "server.toml"; 26 | 27 | lazy_static! { 28 | // dynamically builds the config hierarchy 29 | static ref CONFIG_HIERARCHY: Vec = { 30 | let mut config_dirs = vec![]; 31 | 32 | if let Ok(p) = std::env::var(CONFIG_HOME) { 33 | let p = PathBuf::from(p); 34 | if p.is_dir() { 35 | config_dirs.push(p); 36 | } 37 | } 38 | 39 | if let Ok(dirs) = xdg::BaseDirectories::with_prefix(PROGRAM_NAME) { 40 | config_dirs.push(dirs.get_config_home()); 41 | } 42 | 43 | if let Ok(p) = std::env::var("HOME") { 44 | let mut p = PathBuf::from(p); 45 | p.push(format!(".config/{}", PROGRAM_NAME)); 46 | if p.is_dir() { 47 | config_dirs.push(p); 48 | } 49 | } 50 | 51 | // adds the default config files to the config hierarchy if running through cargo 52 | if cfg!(debug_assertions) { 53 | config_dirs.push(PathBuf::from("./config")); 54 | } 55 | config_dirs 56 | }; 57 | 58 | static ref HOME_DIR: Option = dirs_next::home_dir(); 59 | } 60 | 61 | #[derive(Clone, Debug, Parser)] 62 | pub struct CommandArgs { 63 | #[arg(short = 'v', long = "version")] 64 | version: bool, 65 | } 66 | 67 | fn run_server(args: CommandArgs) -> DiziResult { 68 | if args.version { 69 | let version = env!("CARGO_PKG_VERSION"); 70 | println!("{}", version); 71 | return Ok(()); 72 | } 73 | 74 | let config = AppConfig::get_config(CONFIG_FILE); 75 | 76 | let env_filter = EnvFilter::from_default_env(); 77 | let fmt_layer = tracing_subscriber::fmt::layer(); 78 | tracing_subscriber::registry() 79 | .with(env_filter) 80 | .with(fmt_layer) 81 | .init(); 82 | 83 | tracing::debug!("{:#?}", config); 84 | server::serve(config) 85 | } 86 | 87 | fn main() { 88 | let args = CommandArgs::parse(); 89 | let res = run_server(args); 90 | 91 | match res { 92 | Ok(_) => {} 93 | Err(e) => eprintln!("Error: {}", e), 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/bin/server/playlist/impl_playlist.rs: -------------------------------------------------------------------------------- 1 | use rand::prelude::SliceRandom; 2 | use rand::rng; 3 | 4 | use dizi::song::DiziSongEntry; 5 | 6 | use super::DiziPlaylist; 7 | use crate::traits::{DiziPlaylistEntry, DiziPlaylistTrait}; 8 | 9 | impl DiziPlaylistTrait for DiziPlaylist { 10 | fn is_empty(&self) -> bool { 11 | self.contents.is_empty() 12 | } 13 | fn len(&self) -> usize { 14 | self.contents.len() 15 | } 16 | fn push(&mut self, song: DiziSongEntry) { 17 | self.contents.push(song); 18 | let index = self.len() - 1; 19 | // add song to end of playlist order 20 | self.order.push(index); 21 | } 22 | fn remove(&mut self, index: usize) { 23 | self.contents.remove(index); 24 | } 25 | fn clear(&mut self) { 26 | self.contents.clear(); 27 | self.order.clear(); 28 | self.order_index = None; 29 | } 30 | fn swap(&mut self, index1: usize, index2: usize) { 31 | self.contents.swap(index1, index2); 32 | // if one of the songs is the one curently being played, 33 | // swap the playlist index as well 34 | if let Some(index) = self.order_index { 35 | if index == index1 { 36 | self.order_index = Some(index2); 37 | } 38 | if index == index2 { 39 | self.order_index = Some(index1); 40 | } 41 | } 42 | } 43 | fn is_end(&self) -> bool { 44 | match self.order_index { 45 | None => true, 46 | Some(i) => i + 1 >= self.len(), 47 | } 48 | } 49 | 50 | fn entry_ref(&self, index: usize) -> &DiziSongEntry { 51 | &self.contents[index] 52 | } 53 | 54 | fn current_entry(&self) -> Option { 55 | let order_index = self.order_index?; 56 | let playlist_index = self.order[order_index]; 57 | 58 | Some(DiziPlaylistEntry { 59 | entry_index: playlist_index, 60 | order_index, 61 | entry: self.entry_ref(playlist_index).clone(), 62 | }) 63 | } 64 | 65 | fn next_song_peak(&self) -> Option { 66 | let order_index = self.order_index?; 67 | let order_index = (order_index + 1) % self.len(); 68 | 69 | let entry_index = self.order[order_index]; 70 | 71 | Some(DiziPlaylistEntry { 72 | entry_index, 73 | order_index, 74 | entry: self.entry_ref(entry_index).clone(), 75 | }) 76 | } 77 | fn previous_song_peak(&self) -> Option { 78 | let order_index = self.order_index?; 79 | let order_index = (order_index + self.len() - 1) % self.len(); 80 | 81 | let entry_index = self.order[order_index]; 82 | 83 | Some(DiziPlaylistEntry { 84 | entry_index, 85 | order_index, 86 | entry: self.entry_ref(entry_index).clone(), 87 | }) 88 | } 89 | 90 | fn shuffle(&mut self) { 91 | // the current song being played should be the 92 | // first value of the random order 93 | match self.current_entry() { 94 | Some(entry) => { 95 | let entry_index = entry.entry_index; 96 | let mut new_shuffle_order: Vec = 97 | (0..self.len()).filter(|i| *i != entry_index).collect(); 98 | new_shuffle_order.shuffle(&mut rng()); 99 | new_shuffle_order.insert(0, entry_index); 100 | 101 | self.order = new_shuffle_order; 102 | self.order_index = Some(0); 103 | } 104 | None => { 105 | let mut new_shuffle_order: Vec = (0..self.len()).collect(); 106 | new_shuffle_order.shuffle(&mut rng()); 107 | self.order = new_shuffle_order; 108 | } 109 | } 110 | } 111 | 112 | fn unshuffle(&mut self) { 113 | // make sure unshuffle doesn't cause us to forget which song we were on 114 | if let Some(playlist_index) = self.order_index { 115 | let song_index = self.order[playlist_index]; 116 | self.order_index = Some(song_index); 117 | } 118 | self.order = (0..self.len()).collect(); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/bin/server/playlist/mod.rs: -------------------------------------------------------------------------------- 1 | mod impl_playlist; 2 | 3 | use std::fs; 4 | use std::io; 5 | use std::path::Path; 6 | 7 | use dizi::error::DiziResult; 8 | use dizi::playlist::FilePlaylist; 9 | use dizi::song::{DiziFile, DiziSongEntry}; 10 | 11 | #[derive(Clone, Debug)] 12 | pub struct DiziPlaylist { 13 | pub contents: Vec, 14 | pub order: Vec, 15 | pub order_index: Option, 16 | } 17 | 18 | impl DiziPlaylist { 19 | pub fn new(contents: Vec) -> Self { 20 | let content_count = contents.len(); 21 | Self { 22 | contents, 23 | order: (0..content_count).collect(), 24 | order_index: None, 25 | } 26 | } 27 | 28 | pub fn from_dir(path: &Path) -> io::Result { 29 | // only process regular files 30 | // if we can't read it, then don't play it 31 | let mut contents: Vec<_> = fs::read_dir(path)? 32 | .filter_map(|entry| entry.ok()) 33 | .map(|entry| entry.path()) 34 | .filter(|p| p.is_file()) 35 | .map(|path| DiziSongEntry::Unloaded(DiziFile::new(&path))) 36 | .collect(); 37 | contents.sort_by(|a, b| a.file_name().cmp(b.file_name())); 38 | 39 | let len = contents.len(); 40 | Ok(Self { 41 | contents, 42 | order: (0..len).collect(), 43 | order_index: None, 44 | }) 45 | } 46 | 47 | pub fn from_file(cwd: &Path, path: &Path) -> io::Result { 48 | let mut reader = m3u::Reader::open(path)?; 49 | let read_playlist: Vec<_> = reader.entries().map(|entry| entry.unwrap()).collect(); 50 | let mut entries = Vec::new(); 51 | for entry in read_playlist { 52 | if let m3u::Entry::Path(p) = entry { 53 | let file_path = if p.is_absolute() { 54 | p 55 | } else { 56 | let mut new_path = cwd.to_path_buf(); 57 | new_path.push(p); 58 | new_path 59 | }; 60 | let entry = DiziSongEntry::Unloaded(DiziFile::new(&file_path)); 61 | entries.push(entry); 62 | } 63 | } 64 | let playlist = DiziPlaylist::new(entries); 65 | Ok(playlist) 66 | } 67 | 68 | pub fn to_file_playlist(&self) -> FilePlaylist { 69 | let playing_index = self.order_index.and_then(|i| self.order.get(i)).map(|i| *i); 70 | FilePlaylist { 71 | list: self.contents.clone(), 72 | cursor_index: None, 73 | playing_index, 74 | } 75 | } 76 | 77 | pub fn load_current_entry_metadata(&mut self) -> DiziResult<()> { 78 | if let Some(order_index) = self.order_index { 79 | let entry_index = self.order[order_index]; 80 | let entry = self.contents[entry_index].clone(); 81 | let audio_file = entry.load_metadata()?; 82 | self.contents[entry_index] = DiziSongEntry::Loaded(audio_file); 83 | } 84 | Ok(()) 85 | } 86 | 87 | pub fn push_entry(&mut self, entry: DiziSongEntry) { 88 | self.contents.push(entry); 89 | self.order.push(self.contents.len() - 1); 90 | } 91 | 92 | pub fn remove_entry(&mut self, index: usize) { 93 | self.contents.remove(index); 94 | let new_len = self.contents.len(); 95 | let new_order: Vec = self 96 | .order 97 | .iter() 98 | .filter(|i| **i < new_len) 99 | .map(|i| *i) 100 | .collect(); 101 | self.order = new_order; 102 | } 103 | } 104 | 105 | impl std::default::Default for DiziPlaylist { 106 | fn default() -> Self { 107 | Self { 108 | contents: Vec::new(), 109 | order: Vec::new(), 110 | order_index: None, 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/bin/server/server.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::os::unix::net::UnixListener; 3 | use std::path::Path; 4 | use std::thread; 5 | 6 | use dizi::error::DiziResult; 7 | use dizi::response::server::ServerBroadcastEvent; 8 | 9 | use crate::audio::symphonia::player::SymphoniaPlayer; 10 | use crate::config::AppConfig; 11 | use crate::context::{AppContext, QuitType}; 12 | use crate::events::{AppEvent, Events, ServerEvent, ServerEventSender}; 13 | use crate::server_util; 14 | 15 | pub fn setup_socket(config: &AppConfig) -> DiziResult { 16 | let socket = Path::new(config.server_ref().socket_ref()); 17 | if socket.exists() { 18 | fs::remove_file(socket)?; 19 | } 20 | let stream = UnixListener::bind(socket)?; 21 | Ok(stream) 22 | } 23 | 24 | pub fn serve(config: AppConfig) -> DiziResult { 25 | let events = Events::new(); 26 | 27 | let player = { 28 | let server_event_tx = events.server_event_sender().clone(); 29 | SymphoniaPlayer::new(&config, server_event_tx)? 30 | }; 31 | 32 | let mut context = AppContext { 33 | events, 34 | config, 35 | quit: QuitType::DoNot, 36 | player, 37 | }; 38 | 39 | let listener = setup_socket(context.config_ref())?; 40 | // thread for listening to new client connections 41 | { 42 | let server_event_tx = context.events.server_event_sender().clone(); 43 | thread::spawn(|| listen_for_clients(listener, server_event_tx)); 44 | } 45 | 46 | while context.quit == QuitType::DoNot { 47 | let event = match context.events.next() { 48 | Ok(event) => event, 49 | Err(_) => return Ok(()), 50 | }; 51 | 52 | tracing::debug!("Server Event: {:?}", event); 53 | 54 | match event { 55 | AppEvent::Client { uuid, request } => { 56 | let res = server_util::process_client_request(&mut context, &uuid, request); 57 | if let Err(err) = res { 58 | tracing::debug!("Error: {:?}", err); 59 | context 60 | .events 61 | .broadcast_event(ServerBroadcastEvent::ServerError { 62 | msg: err.to_string(), 63 | }); 64 | } 65 | } 66 | AppEvent::Server(event) => { 67 | let res = server_util::process_server_event(&mut context, event); 68 | if let Err(err) = res { 69 | tracing::debug!("Error: {:?}", err); 70 | } 71 | } 72 | } 73 | } 74 | 75 | let playlist_path = context.config_ref().server_ref().playlist_ref(); 76 | let playlist = &context.player.playlist_context.file_playlist; 77 | 78 | tracing::debug!("Saving playlist to '{}'", playlist_path.to_string_lossy()); 79 | 80 | let mut file = std::fs::File::create(playlist_path)?; 81 | let mut writer = m3u::Writer::new(&mut file); 82 | for song in playlist.contents.iter() { 83 | let entry = m3u::Entry::Path(song.file_path().to_path_buf()); 84 | writer.write_entry(&entry)?; 85 | } 86 | tracing::debug!("Playlist saved!"); 87 | 88 | // broadcast to all clients that the server has exited 89 | context 90 | .events 91 | .broadcast_event(ServerBroadcastEvent::ServerQuit); 92 | 93 | Ok(()) 94 | } 95 | 96 | pub fn listen_for_clients(listener: UnixListener, event_tx: ServerEventSender) -> DiziResult { 97 | for stream in listener.incoming().flatten() { 98 | let _ = event_tx.send(ServerEvent::NewClient(stream)); 99 | } 100 | Ok(()) 101 | } 102 | -------------------------------------------------------------------------------- /src/bin/server/server_commands/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod player; 2 | pub mod playlist; 3 | pub mod server; 4 | 5 | pub use self::player::*; 6 | -------------------------------------------------------------------------------- /src/bin/server/server_commands/player.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use dizi::error::DiziResult; 4 | use dizi::player::PlayerStatus; 5 | 6 | use crate::context::AppContext; 7 | use crate::server_util::run_on_song_change; 8 | use crate::traits::AudioPlayer; 9 | 10 | pub fn player_play(context: &mut AppContext, path: &Path) -> DiziResult { 11 | context.player.play_directory(path)?; 12 | 13 | run_on_song_change(context); 14 | Ok(()) 15 | } 16 | 17 | pub fn player_pause(context: &mut AppContext) -> DiziResult { 18 | context.player.pause() 19 | } 20 | 21 | pub fn player_resume(context: &mut AppContext) -> DiziResult { 22 | context.player.resume() 23 | } 24 | 25 | pub fn player_toggle_play(context: &mut AppContext) -> DiziResult { 26 | let status = context.player.toggle_play()?; 27 | Ok(status) 28 | } 29 | 30 | pub fn player_get_volume(context: &mut AppContext) -> usize { 31 | context.player.get_volume() 32 | } 33 | 34 | pub fn player_set_volume(context: &mut AppContext, volume: usize) -> DiziResult { 35 | context.player.set_volume(volume)?; 36 | Ok(()) 37 | } 38 | 39 | pub fn player_volume_increase(context: &mut AppContext, amount: usize) -> DiziResult { 40 | let volume = player_get_volume(context); 41 | 42 | let volume = if volume + amount > 100 { 43 | 100 44 | } else { 45 | volume + amount 46 | }; 47 | player_set_volume(context, volume)?; 48 | 49 | tracing::debug!("volume is now: {volume}"); 50 | Ok(volume) 51 | } 52 | 53 | pub fn player_volume_decrease(context: &mut AppContext, amount: usize) -> DiziResult { 54 | let volume = player_get_volume(context); 55 | 56 | let volume = if amount > volume { 0 } else { volume - amount }; 57 | player_set_volume(context, volume)?; 58 | 59 | tracing::debug!("volume is now: {volume}"); 60 | Ok(volume) 61 | } 62 | 63 | pub fn player_play_again(context: &mut AppContext) -> DiziResult { 64 | context.player.play_again()?; 65 | run_on_song_change(context); 66 | Ok(()) 67 | } 68 | 69 | pub fn player_play_next(context: &mut AppContext) -> DiziResult { 70 | context.player.play_next()?; 71 | run_on_song_change(context); 72 | Ok(()) 73 | } 74 | 75 | pub fn player_play_previous(context: &mut AppContext) -> DiziResult { 76 | context.player.play_previous()?; 77 | run_on_song_change(context); 78 | Ok(()) 79 | } 80 | -------------------------------------------------------------------------------- /src/bin/server/server_commands/server.rs: -------------------------------------------------------------------------------- 1 | use dizi::error::DiziResult; 2 | 3 | use crate::{ 4 | context::{AppContext, QuitType}, 5 | traits::AudioPlayer, 6 | }; 7 | 8 | pub fn quit_server(context: &mut AppContext) -> DiziResult { 9 | context.quit = QuitType::Server; 10 | Ok(()) 11 | } 12 | 13 | pub fn query(context: &mut AppContext, query: &str) -> DiziResult { 14 | let player_state = context.player.player_state(); 15 | let res = player_state.query(query)?; 16 | Ok(res) 17 | } 18 | -------------------------------------------------------------------------------- /src/bin/server/traits/audio_player.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | use std::time; 3 | 4 | use dizi::error::DiziResult; 5 | use dizi::player::{PlayerState, PlayerStatus}; 6 | use dizi::song::DiziAudioFile; 7 | 8 | use crate::context::PlaylistContext; 9 | 10 | pub trait AudioPlayer { 11 | fn player_state(&self) -> PlayerState; 12 | 13 | fn play_directory(&mut self, path: &Path) -> DiziResult; 14 | fn play_from_playlist(&mut self, index: usize) -> DiziResult; 15 | 16 | fn play_again(&mut self) -> DiziResult; 17 | fn play_next(&mut self) -> DiziResult; 18 | fn play_previous(&mut self) -> DiziResult; 19 | 20 | fn pause(&mut self) -> DiziResult; 21 | fn resume(&mut self) -> DiziResult; 22 | fn stop(&mut self) -> DiziResult; 23 | fn toggle_play(&mut self) -> DiziResult; 24 | 25 | fn fast_forward(&mut self, duration: time::Duration) -> DiziResult; 26 | fn rewind(&mut self, duration: time::Duration) -> DiziResult; 27 | 28 | fn get_volume(&self) -> usize; 29 | fn set_volume(&mut self, volume: usize) -> DiziResult; 30 | 31 | fn next_enabled(&self) -> bool; 32 | fn repeat_enabled(&self) -> bool; 33 | fn shuffle_enabled(&self) -> bool; 34 | 35 | fn set_next(&mut self, next: bool); 36 | fn set_repeat(&mut self, repeat: bool); 37 | fn set_shuffle(&mut self, shuffle: bool); 38 | 39 | fn set_elapsed(&mut self, elapsed: time::Duration); 40 | 41 | fn current_song_ref(&self) -> Option<&DiziAudioFile>; 42 | 43 | fn playlist_context_mut(&mut self) -> &mut PlaylistContext; 44 | } 45 | -------------------------------------------------------------------------------- /src/bin/server/traits/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod audio_player; 2 | pub mod playlist; 3 | 4 | pub use self::audio_player::*; 5 | pub use self::playlist::*; 6 | -------------------------------------------------------------------------------- /src/bin/server/traits/playlist.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use dizi::song::DiziSongEntry; 4 | 5 | #[derive(Clone, Debug, Deserialize, Serialize)] 6 | pub struct DiziPlaylistEntry { 7 | pub entry_index: usize, 8 | pub order_index: usize, 9 | pub entry: DiziSongEntry, 10 | } 11 | 12 | pub trait DiziPlaylistTrait { 13 | fn is_empty(&self) -> bool; 14 | fn len(&self) -> usize; 15 | fn push(&mut self, song: DiziSongEntry); 16 | fn remove(&mut self, index: usize); 17 | fn clear(&mut self); 18 | fn swap(&mut self, index1: usize, index2: usize); 19 | 20 | fn is_end(&self) -> bool; 21 | 22 | fn entry_ref(&self, index: usize) -> &DiziSongEntry; 23 | 24 | fn current_entry(&self) -> Option; 25 | 26 | fn next_song_peak(&self) -> Option; 27 | fn previous_song_peak(&self) -> Option; 28 | 29 | fn shuffle(&mut self); 30 | fn unshuffle(&mut self); 31 | } 32 | -------------------------------------------------------------------------------- /src/bin/server/util/mimetype.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use std::path::Path; 3 | use std::process::Command; 4 | 5 | pub fn get_mimetype(p: &Path) -> io::Result { 6 | let output = Command::new("file") 7 | .arg("-b") 8 | .arg("--mime-type") 9 | .arg(p) 10 | .output()?; 11 | let stdout = std::str::from_utf8(&output.stdout).expect("Failed to read from stdout"); 12 | let mimetype = stdout.to_string(); 13 | tracing::debug!("{:?} mimetype: {}", p, mimetype); 14 | Ok(mimetype) 15 | } 16 | 17 | pub fn is_playable(p: &Path) -> io::Result { 18 | let mimetype = get_mimetype(p)?; 19 | let is_audio_mimetype = is_mimetype_audio(&mimetype) || is_mimetype_video(&mimetype); 20 | if is_audio_mimetype { 21 | return Ok(true); 22 | } 23 | match p.extension() { 24 | None => Ok(false), 25 | Some(s) => match s.to_string_lossy().as_ref() { 26 | "aac" | "flac" | "mp3" | "mp4" | "m4a" | "ogg" | "opus" | "wav" | "webm" => Ok(true), 27 | _ => Ok(false), 28 | }, 29 | } 30 | } 31 | 32 | pub fn is_mimetype_audio(s: &str) -> bool { 33 | s.starts_with("audio/") 34 | } 35 | 36 | pub fn is_mimetype_video(s: &str) -> bool { 37 | s.starts_with("video/") 38 | } 39 | -------------------------------------------------------------------------------- /src/bin/server/util/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod mimetype; 2 | -------------------------------------------------------------------------------- /src/error/error_kind.rs: -------------------------------------------------------------------------------- 1 | use std::convert::From; 2 | use std::io; 3 | 4 | #[derive(Debug)] 5 | pub enum DiziErrorKind { 6 | Server, 7 | Symphonia, 8 | 9 | // io related 10 | IoError(io::ErrorKind), 11 | 12 | // environment variable not found 13 | EnvVarNotPresent, 14 | 15 | // parse error 16 | ParseError, 17 | SerdeJson, 18 | ClipboardError, 19 | 20 | Glob, 21 | InvalidParameters, 22 | 23 | SendError, 24 | ReceiveError, 25 | 26 | CpalBuildStreamError(cpal::BuildStreamError), 27 | CpalPlayStreamError(cpal::PlayStreamError), 28 | CpalPauseStreamError(cpal::PauseStreamError), 29 | 30 | NoDevice, 31 | UnrecognizedFormat, 32 | NotAudioFile, 33 | 34 | UnrecognizedArgument, 35 | UnrecognizedCommand, 36 | } 37 | 38 | impl From for DiziErrorKind { 39 | fn from(err: io::ErrorKind) -> Self { 40 | Self::IoError(err) 41 | } 42 | } 43 | 44 | impl From<&globset::ErrorKind> for DiziErrorKind { 45 | fn from(_: &globset::ErrorKind) -> Self { 46 | Self::Glob 47 | } 48 | } 49 | 50 | impl From for DiziErrorKind { 51 | fn from(_: std::env::VarError) -> Self { 52 | Self::EnvVarNotPresent 53 | } 54 | } 55 | 56 | impl From for DiziErrorKind { 57 | fn from(_: serde_json::Error) -> Self { 58 | Self::SerdeJson 59 | } 60 | } 61 | 62 | impl From for DiziErrorKind { 63 | fn from(_: toml::de::Error) -> Self { 64 | Self::ParseError 65 | } 66 | } 67 | 68 | impl From for DiziErrorKind { 69 | fn from(_: symphonia::core::errors::Error) -> Self { 70 | Self::Symphonia 71 | } 72 | } 73 | 74 | impl From for DiziErrorKind { 75 | fn from(e: cpal::BuildStreamError) -> Self { 76 | Self::CpalBuildStreamError(e) 77 | } 78 | } 79 | 80 | impl From for DiziErrorKind { 81 | fn from(e: cpal::PlayStreamError) -> Self { 82 | Self::CpalPlayStreamError(e) 83 | } 84 | } 85 | 86 | impl From for DiziErrorKind { 87 | fn from(e: cpal::PauseStreamError) -> Self { 88 | Self::CpalPauseStreamError(e) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/error/error_type.rs: -------------------------------------------------------------------------------- 1 | use std::convert::From; 2 | use std::io; 3 | 4 | use super::DiziErrorKind; 5 | 6 | #[derive(Debug)] 7 | pub struct DiziError { 8 | _kind: DiziErrorKind, 9 | _cause: String, 10 | } 11 | 12 | impl DiziError { 13 | pub fn new(_kind: DiziErrorKind, _cause: String) -> Self { 14 | Self { _kind, _cause } 15 | } 16 | 17 | pub fn kind(&self) -> &DiziErrorKind { 18 | &self._kind 19 | } 20 | } 21 | 22 | impl std::fmt::Display for DiziError { 23 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 24 | write!(f, "{}", self._cause) 25 | } 26 | } 27 | 28 | impl From for DiziError { 29 | fn from(err: io::Error) -> Self { 30 | let _cause = err.to_string(); 31 | Self { 32 | _kind: DiziErrorKind::from(err.kind()), 33 | _cause, 34 | } 35 | } 36 | } 37 | 38 | impl From for DiziError { 39 | fn from(err: globset::Error) -> Self { 40 | let _cause = err.to_string(); 41 | Self { 42 | _kind: DiziErrorKind::from(err.kind()), 43 | _cause, 44 | } 45 | } 46 | } 47 | 48 | impl From for DiziError { 49 | fn from(err: std::env::VarError) -> Self { 50 | let _cause = err.to_string(); 51 | Self { 52 | _kind: DiziErrorKind::from(err), 53 | _cause, 54 | } 55 | } 56 | } 57 | 58 | impl From for DiziError { 59 | fn from(err: std::sync::mpsc::RecvError) -> Self { 60 | let _cause = err.to_string(); 61 | Self { 62 | _kind: DiziErrorKind::ReceiveError, 63 | _cause, 64 | } 65 | } 66 | } 67 | 68 | impl From> for DiziError { 69 | fn from(err: std::sync::mpsc::SendError) -> Self { 70 | let _cause = err.to_string(); 71 | Self { 72 | _kind: DiziErrorKind::SendError, 73 | _cause, 74 | } 75 | } 76 | } 77 | 78 | impl From for DiziError { 79 | fn from(err: serde_json::Error) -> Self { 80 | let _cause = err.to_string(); 81 | Self { 82 | _kind: DiziErrorKind::from(err), 83 | _cause, 84 | } 85 | } 86 | } 87 | 88 | impl From for DiziError { 89 | fn from(err: toml::de::Error) -> Self { 90 | let _cause = err.to_string(); 91 | Self { 92 | _kind: DiziErrorKind::from(err), 93 | _cause, 94 | } 95 | } 96 | } 97 | 98 | impl From for DiziError { 99 | fn from(err: symphonia::core::errors::Error) -> Self { 100 | let _cause = err.to_string(); 101 | Self { 102 | _kind: DiziErrorKind::from(err), 103 | _cause, 104 | } 105 | } 106 | } 107 | 108 | impl From for DiziError { 109 | fn from(err: cpal::BuildStreamError) -> Self { 110 | let _cause = err.to_string(); 111 | Self { 112 | _kind: DiziErrorKind::from(err), 113 | _cause, 114 | } 115 | } 116 | } 117 | 118 | impl From for DiziError { 119 | fn from(err: cpal::PlayStreamError) -> Self { 120 | let _cause = err.to_string(); 121 | Self { 122 | _kind: DiziErrorKind::from(err), 123 | _cause, 124 | } 125 | } 126 | } 127 | 128 | impl From for DiziError { 129 | fn from(err: cpal::PauseStreamError) -> Self { 130 | let _cause = err.to_string(); 131 | Self { 132 | _kind: DiziErrorKind::from(err), 133 | _cause, 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/error/mod.rs: -------------------------------------------------------------------------------- 1 | mod error_kind; 2 | mod error_type; 3 | 4 | pub use self::error_kind::DiziErrorKind; 5 | pub use self::error_type::DiziError; 6 | 7 | pub type DiziResult = Result; 8 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod error; 2 | pub mod player; 3 | pub mod playlist; 4 | pub mod request; 5 | pub mod response; 6 | pub mod song; 7 | pub mod traits; 8 | pub mod utils; 9 | -------------------------------------------------------------------------------- /src/player.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::string::ToString; 3 | use std::time; 4 | 5 | use serde::{Deserialize, Serialize}; 6 | 7 | use strfmt::strfmt; 8 | 9 | use crate::error::{DiziError, DiziErrorKind, DiziResult}; 10 | use crate::playlist::{FilePlaylist, PlaylistType}; 11 | use crate::song::DiziAudioFile; 12 | 13 | #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] 14 | pub enum PlayerStatus { 15 | Playing, 16 | Paused, 17 | Stopped, 18 | } 19 | 20 | impl ToString for PlayerStatus { 21 | fn to_string(&self) -> String { 22 | match *self { 23 | Self::Playing => "playing".to_string(), 24 | Self::Paused => "paused".to_string(), 25 | Self::Stopped => "stopped".to_string(), 26 | } 27 | } 28 | } 29 | 30 | #[derive(Clone, Debug, Serialize, Deserialize)] 31 | pub struct PlayerState { 32 | pub song: Option, 33 | pub elapsed: time::Duration, 34 | 35 | pub status: PlayerStatus, 36 | pub playlist_status: PlaylistType, 37 | 38 | pub volume: usize, 39 | 40 | pub next: bool, 41 | pub repeat: bool, 42 | pub shuffle: bool, 43 | 44 | pub playlist: FilePlaylist, 45 | 46 | pub audio_host: String, 47 | } 48 | 49 | impl PlayerState { 50 | pub fn new() -> Self { 51 | Self::default() 52 | } 53 | 54 | pub fn query(&self, query: &str) -> DiziResult { 55 | let vars = self.query_all(); 56 | 57 | match strfmt(&query, &vars) { 58 | Ok(s) => Ok(s), 59 | Err(e) => Err(DiziError::new( 60 | DiziErrorKind::InvalidParameters, 61 | format!( 62 | "Failed to process query '{}', Reason: '{}'", 63 | query, 64 | e.to_string() 65 | ), 66 | )), 67 | } 68 | } 69 | 70 | pub fn query_all(&self) -> HashMap { 71 | let mut vars = HashMap::new(); 72 | Self::load_player_query_vars(&mut vars, self); 73 | if let Some(song) = self.song.as_ref() { 74 | Self::load_song_query_vars(&mut vars, song); 75 | } 76 | vars 77 | } 78 | 79 | fn load_player_query_vars(vars: &mut HashMap, player_state: &PlayerState) { 80 | vars.insert("player.status".to_string(), player_state.status.to_string()); 81 | vars.insert( 82 | "player.volume".to_string(), 83 | format!("{}", player_state.volume), 84 | ); 85 | vars.insert("player.next".to_string(), format!("{}", player_state.next)); 86 | vars.insert( 87 | "player.repeat".to_string(), 88 | format!("{}", player_state.repeat), 89 | ); 90 | vars.insert( 91 | "player.shuffle".to_string(), 92 | format!("{}", player_state.shuffle), 93 | ); 94 | vars.insert( 95 | "playlist.status".to_string(), 96 | player_state.playlist_status.to_string(), 97 | ); 98 | 99 | if let Some(index) = player_state.playlist.get_playing_index() { 100 | vars.insert("playlist.index".to_string(), format!("{}", index)); 101 | } 102 | vars.insert( 103 | "playlist.length".to_string(), 104 | format!("{}", player_state.playlist.len()), 105 | ); 106 | vars.insert("audio.host".to_string(), player_state.audio_host.clone()); 107 | } 108 | 109 | fn load_song_query_vars(vars: &mut HashMap, song: &DiziAudioFile) { 110 | vars.insert("song.file_name".to_string(), song.file_name().to_string()); 111 | vars.insert( 112 | "song.file_path".to_string(), 113 | song.file_path().to_string_lossy().to_string(), 114 | ); 115 | for (tag, value) in song.music_metadata.standard_tags.iter() { 116 | vars.insert( 117 | format!("song.tag.{}", tag.to_lowercase()), 118 | value.to_string(), 119 | ); 120 | } 121 | if let Some(total_duration) = song.audio_metadata.total_duration.as_ref() { 122 | vars.insert( 123 | "song.total_duration".to_string(), 124 | total_duration.as_secs().to_string(), 125 | ); 126 | } 127 | } 128 | } 129 | 130 | impl std::default::Default for PlayerState { 131 | fn default() -> Self { 132 | Self { 133 | song: None, 134 | status: PlayerStatus::Stopped, 135 | playlist_status: PlaylistType::PlaylistFile, 136 | elapsed: time::Duration::from_secs(0), 137 | volume: 50, 138 | next: true, 139 | repeat: false, 140 | shuffle: false, 141 | playlist: FilePlaylist::new(), 142 | audio_host: "UNKNOWN".to_string(), 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/playlist.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use crate::song::DiziSongEntry; 6 | 7 | #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] 8 | pub enum PlaylistType { 9 | DirectoryListing, 10 | PlaylistFile, 11 | } 12 | 13 | impl ToString for PlaylistType { 14 | fn to_string(&self) -> String { 15 | match *self { 16 | Self::DirectoryListing => "directory".to_string(), 17 | Self::PlaylistFile => "file".to_string(), 18 | } 19 | } 20 | } 21 | 22 | #[derive(Clone, Debug, Deserialize, Serialize)] 23 | pub struct FilePlaylist { 24 | pub list: Vec, 25 | pub cursor_index: Option, 26 | pub playing_index: Option, 27 | } 28 | 29 | impl FilePlaylist { 30 | pub fn new() -> Self { 31 | Self::default() 32 | } 33 | 34 | pub fn first_index_for_viewport(&self, viewport_height: usize) -> usize { 35 | match self.get_cursor_index() { 36 | Some(index) => index / viewport_height as usize * viewport_height as usize, 37 | None => 0, 38 | } 39 | } 40 | 41 | pub fn playlist(&self) -> &[DiziSongEntry] { 42 | self.list.as_slice() 43 | } 44 | 45 | pub fn clear(&mut self) { 46 | self.list_mut().clear(); 47 | self.cursor_index = None; 48 | self.playing_index = None; 49 | } 50 | 51 | pub fn append_song(&mut self, s: DiziSongEntry) { 52 | self.list_mut().push(s); 53 | } 54 | 55 | pub fn remove_song(&mut self, index: usize) -> DiziSongEntry { 56 | let song = self.list_mut().remove(index); 57 | 58 | if let Some(playing_index) = self.playing_index { 59 | if playing_index == index { 60 | self.set_playing_index(None); 61 | } 62 | } 63 | if self.list_ref().is_empty() { 64 | self.set_cursor_index(None); 65 | } else { 66 | match self.get_cursor_index() { 67 | Some(i) if i >= self.list_ref().len() => { 68 | self.set_cursor_index(Some(self.list_ref().len() - 1)); 69 | } 70 | _ => {} 71 | } 72 | match self.get_playing_index() { 73 | Some(i) if i > index => { 74 | self.set_playing_index(Some(i - 1)); 75 | } 76 | _ => {} 77 | } 78 | } 79 | song 80 | } 81 | 82 | pub fn get_cursor_index(&self) -> Option { 83 | self.cursor_index 84 | } 85 | pub fn set_cursor_index(&mut self, index: Option) { 86 | self.cursor_index = index; 87 | } 88 | 89 | pub fn get_playing_index(&self) -> Option { 90 | self.playing_index 91 | } 92 | pub fn set_playing_index(&mut self, index: Option) { 93 | self.playing_index = index; 94 | } 95 | 96 | pub fn len(&self) -> usize { 97 | self.list.len() 98 | } 99 | 100 | pub fn is_empty(&self) -> bool { 101 | self.list.is_empty() 102 | } 103 | 104 | pub fn list_ref(&self) -> &[DiziSongEntry] { 105 | &self.list 106 | } 107 | pub fn list_mut(&mut self) -> &mut Vec { 108 | &mut self.list 109 | } 110 | } 111 | 112 | impl std::default::Default for FilePlaylist { 113 | fn default() -> Self { 114 | Self { 115 | list: Vec::new(), 116 | cursor_index: None, 117 | playing_index: None, 118 | } 119 | } 120 | } 121 | 122 | #[derive(Clone, Debug)] 123 | pub struct DirectoryPlaylist { 124 | _list: Vec, 125 | pub index: usize, 126 | } 127 | 128 | impl DirectoryPlaylist { 129 | pub fn new() -> Self { 130 | Self::default() 131 | } 132 | 133 | pub fn set_playing_index(&mut self, index: usize) { 134 | self.index = index; 135 | } 136 | pub fn get_playing_index(&self) -> usize { 137 | self.index 138 | } 139 | 140 | pub fn len(&self) -> usize { 141 | self._list.len() 142 | } 143 | 144 | pub fn list_ref(&self) -> &Vec { 145 | &self._list 146 | } 147 | pub fn list_mut(&mut self) -> &mut Vec { 148 | &mut self._list 149 | } 150 | } 151 | 152 | impl std::default::Default for DirectoryPlaylist { 153 | fn default() -> Self { 154 | Self { 155 | _list: Vec::new(), 156 | index: 0, 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/request/client.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[derive(Clone, Debug, Deserialize, Serialize)] 6 | #[serde(tag = "api")] 7 | pub enum ClientRequest { 8 | // quit server 9 | #[serde(rename = "/server/quit")] 10 | ServerQuit, 11 | #[serde(rename = "/server/query")] 12 | ServerQuery { query: String }, 13 | #[serde(rename = "/server/query_all")] 14 | ServerQueryAll, 15 | 16 | // client left 17 | #[serde(rename = "/client/leave")] 18 | ClientLeave { uuid: String }, 19 | 20 | // player requests 21 | #[serde(rename = "/player/state")] 22 | PlayerState, 23 | #[serde(rename = "/player/play/file")] 24 | PlayerFilePlay { path: Option }, 25 | 26 | #[serde(rename = "/player/play/next")] 27 | PlayerPlayNext, 28 | #[serde(rename = "/player/play/previous")] 29 | PlayerPlayPrevious, 30 | 31 | #[serde(rename = "/player/pause")] 32 | PlayerPause, 33 | #[serde(rename = "/player/resume")] 34 | PlayerResume, 35 | #[serde(rename = "/player/volume/get")] 36 | PlayerGetVolume, 37 | 38 | #[serde(rename = "/player/rewind")] 39 | PlayerRewind { amount: usize }, 40 | #[serde(rename = "/player/fast_forward")] 41 | PlayerFastForward { amount: usize }, 42 | 43 | #[serde(rename = "/player/toggle/play")] 44 | PlayerTogglePlay, 45 | #[serde(rename = "/player/toggle/next")] 46 | PlayerToggleNext, 47 | #[serde(rename = "/player/toggle/repeat")] 48 | PlayerToggleRepeat, 49 | #[serde(rename = "/player/toggle/shuffle")] 50 | PlayerToggleShuffle, 51 | 52 | #[serde(rename = "/player/volume/increase")] 53 | PlayerVolumeUp { amount: usize }, 54 | #[serde(rename = "/player/volume/decrease")] 55 | PlayerVolumeDown { amount: usize }, 56 | 57 | // playlist requests 58 | #[serde(rename = "/playlist/state")] 59 | PlaylistState, 60 | #[serde(rename = "/playlist/open")] 61 | PlaylistOpen { 62 | cwd: Option, 63 | path: Option, 64 | }, 65 | #[serde(rename = "/playlist/play")] 66 | PlaylistPlay { index: Option }, 67 | 68 | #[serde(rename = "/playlist/append")] 69 | PlaylistAppend { path: Option }, 70 | #[serde(rename = "/playlist/remove")] 71 | PlaylistRemove { index: Option }, 72 | #[serde(rename = "/playlist/clear")] 73 | PlaylistClear, 74 | #[serde(rename = "/playlist/move_up")] 75 | PlaylistMoveUp { index: Option }, 76 | #[serde(rename = "/playlist/move_down")] 77 | PlaylistMoveDown { index: Option }, 78 | } 79 | 80 | impl ClientRequest { 81 | pub fn api_path(&self) -> &'static str { 82 | match &*self { 83 | Self::ClientLeave { .. } => "/client/leave", 84 | Self::ServerQuit => "/server/quit", 85 | Self::ServerQuery { .. } => "/server/query", 86 | Self::ServerQueryAll => "/server/query_all", 87 | 88 | Self::PlayerState => "/player/state", 89 | Self::PlayerFilePlay { .. } => "/player/play/file", 90 | Self::PlayerPlayNext => "/player/play/next", 91 | Self::PlayerPlayPrevious => "/player/play/previous", 92 | Self::PlayerPause => "/player/pause", 93 | Self::PlayerResume => "/player/resume", 94 | Self::PlayerGetVolume => "/player/volume/get", 95 | Self::PlayerRewind { .. } => "/player/rewind", 96 | Self::PlayerFastForward { .. } => "/player/fast_forward", 97 | Self::PlayerTogglePlay => "/player/toggle/play", 98 | Self::PlayerToggleNext => "/player/toggle/next", 99 | Self::PlayerToggleRepeat => "/player/toggle/repeat", 100 | Self::PlayerToggleShuffle => "/player/toggle/shuffle", 101 | Self::PlayerVolumeUp { .. } => "/player/volume/increase", 102 | Self::PlayerVolumeDown { .. } => "/player/volume/decrease", 103 | 104 | Self::PlaylistState => "/playlist/state", 105 | Self::PlaylistOpen { .. } => "/playlist/open", 106 | Self::PlaylistPlay { .. } => "/playlist/play", 107 | 108 | Self::PlaylistAppend { .. } => "/playlist/append", 109 | Self::PlaylistRemove { .. } => "/playlist/remove", 110 | Self::PlaylistClear => "/playlist/clear", 111 | 112 | Self::PlaylistMoveUp { .. } => "/playlist/move_up", 113 | Self::PlaylistMoveDown { .. } => "/playlist/move_down", 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/request/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod client; 2 | -------------------------------------------------------------------------------- /src/response/constants.rs: -------------------------------------------------------------------------------- 1 | pub const RESP_SERVER_QUIT: &str = "/server/quit"; 2 | 3 | pub const RESP_PLAYLIST_GET: &str = "/playlist/get"; 4 | pub const RESP_PLAYLIST_ADD: &str = "/playlist/add"; 5 | pub const RESP_PLAYLIST_REMOVE: &str = "/playlist/remove"; 6 | 7 | pub const RESP_PLAYER_GET: &str = "/player/get"; 8 | pub const RESP_PLAYER_PLAY: &str = "/player/play"; 9 | pub const RESP_PLAYER_PAUSE: &str = "/player/pause"; 10 | pub const RESP_PLAYER_RESUME: &str = "/player/resume"; 11 | 12 | pub const RESP_PLAYER_SHUFFLE_ON: &str = "/player/shuffle/on"; 13 | pub const RESP_PLAYER_SHUFFLE_OFF: &str = "/player/shuffle/off"; 14 | pub const RESP_PLAYER_REPEAT_ON: &str = "/player/repeat/on"; 15 | pub const RESP_PLAYER_REPEAT_OFF: &str = "/player/repeat/off"; 16 | pub const RESP_PLAYER_NEXT_ON: &str = "/player/next/on"; 17 | pub const RESP_PLAYER_NEXT_OFF: &str = "/player/next/off"; 18 | 19 | pub const RESP_PLAYER_VOLUME_UPDATE: &str = "/player/volume/update"; 20 | pub const RESP_PLAYER_PROGRESS_UPDATE: &str = "/player/progress/update"; 21 | -------------------------------------------------------------------------------- /src/response/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod constants; 2 | pub mod server; 3 | 4 | pub use self::constants::*; 5 | -------------------------------------------------------------------------------- /src/response/server.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::time; 3 | 4 | use serde::{Deserialize, Serialize}; 5 | 6 | use crate::player::PlayerState; 7 | use crate::song::DiziAudioFile; 8 | 9 | #[derive(Clone, Debug, Deserialize, Serialize)] 10 | pub enum ServerBroadcastEvent { 11 | // server is shutting down 12 | ServerQuit, 13 | ServerError { 14 | msg: String, 15 | }, 16 | ServerQuery { 17 | query: String, 18 | }, 19 | ServerQueryAll { 20 | query_items: HashMap, 21 | }, 22 | 23 | // player status updates 24 | PlayerState { 25 | state: PlayerState, 26 | }, 27 | 28 | PlayerFilePlay { 29 | file: DiziAudioFile, 30 | }, 31 | 32 | PlayerPause, 33 | PlayerResume, 34 | PlayerStop, 35 | 36 | PlayerRepeat { 37 | on: bool, 38 | }, 39 | PlayerShuffle { 40 | on: bool, 41 | }, 42 | PlayerNext { 43 | on: bool, 44 | }, 45 | 46 | PlayerVolumeUpdate { 47 | volume: usize, 48 | }, 49 | PlayerProgressUpdate { 50 | elapsed: time::Duration, 51 | }, 52 | 53 | // playlist 54 | PlaylistOpen { 55 | state: PlayerState, 56 | }, 57 | PlaylistPlay { 58 | index: usize, 59 | }, 60 | PlaylistAppend { 61 | audio_files: Vec, 62 | }, 63 | PlaylistRemove { 64 | index: usize, 65 | }, 66 | PlaylistSwapMove { 67 | index1: usize, 68 | index2: usize, 69 | }, 70 | PlaylistClear, 71 | } 72 | -------------------------------------------------------------------------------- /src/traits.rs: -------------------------------------------------------------------------------- 1 | pub trait DiziJsonCommand<'a>: serde::Deserialize<'a> + serde::Serialize { 2 | fn path() -> &'static str; 3 | } 4 | -------------------------------------------------------------------------------- /src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod stream; 2 | 3 | pub use self::stream::*; 4 | -------------------------------------------------------------------------------- /src/utils/stream.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use std::io::Write; 3 | use std::os::unix::net::UnixStream; 4 | 5 | pub const NEWLINE: &[u8] = &['\n' as u8]; 6 | 7 | pub fn flush(stream: &mut UnixStream) -> io::Result<()> { 8 | stream.write(NEWLINE)?; 9 | Ok(()) 10 | } 11 | --------------------------------------------------------------------------------