├── .github └── FUNDING.yml ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── demo.gif └── src ├── app.rs ├── input ├── controls.rs └── mod.rs ├── main.rs ├── messages ├── event.rs ├── instruction.rs └── mod.rs ├── os ├── mod.rs ├── unix.rs └── windows.rs ├── state ├── file_to_delete.rs ├── files │ ├── file_or_folder.rs │ ├── file_tree.rs │ └── mod.rs ├── mod.rs ├── tiles │ ├── board.rs │ ├── files_in_folder.rs │ ├── mod.rs │ ├── rect_float.rs │ ├── tile.rs │ └── treemap.rs └── ui_effects.rs ├── tests ├── cases │ ├── mod.rs │ ├── snapshots │ │ ├── diskonaut__tests__cases__ui__cannot_move_into_small_files-2.snap │ │ ├── diskonaut__tests__cases__ui__cannot_move_into_small_files-3.snap │ │ ├── diskonaut__tests__cases__ui__cannot_move_into_small_files-4.snap │ │ ├── diskonaut__tests__cases__ui__cannot_move_into_small_files-5.snap │ │ ├── diskonaut__tests__cases__ui__cannot_move_into_small_files.snap │ │ ├── diskonaut__tests__cases__ui__cant_delete_file_with_term_too_small.snap │ │ ├── diskonaut__tests__cases__ui__clear_selection_when_moving_off_screen_edges-2.snap │ │ ├── diskonaut__tests__cases__ui__clear_selection_when_moving_off_screen_edges-3.snap │ │ ├── diskonaut__tests__cases__ui__clear_selection_when_moving_off_screen_edges-4.snap │ │ ├── diskonaut__tests__cases__ui__clear_selection_when_moving_off_screen_edges-5.snap │ │ ├── diskonaut__tests__cases__ui__clear_selection_when_moving_off_screen_edges.snap │ │ ├── diskonaut__tests__cases__ui__delete_file-2.snap │ │ ├── diskonaut__tests__cases__ui__delete_file-3.snap │ │ ├── diskonaut__tests__cases__ui__delete_file-4.snap │ │ ├── diskonaut__tests__cases__ui__delete_file-5.snap │ │ ├── diskonaut__tests__cases__ui__delete_file-6.snap │ │ ├── diskonaut__tests__cases__ui__delete_file-7.snap │ │ ├── diskonaut__tests__cases__ui__delete_file-8.snap │ │ ├── diskonaut__tests__cases__ui__delete_file.snap │ │ ├── diskonaut__tests__cases__ui__delete_file_no_confirmation-2.snap │ │ ├── diskonaut__tests__cases__ui__delete_file_no_confirmation-3.snap │ │ ├── diskonaut__tests__cases__ui__delete_file_no_confirmation-4.snap │ │ ├── diskonaut__tests__cases__ui__delete_file_no_confirmation-5.snap │ │ ├── diskonaut__tests__cases__ui__delete_file_no_confirmation-6.snap │ │ ├── diskonaut__tests__cases__ui__delete_file_no_confirmation-7.snap │ │ ├── diskonaut__tests__cases__ui__delete_file_no_confirmation.snap │ │ ├── diskonaut__tests__cases__ui__delete_file_press_n-2.snap │ │ ├── diskonaut__tests__cases__ui__delete_file_press_n-3.snap │ │ ├── diskonaut__tests__cases__ui__delete_file_press_n-4.snap │ │ ├── diskonaut__tests__cases__ui__delete_file_press_n-5.snap │ │ ├── diskonaut__tests__cases__ui__delete_file_press_n.snap │ │ ├── diskonaut__tests__cases__ui__delete_folder-2.snap │ │ ├── diskonaut__tests__cases__ui__delete_folder-3.snap │ │ ├── diskonaut__tests__cases__ui__delete_folder-4.snap │ │ ├── diskonaut__tests__cases__ui__delete_folder-5.snap │ │ ├── diskonaut__tests__cases__ui__delete_folder-6.snap │ │ ├── diskonaut__tests__cases__ui__delete_folder-7.snap │ │ ├── diskonaut__tests__cases__ui__delete_folder-8.snap │ │ ├── diskonaut__tests__cases__ui__delete_folder-9.snap │ │ ├── diskonaut__tests__cases__ui__delete_folder.snap │ │ ├── diskonaut__tests__cases__ui__delete_folder_no_confirmation-2.snap │ │ ├── diskonaut__tests__cases__ui__delete_folder_no_confirmation-3.snap │ │ ├── diskonaut__tests__cases__ui__delete_folder_no_confirmation-4.snap │ │ ├── diskonaut__tests__cases__ui__delete_folder_no_confirmation-5.snap │ │ ├── diskonaut__tests__cases__ui__delete_folder_no_confirmation-6.snap │ │ ├── diskonaut__tests__cases__ui__delete_folder_no_confirmation-7.snap │ │ ├── diskonaut__tests__cases__ui__delete_folder_no_confirmation-8.snap │ │ ├── diskonaut__tests__cases__ui__delete_folder_no_confirmation.snap │ │ ├── diskonaut__tests__cases__ui__delete_folder_small_window-10.snap │ │ ├── diskonaut__tests__cases__ui__delete_folder_small_window-2.snap │ │ ├── diskonaut__tests__cases__ui__delete_folder_small_window-3.snap │ │ ├── diskonaut__tests__cases__ui__delete_folder_small_window-4.snap │ │ ├── diskonaut__tests__cases__ui__delete_folder_small_window-5.snap │ │ ├── diskonaut__tests__cases__ui__delete_folder_small_window-6.snap │ │ ├── diskonaut__tests__cases__ui__delete_folder_small_window-7.snap │ │ ├── diskonaut__tests__cases__ui__delete_folder_small_window-8.snap │ │ ├── diskonaut__tests__cases__ui__delete_folder_small_window-9.snap │ │ ├── diskonaut__tests__cases__ui__delete_folder_small_window.snap │ │ ├── diskonaut__tests__cases__ui__delete_folder_small_window_no_confirmation-2.snap │ │ ├── diskonaut__tests__cases__ui__delete_folder_small_window_no_confirmation-3.snap │ │ ├── diskonaut__tests__cases__ui__delete_folder_small_window_no_confirmation-4.snap │ │ ├── diskonaut__tests__cases__ui__delete_folder_small_window_no_confirmation-5.snap │ │ ├── diskonaut__tests__cases__ui__delete_folder_small_window_no_confirmation-6.snap │ │ ├── diskonaut__tests__cases__ui__delete_folder_small_window_no_confirmation-7.snap │ │ ├── diskonaut__tests__cases__ui__delete_folder_small_window_no_confirmation-8.snap │ │ ├── diskonaut__tests__cases__ui__delete_folder_small_window_no_confirmation-9.snap │ │ ├── diskonaut__tests__cases__ui__delete_folder_small_window_no_confirmation.snap │ │ ├── diskonaut__tests__cases__ui__delete_folder_with_multiple_children-2.snap │ │ ├── diskonaut__tests__cases__ui__delete_folder_with_multiple_children-3.snap │ │ ├── diskonaut__tests__cases__ui__delete_folder_with_multiple_children-4.snap │ │ ├── diskonaut__tests__cases__ui__delete_folder_with_multiple_children-5.snap │ │ ├── diskonaut__tests__cases__ui__delete_folder_with_multiple_children-6.snap │ │ ├── diskonaut__tests__cases__ui__delete_folder_with_multiple_children-7.snap │ │ ├── diskonaut__tests__cases__ui__delete_folder_with_multiple_children-8.snap │ │ ├── diskonaut__tests__cases__ui__delete_folder_with_multiple_children-9.snap │ │ ├── diskonaut__tests__cases__ui__delete_folder_with_multiple_children.snap │ │ ├── diskonaut__tests__cases__ui__delete_folder_with_multiple_children_no_confirmation-2.snap │ │ ├── diskonaut__tests__cases__ui__delete_folder_with_multiple_children_no_confirmation-3.snap │ │ ├── diskonaut__tests__cases__ui__delete_folder_with_multiple_children_no_confirmation-4.snap │ │ ├── diskonaut__tests__cases__ui__delete_folder_with_multiple_children_no_confirmation-5.snap │ │ ├── diskonaut__tests__cases__ui__delete_folder_with_multiple_children_no_confirmation-6.snap │ │ ├── diskonaut__tests__cases__ui__delete_folder_with_multiple_children_no_confirmation-7.snap │ │ ├── diskonaut__tests__cases__ui__delete_folder_with_multiple_children_no_confirmation-8.snap │ │ ├── diskonaut__tests__cases__ui__delete_folder_with_multiple_children_no_confirmation.snap │ │ ├── diskonaut__tests__cases__ui__eleven_files-2.snap │ │ ├── diskonaut__tests__cases__ui__eleven_files.snap │ │ ├── diskonaut__tests__cases__ui__empty_folder-2.snap │ │ ├── diskonaut__tests__cases__ui__empty_folder.snap │ │ ├── diskonaut__tests__cases__ui__enter_folder-2.snap │ │ ├── diskonaut__tests__cases__ui__enter_folder-3.snap │ │ ├── diskonaut__tests__cases__ui__enter_folder-4.snap │ │ ├── diskonaut__tests__cases__ui__enter_folder-5.snap │ │ ├── diskonaut__tests__cases__ui__enter_folder-6.snap │ │ ├── diskonaut__tests__cases__ui__enter_folder-7.snap │ │ ├── diskonaut__tests__cases__ui__enter_folder.snap │ │ ├── diskonaut__tests__cases__ui__enter_folder_medium_width-2.snap │ │ ├── diskonaut__tests__cases__ui__enter_folder_medium_width-3.snap │ │ ├── diskonaut__tests__cases__ui__enter_folder_medium_width-4.snap │ │ ├── diskonaut__tests__cases__ui__enter_folder_medium_width-5.snap │ │ ├── diskonaut__tests__cases__ui__enter_folder_medium_width.snap │ │ ├── diskonaut__tests__cases__ui__enter_folder_small_width-2.snap │ │ ├── diskonaut__tests__cases__ui__enter_folder_small_width-3.snap │ │ ├── diskonaut__tests__cases__ui__enter_folder_small_width-4.snap │ │ ├── diskonaut__tests__cases__ui__enter_folder_small_width-5.snap │ │ ├── diskonaut__tests__cases__ui__enter_folder_small_width.snap │ │ ├── diskonaut__tests__cases__ui__enter_largest_folder_with_no_selected_tile-2.snap │ │ ├── diskonaut__tests__cases__ui__enter_largest_folder_with_no_selected_tile-3.snap │ │ ├── diskonaut__tests__cases__ui__enter_largest_folder_with_no_selected_tile.snap │ │ ├── diskonaut__tests__cases__ui__esc_to_go_up-10.snap │ │ ├── diskonaut__tests__cases__ui__esc_to_go_up-11.snap │ │ ├── diskonaut__tests__cases__ui__esc_to_go_up-12.snap │ │ ├── diskonaut__tests__cases__ui__esc_to_go_up-13.snap │ │ ├── diskonaut__tests__cases__ui__esc_to_go_up-2.snap │ │ ├── diskonaut__tests__cases__ui__esc_to_go_up-3.snap │ │ ├── diskonaut__tests__cases__ui__esc_to_go_up-4.snap │ │ ├── diskonaut__tests__cases__ui__esc_to_go_up-5.snap │ │ ├── diskonaut__tests__cases__ui__esc_to_go_up-6.snap │ │ ├── diskonaut__tests__cases__ui__esc_to_go_up-7.snap │ │ ├── diskonaut__tests__cases__ui__esc_to_go_up-8.snap │ │ ├── diskonaut__tests__cases__ui__esc_to_go_up-9.snap │ │ ├── diskonaut__tests__cases__ui__esc_to_go_up.snap │ │ ├── diskonaut__tests__cases__ui__files_with_size_zero-2.snap │ │ ├── diskonaut__tests__cases__ui__files_with_size_zero.snap │ │ ├── diskonaut__tests__cases__ui__medium_width-2.snap │ │ ├── diskonaut__tests__cases__ui__medium_width.snap │ │ ├── diskonaut__tests__cases__ui__minimum_tile_sides-2.snap │ │ ├── diskonaut__tests__cases__ui__minimum_tile_sides.snap │ │ ├── diskonaut__tests__cases__ui__move_down_and_enter_folder-2.snap │ │ ├── diskonaut__tests__cases__ui__move_down_and_enter_folder-3.snap │ │ ├── diskonaut__tests__cases__ui__move_down_and_enter_folder-4.snap │ │ ├── diskonaut__tests__cases__ui__move_down_and_enter_folder-5.snap │ │ ├── diskonaut__tests__cases__ui__move_down_and_enter_folder-6.snap │ │ ├── diskonaut__tests__cases__ui__move_down_and_enter_folder-7.snap │ │ ├── diskonaut__tests__cases__ui__move_down_and_enter_folder-8.snap │ │ ├── diskonaut__tests__cases__ui__move_down_and_enter_folder.snap │ │ ├── diskonaut__tests__cases__ui__move_left_and_enter_folder-2.snap │ │ ├── diskonaut__tests__cases__ui__move_left_and_enter_folder-3.snap │ │ ├── diskonaut__tests__cases__ui__move_left_and_enter_folder-4.snap │ │ ├── diskonaut__tests__cases__ui__move_left_and_enter_folder-5.snap │ │ ├── diskonaut__tests__cases__ui__move_left_and_enter_folder-6.snap │ │ ├── diskonaut__tests__cases__ui__move_left_and_enter_folder-7.snap │ │ ├── diskonaut__tests__cases__ui__move_left_and_enter_folder-8.snap │ │ ├── diskonaut__tests__cases__ui__move_left_and_enter_folder-9.snap │ │ ├── diskonaut__tests__cases__ui__move_left_and_enter_folder.snap │ │ ├── diskonaut__tests__cases__ui__move_right_and_enter_folder-2.snap │ │ ├── diskonaut__tests__cases__ui__move_right_and_enter_folder-3.snap │ │ ├── diskonaut__tests__cases__ui__move_right_and_enter_folder-4.snap │ │ ├── diskonaut__tests__cases__ui__move_right_and_enter_folder-5.snap │ │ ├── diskonaut__tests__cases__ui__move_right_and_enter_folder-6.snap │ │ ├── diskonaut__tests__cases__ui__move_right_and_enter_folder-7.snap │ │ ├── diskonaut__tests__cases__ui__move_right_and_enter_folder-8.snap │ │ ├── diskonaut__tests__cases__ui__move_right_and_enter_folder.snap │ │ ├── diskonaut__tests__cases__ui__move_up_and_enter_folder-2.snap │ │ ├── diskonaut__tests__cases__ui__move_up_and_enter_folder-3.snap │ │ ├── diskonaut__tests__cases__ui__move_up_and_enter_folder-4.snap │ │ ├── diskonaut__tests__cases__ui__move_up_and_enter_folder-5.snap │ │ ├── diskonaut__tests__cases__ui__move_up_and_enter_folder-6.snap │ │ ├── diskonaut__tests__cases__ui__move_up_and_enter_folder-7.snap │ │ ├── diskonaut__tests__cases__ui__move_up_and_enter_folder-8.snap │ │ ├── diskonaut__tests__cases__ui__move_up_and_enter_folder-9.snap │ │ ├── diskonaut__tests__cases__ui__move_up_and_enter_folder.snap │ │ ├── diskonaut__tests__cases__ui__noop_when_entering_file-2.snap │ │ ├── diskonaut__tests__cases__ui__noop_when_entering_file-3.snap │ │ ├── diskonaut__tests__cases__ui__noop_when_entering_file-4.snap │ │ ├── diskonaut__tests__cases__ui__noop_when_entering_file.snap │ │ ├── diskonaut__tests__cases__ui__noop_when_moving_off_screen_edges-2.snap │ │ ├── diskonaut__tests__cases__ui__noop_when_moving_off_screen_edges-3.snap │ │ ├── diskonaut__tests__cases__ui__noop_when_moving_off_screen_edges-4.snap │ │ ├── diskonaut__tests__cases__ui__noop_when_moving_off_screen_edges.snap │ │ ├── diskonaut__tests__cases__ui__noop_when_pressing_esc_at_base_folder-10.snap │ │ ├── diskonaut__tests__cases__ui__noop_when_pressing_esc_at_base_folder-11.snap │ │ ├── diskonaut__tests__cases__ui__noop_when_pressing_esc_at_base_folder-12.snap │ │ ├── diskonaut__tests__cases__ui__noop_when_pressing_esc_at_base_folder-13.snap │ │ ├── diskonaut__tests__cases__ui__noop_when_pressing_esc_at_base_folder-14.snap │ │ ├── diskonaut__tests__cases__ui__noop_when_pressing_esc_at_base_folder-15.snap │ │ ├── diskonaut__tests__cases__ui__noop_when_pressing_esc_at_base_folder-16.snap │ │ ├── diskonaut__tests__cases__ui__noop_when_pressing_esc_at_base_folder-17.snap │ │ ├── diskonaut__tests__cases__ui__noop_when_pressing_esc_at_base_folder-18.snap │ │ ├── diskonaut__tests__cases__ui__noop_when_pressing_esc_at_base_folder-2.snap │ │ ├── diskonaut__tests__cases__ui__noop_when_pressing_esc_at_base_folder-3.snap │ │ ├── diskonaut__tests__cases__ui__noop_when_pressing_esc_at_base_folder-4.snap │ │ ├── diskonaut__tests__cases__ui__noop_when_pressing_esc_at_base_folder-5.snap │ │ ├── diskonaut__tests__cases__ui__noop_when_pressing_esc_at_base_folder-6.snap │ │ ├── diskonaut__tests__cases__ui__noop_when_pressing_esc_at_base_folder-7.snap │ │ ├── diskonaut__tests__cases__ui__noop_when_pressing_esc_at_base_folder-8.snap │ │ ├── diskonaut__tests__cases__ui__noop_when_pressing_esc_at_base_folder-9.snap │ │ ├── diskonaut__tests__cases__ui__noop_when_pressing_esc_at_base_folder.snap │ │ ├── diskonaut__tests__cases__ui__permission_denied_when_deleting-10.snap │ │ ├── diskonaut__tests__cases__ui__permission_denied_when_deleting-2.snap │ │ ├── diskonaut__tests__cases__ui__permission_denied_when_deleting-3.snap │ │ ├── diskonaut__tests__cases__ui__permission_denied_when_deleting-4.snap │ │ ├── diskonaut__tests__cases__ui__permission_denied_when_deleting-5.snap │ │ ├── diskonaut__tests__cases__ui__permission_denied_when_deleting-6.snap │ │ ├── diskonaut__tests__cases__ui__permission_denied_when_deleting-7.snap │ │ ├── diskonaut__tests__cases__ui__permission_denied_when_deleting-8.snap │ │ ├── diskonaut__tests__cases__ui__permission_denied_when_deleting-9.snap │ │ ├── diskonaut__tests__cases__ui__permission_denied_when_deleting.snap │ │ ├── diskonaut__tests__cases__ui__permission_denied_when_deleting_no_confirmation-2.snap │ │ ├── diskonaut__tests__cases__ui__permission_denied_when_deleting_no_confirmation-3.snap │ │ ├── diskonaut__tests__cases__ui__permission_denied_when_deleting_no_confirmation-4.snap │ │ ├── diskonaut__tests__cases__ui__permission_denied_when_deleting_no_confirmation-5.snap │ │ ├── diskonaut__tests__cases__ui__permission_denied_when_deleting_no_confirmation-6.snap │ │ ├── diskonaut__tests__cases__ui__permission_denied_when_deleting_no_confirmation-7.snap │ │ ├── diskonaut__tests__cases__ui__permission_denied_when_deleting_no_confirmation-8.snap │ │ ├── diskonaut__tests__cases__ui__permission_denied_when_deleting_no_confirmation-9.snap │ │ ├── diskonaut__tests__cases__ui__permission_denied_when_deleting_no_confirmation.snap │ │ ├── diskonaut__tests__cases__ui__pressing_delete_with_no_selected_tile-2.snap │ │ ├── diskonaut__tests__cases__ui__pressing_delete_with_no_selected_tile.snap │ │ ├── diskonaut__tests__cases__ui__small_files-2.snap │ │ ├── diskonaut__tests__cases__ui__small_files.snap │ │ ├── diskonaut__tests__cases__ui__small_files_non_square.snap │ │ ├── diskonaut__tests__cases__ui__small_files_with_x_as_zero-2.snap │ │ ├── diskonaut__tests__cases__ui__small_files_with_x_as_zero.snap │ │ ├── diskonaut__tests__cases__ui__small_files_with_y_as_zero-2.snap │ │ ├── diskonaut__tests__cases__ui__small_files_with_y_as_zero.snap │ │ ├── diskonaut__tests__cases__ui__small_width-2.snap │ │ ├── diskonaut__tests__cases__ui__small_width.snap │ │ ├── diskonaut__tests__cases__ui__small_width_long_folder_name-2.snap │ │ ├── diskonaut__tests__cases__ui__small_width_long_folder_name.snap │ │ ├── diskonaut__tests__cases__ui__too_small_height.snap │ │ ├── diskonaut__tests__cases__ui__too_small_width_five.snap │ │ ├── diskonaut__tests__cases__ui__too_small_width_four.snap │ │ ├── diskonaut__tests__cases__ui__too_small_width_one.snap │ │ ├── diskonaut__tests__cases__ui__too_small_width_three.snap │ │ ├── diskonaut__tests__cases__ui__too_small_width_two.snap │ │ ├── diskonaut__tests__cases__ui__two_large_files_one_small_file-2.snap │ │ ├── diskonaut__tests__cases__ui__two_large_files_one_small_file.snap │ │ ├── diskonaut__tests__cases__ui__zoom_into_small_files-2.snap │ │ ├── diskonaut__tests__cases__ui__zoom_into_small_files-3.snap │ │ ├── diskonaut__tests__cases__ui__zoom_into_small_files-4.snap │ │ ├── diskonaut__tests__cases__ui__zoom_into_small_files-5.snap │ │ ├── diskonaut__tests__cases__ui__zoom_into_small_files-6.snap │ │ ├── diskonaut__tests__cases__ui__zoom_into_small_files-7.snap │ │ ├── diskonaut__tests__cases__ui__zoom_into_small_files-8.snap │ │ └── diskonaut__tests__cases__ui__zoom_into_small_files.snap │ ├── test_utils.rs │ └── ui.rs ├── fakes │ ├── fake_input.rs │ ├── fake_output.rs │ └── mod.rs └── mod.rs └── ui ├── bottom_line.rs ├── display.rs ├── format ├── display_size.rs ├── mod.rs └── truncate.rs ├── grid ├── draw_next_symbol.rs ├── draw_rect.rs ├── mod.rs └── rectangle_grid.rs ├── mod.rs ├── modals ├── confirm_box.rs ├── error_box.rs ├── message_box.rs ├── mod.rs └── warning_box.rs ├── term_too_small.rs └── title ├── mod.rs ├── title_line.rs └── title_telescope.rs /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [imsnif] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cargo/ 2 | target/ 3 | vendor/ 4 | vendor.tar 5 | **/*.rs.bk 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: 2 | - linux 3 | - osx 4 | - windows 5 | language: rust 6 | rust: 7 | - stable 8 | - beta 9 | before_script: 10 | - rustup component add rustfmt 11 | script: 12 | - set -e # Abort on failure, see https://github.com/travis-ci/travis-ci/issues/1066 13 | - cargo fmt -- --check 14 | - cargo build --verbose 15 | - cargo test --verbose 16 | matrix: 17 | include: 18 | - os: linux 19 | rust: nightly 20 | before_script: 21 | - rustup component add rustfmt 22 | - os: osx 23 | rust: nightly 24 | before_script: 25 | - rustup component add rustfmt 26 | - os: windows 27 | rust: nightly 28 | before_script: 29 | - rustup component add rustfmt 30 | 31 | allow_failures: 32 | - rust: nightly 33 | 34 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) 6 | 7 | ## [Unreleased] 8 | 9 | ### Added 10 | * Only show "Small Files" legend when there are small files on screen (https://github.com/imsnif/diskonaut/pull/75) - [@pjsier](https://github.com/pjsier) 11 | 12 | ## [0.11.0] - 2020-09-23 13 | 14 | ### Added 15 | * Windows support (https://github.com/imsnif/diskonaut/pull/74) - [@pm100](https://github.com/pm100) 16 | 17 | ## [0.10.0] - 2020-09-11 18 | 19 | ### Added 20 | * Add `--disable-delete-confirmation` flag to not immediately delete files without a prompt (https://github.com/imsnif/diskonaut/pull/71) - [@markafarrell](https://github.com/markafarrell) 21 | 22 | ## [0.9.0] - 2020-07-12 23 | 24 | ### Added 25 | * Add `--apparent-size` flag to show actual file size rather than file size on disk (https://github.com/imsnif/diskonaut/pull/66) - [@imsnif](https://github.com/imsnif) 26 | 27 | ## [0.8.0] - 2020-07-09 28 | 29 | ### Fixed 30 | * Change delete key to BACKSPACE for cross platform support (https://github.com/imsnif/diskonaut/pull/64) - [@maxheyer](https://github.com/maxheyer) 31 | * Do not crash with extremely large files/folders (https://github.com/imsnif/diskonaut/pull/63) - [@Freaky](https://github.com/Freaky) 32 | 33 | ## [0.7.0] - 2020-07-04 34 | 35 | ### Added 36 | * Show warning when trying to delete while still scanning (https://github.com/imsnif/diskonaut/pull/60) - [@mhdmhsni](https://github.com/mhdmhsni) 37 | * Add ability to zoom in and out (eg. to see small files) (https://github.com/imsnif/diskonaut/pull/61) - [@imsnif](https://github.com/imsnif) 38 | 39 | ## [0.6.0] - 2020-07-03 40 | 41 | ### Added 42 | * Add a visual indication when running as root (https://github.com/imsnif/diskonaut/pull/57) - [@c3st7n](https://github.com/c3st7n) 43 | * Change delete key to DELETE (https://github.com/imsnif/diskonaut/pull/59) - [@maxheyer](https://github.com/maxheyer) 44 | 45 | ## [0.5.0] - 2020-06-27 46 | 47 | ### Added 48 | * Add an "Are you sure you want to quit?" modal (https://github.com/imsnif/diskonaut/pull/44) - [@mhdmhsni](https://github.com/mhdmhsni) 49 | 50 | ### Fixed 51 | * Fix some small_files rendering edge-cases (https://github.com/imsnif/diskonaut/pull/55) - [@imsnif](https://github.com/imsnif) 52 | 53 | ## [0.4.0] - 2020-06-26 54 | 55 | ### Added 56 | * Support emacs keybindings (https://github.com/imsnif/diskonaut/pull/40) - [@redzic](https://github.com/redzic) 57 | * Make enter select largest folder if nothing is selected (https://github.com/imsnif/diskonaut/pull/45) - [@redzic](https://github.com/redzic) 58 | * Keep track of tile selection in previous folder (https://github.com/imsnif/diskonaut/pull/53) - [@therealprof](https://github.com/therealprof) 59 | 60 | ### Fixed 61 | * Do not scan in parallel when running tests (https://github.com/imsnif/diskonaut/pull/43) - [@redzic](https://github.com/redzic) 62 | * Prevent crashes for multibyte characters on grid (https://github.com/imsnif/diskonaut/pull/51) - [@goto-bus-stop](https://github.com/goto-bus-stop) 63 | * Show quit shortcut in legend (https://github.com/imsnif/diskonaut/pull/46) - [@olehs0](https://github.com/olehs0) 64 | 65 | ## [0.3.0] - 2020-06-21 66 | 67 | ### Fixed 68 | * Remove unneeded dev dependency (https://github.com/imsnif/diskonaut/pull/35) - [@ignatenkobrain](https://github.com/ignatenkobrain) 69 | * Improve scanning speed (https://github.com/imsnif/diskonaut/pull/38) - [@imsnif](https://github.com/imsnif) 70 | * Refactor movement methods (https://github.com/imsnif/diskonaut/pull/31) - [@phimuemue](https://github.com/phimuemue) 71 | 72 | ## [0.2.0] - 2020-06-18 73 | 74 | ### Fixed 75 | * Cross platform file size calculation (https://github.com/imsnif/diskonaut/pull/28) - [@Freaky](https://github.com/Freaky) 76 | * Bumped insta dependency to 0.16.0, bumped cargo-insta dependency to 0.16.0 (https://github.com/imsnif/diskonaut/pull/25) - [@tim77](https://github.com/tim77) 77 | * Bumped tui dependency to 0.9 (https://github.com/imsnif/diskonaut/pull/30) - [@silwol](https://github.com/silwol) 78 | 79 | ## [0.1.0] - 2020-06-17 80 | 81 | Initial release with all the things. 82 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at aram@poor.dev. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute to Diskonaut 2 | You can contribute to `diskonaut` in several ways, all of which would be very much appreciated. 3 | Following are a few guidelines that might help you find your way around. 4 | 5 | Please note that Diskonaut has a Code of Conduct (you can find it in the root of the repository). 6 | It is there so that we can have a safe and pleasant environment that encourages acceptance, participation and kindness. 7 | 8 | ## Have you found a bug? 9 | If Diskonaut is not working for you, or you found something that is not behaving as it should, it would be great if you could let us know about it. 10 | To do this, please open an issue in the repository and provide as much relevant information as possible. Ideally, also a way to reproduce this bug. 11 | If you would like to try and fix this bug yourself, please open a pull request (for more information, see the section of this document regarding code contributions). 12 | 13 | ## Do you have an idea for a new feature? 14 | `diskonaut` can always be improved, and one of the best ways to improve it is by having its users suggest new features and different ways it can behave. 15 | If you'd like to make such a suggestion, please open an issue detailing it. In this issue others would be able to comment on the suggestion. 16 | Finally, you or someone else would be able to implement this suggestion if it is decided to do so. 17 | 18 | ## Would you like to make a code contribution? 19 | Code contributions to `diskonaut` are very welcome and encouraged. If you're unsure what to work on, a good place to start is the "Help Wanted" or "Good First Issue" tags in the issues of this repository. 20 | 21 | Following is some information you might find useful regarding the particularities of contributing to `diskonaut`: 22 | 23 | ### Testing 24 | Diskonaut uses automated integration tests to make sure everything is working as it should. 25 | If you're adding new functionality, you might want to add new tests or adjust the existing ones. 26 | 27 | These tests work by creating textual "snapshots" of how the UI looks in certain situations. One test can have several snapshots. 28 | 29 | An example test would be: 30 | 1. Create a temporary folder with a few files and subfolders. 31 | 2. Run diskonaut on that folder and take a snapshot of its UI. Make sure that snapshot is identical to the snapshot stored in this repository for that test. 32 | 3. Enter one of the subfolders, take another snapshot and make sure it is identical to the second stored snapshot for that test. 33 | 34 | To store and compare the snapshots, we use [`insta`](https://docs.rs/insta/0.8.1/insta/) 35 | 36 | For an improved experience, you can install `cargo-insta` on your computer, which will allow you to review new snapshots - approving or rejecting them. Please see the insta documentation for more details. 37 | 38 | ### Code formatting 39 | `diskonaut` uses [rustfmt](https://github.com/rust-lang/rustfmt) for code formatting. In most (all?) cases, if you have it installed on your computer, you can run `cargo fmt` in the project folder, and it will auto fix your code. 40 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "diskonaut" 3 | description = "Terminal disk space visual navigator" 4 | version = "0.11.0" 5 | authors = ["Aram Drevekenin "] 6 | readme = "README.md" 7 | homepage = "https://github.com/imsnif/diskonaut" 8 | repository = "https://github.com/imsnif/diskonaut" 9 | license = "MIT" 10 | edition = "2018" 11 | 12 | [dependencies] 13 | tui = {version="0.11", default-features = false, features = ['crossterm'] } 14 | crossterm = "0.17" 15 | failure = "0.1" 16 | jwalk = "0.5" 17 | structopt = "0.3" 18 | filesize = "0.2.0" 19 | unicode-width = "0.1.7" 20 | nix = "0.17.0" 21 | [target.'cfg(windows)'.dependencies] 22 | winapi = { version = "0.3", features = ["securitybaseapi","debugapi"] } 23 | 24 | [dev-dependencies] 25 | insta = "0.16.0" 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Aram Drevekenin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # diskonaut 2 | ![demo](demo.gif) 3 | 4 | ## How does it work? 5 | 6 | Given a path on your hard-drive (which could also be the root path, eg. `/`). `diskonaut` scans it and indexes its metadata to memory so that you could explore its contents (even while still scanning!). 7 | 8 | Once completed, you can navigate through subfolders, getting a visual treemap representation of what's taking up your disk space. You can even delete files or folders and `diskonaut` will track how much space you've freed up in this session. 9 | 10 | ## Installation 11 | 12 | ### Download a prebuilt binary 13 | If you're using linux, you can check out the "releases" of this repository to download the latest prebuilt binary. 14 | 15 | ### With cargo (linux / macOS / windows) 16 | ``` 17 | cargo install diskonaut 18 | ``` 19 | 20 | ### Fedora/CentOS 21 | 22 | ``` 23 | sudo dnf install diskonaut 24 | ``` 25 | For older Fedora releases and CentOS available in [COPR](https://copr.fedorainfracloud.org/coprs/atim/diskonaut/): 26 | ``` 27 | sudo dnf copr enable atim/diskonaut -y 28 | sudo dnf install diskonaut 29 | ``` 30 | 31 | ### Arch Linux 32 | 33 | Available in the [AUR](https://aur.archlinux.org/packages/diskonaut): 34 | 35 | ``` 36 | yay diskonaut 37 | ``` 38 | 39 | ### Gentoo 40 | 41 | Available through the following overlay as `sys-fs/diskonaut`: 42 | 43 | https://github.com/telans/EBUILDS 44 | 45 | ### Nix / NixOS 46 | Available in [nixpkgs](https://github.com/NixOS/nixpkgs/blob/master/pkgs/tools/misc/diskonaut/default.nix): 47 | ``` 48 | $ nix-env --install diskonaut 49 | $ # (Or using the attribute name, which is also diskonaut.) 50 | ``` 51 | 52 | ### FreeBSD 53 | Available in [freshports](https://www.freshports.org/sysutils/diskonaut/): 54 | ``` 55 | pkg install diskonaut 56 | ``` 57 | 58 | ### macOS 59 | Available in [Homebrew](https://formulae.brew.sh/formula/diskonaut) 60 | ``` 61 | brew install diskonaut 62 | ``` 63 | Also available in [nixpkgs](https://github.com/NixOS/nixpkgs/blob/master/pkgs/tools/misc/diskonaut/default.nix): see Nix section for more details. 64 | 65 | ## Supported platforms 66 | Right now `diskonaut` supports linux, macos and windows. 67 | 68 | ## Usage 69 | Either start `diskonaut` in the folder you want to scan, or provide it with the folder you'd like to scan as an argument: 70 | ``` 71 | $ diskonaut /home/aram 72 | ``` 73 | 74 | ## Contributing 75 | Contributions of any kind are very much welcome. If you think `diskonaut` is cool and you'd like to hack at it, feel free to look through the issues. Take a look especially at ones marked "help wanted" or "good first issue". 76 | Also, if you found a bug or have an idea for a new feature, please feel free to open an issue to discuss it. 77 | 78 | For more detailed information, please see the CONTRIBUTING.md file at the root of this repository. 79 | 80 | If at any point you feel stuck, are unsure how to begin or what to work on, please do not hesitate to reach out to me personally: aram@poor.dev 81 | 82 | ## License 83 | MIT 84 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imsnif/diskonaut/65cd829069e275dc8a3e4493d0da7f8f08055d82/demo.gif -------------------------------------------------------------------------------- /src/input/controls.rs: -------------------------------------------------------------------------------- 1 | use ::tui::backend::Backend; 2 | use crossterm::event::Event; 3 | use crossterm::event::KeyModifiers; 4 | use crossterm::event::{read, KeyCode, KeyEvent}; 5 | 6 | use crate::state::FileToDelete; 7 | use crate::App; 8 | 9 | #[derive(Clone)] 10 | pub struct TerminalEvents; 11 | 12 | impl Iterator for TerminalEvents { 13 | type Item = Event; 14 | fn next(&mut self) -> Option { 15 | Some(read().unwrap()) 16 | } 17 | } 18 | macro_rules! key { 19 | (char $x:expr) => { 20 | Event::Key(KeyEvent { 21 | code: KeyCode::Char($x), 22 | modifiers: KeyModifiers::NONE, 23 | }) 24 | }; 25 | (shift $x:expr) => { 26 | Event::Key(KeyEvent { 27 | code: KeyCode::Char($x), 28 | modifiers: KeyModifiers::SHIFT, 29 | }) 30 | }; 31 | (ctrl $x:expr) => { 32 | Event::Key(KeyEvent { 33 | code: KeyCode::Char($x), 34 | modifiers: KeyModifiers::CONTROL, 35 | }) 36 | }; 37 | ($x:ident) => { 38 | Event::Key(KeyEvent { 39 | code: KeyCode::$x, 40 | modifiers: KeyModifiers::NONE, 41 | }) 42 | }; 43 | } 44 | 45 | pub fn handle_keypress_loading_mode(evt: Event, app: &mut App) { 46 | match evt { 47 | key!(ctrl 'c') | key!(char 'q') => { 48 | app.prompt_exit(); 49 | } 50 | key!(char 'l') | key!(Right) | key!(ctrl 'f') => { 51 | app.move_selected_right(); 52 | } 53 | key!(char 'h') | key!(Left) | key!(ctrl 'b') => { 54 | app.move_selected_left(); 55 | } 56 | key!(char 'j') | key!(Down) | key!(ctrl 'n') => { 57 | app.move_selected_down(); 58 | } 59 | key!(char 'k') | key!(Up) | key!(ctrl 'p') => { 60 | app.move_selected_up(); 61 | } 62 | key!(char '+') | key!(shift '+') => { 63 | app.zoom_in(); 64 | } 65 | key!(char '-') => { 66 | app.zoom_out(); 67 | } 68 | key!(char '0') => { 69 | app.reset_zoom(); 70 | } 71 | key!(char '\n') | key!(Enter) => { 72 | app.handle_enter(); 73 | } 74 | key!(Backspace) => { 75 | app.show_warning_modal(); 76 | } 77 | key!(Esc) => { 78 | app.go_up(); 79 | } 80 | _ => (), 81 | }; 82 | } 83 | 84 | pub fn handle_keypress_normal_mode(evt: Event, app: &mut App) { 85 | match evt { 86 | key!(ctrl 'c') | key!(char 'q') => { 87 | app.prompt_exit(); 88 | } 89 | key!(Backspace) => { 90 | app.prompt_file_deletion(); 91 | } 92 | key!(char 'l') | key!(Right) | key!(ctrl 'f') => { 93 | app.move_selected_right(); 94 | } 95 | key!(char 'h') | key!(Left) | key!(ctrl 'b') => { 96 | app.move_selected_left(); 97 | } 98 | key!(char 'j') | key!(Down) | key!(ctrl 'n') => { 99 | app.move_selected_down(); 100 | } 101 | key!(char 'k') | key!(Up) | key!(ctrl 'p') => { 102 | app.move_selected_up(); 103 | } 104 | key!(char '+') | key!(shift '+') => { 105 | app.zoom_in(); 106 | } 107 | key!(char '-') => { 108 | app.zoom_out(); 109 | } 110 | key!(char '0') => { 111 | app.reset_zoom(); 112 | } 113 | key!(char '\n') | key!(Enter) => { 114 | app.handle_enter(); 115 | } 116 | key!(Esc) => { 117 | app.go_up(); 118 | } 119 | _ => (), 120 | }; 121 | } 122 | 123 | pub fn handle_keypress_delete_file_mode( 124 | evt: Event, 125 | app: &mut App, 126 | file_to_delete: FileToDelete, 127 | ) { 128 | match evt { 129 | key!(ctrl 'c') | key!(char 'q') | key!(Esc) | key!(char 'n') => { 130 | app.normal_mode(); 131 | } 132 | key!(char 'y') => { 133 | app.delete_file(&file_to_delete); 134 | } 135 | _ => (), 136 | }; 137 | } 138 | 139 | pub fn handle_keypress_error_message(evt: Event, app: &mut App) { 140 | match evt { 141 | key!(ctrl 'c') | key!(char 'q') | key!(Esc) => { 142 | app.normal_mode(); 143 | } 144 | _ => (), 145 | }; 146 | } 147 | 148 | pub fn handle_keypress_screen_too_small(evt: Event, app: &mut App) { 149 | match evt { 150 | key!(ctrl 'c') | key!(char 'q') => { 151 | app.exit(); 152 | } 153 | _ => (), 154 | }; 155 | } 156 | 157 | pub fn handle_keypress_exiting_mode(evt: Event, app: &mut App) { 158 | match evt { 159 | key!(ctrl 'c') | key!(char 'q') | key!(Esc) | key!(char 'n') => { 160 | app.reset_ui_mode(); 161 | // we have to manually call render here to make sure ui gets updated 162 | // because reset_ui_mode does not call it itself 163 | app.render(); 164 | } 165 | key!(char 'y') => { 166 | app.exit(); 167 | } 168 | _ => (), 169 | }; 170 | } 171 | 172 | pub fn handle_keypress_warning_message(evt: Event, app: &mut App) { 173 | match evt { 174 | _ => { 175 | app.reset_ui_mode(); 176 | } 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/input/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod controls; 2 | 3 | pub use controls::*; 4 | -------------------------------------------------------------------------------- /src/messages/event.rs: -------------------------------------------------------------------------------- 1 | use ::std::thread::park_timeout; 2 | use ::std::time; 3 | 4 | use crate::messages::Instruction; 5 | 6 | pub enum Event { 7 | PathError, 8 | FileDeleted, 9 | AppExit, 10 | } 11 | 12 | use std::sync::mpsc::{Receiver, SyncSender}; 13 | 14 | pub fn handle_events(event_receiver: Receiver, instruction_sender: SyncSender) { 15 | loop { 16 | let event = event_receiver 17 | .recv() 18 | .expect("failed to receive event on channel"); 19 | match event { 20 | Event::PathError => { 21 | let _ = instruction_sender.send(Instruction::SetPathToRed); 22 | let _ = instruction_sender.send(Instruction::Render); 23 | park_timeout(time::Duration::from_millis(250)); 24 | let _ = instruction_sender.send(Instruction::ResetCurrentPathColor); 25 | let _ = instruction_sender.send(Instruction::Render); 26 | } 27 | Event::FileDeleted => { 28 | let _ = instruction_sender.send(Instruction::FlashSpaceFreed); 29 | let _ = instruction_sender.send(Instruction::Render); 30 | park_timeout(time::Duration::from_millis(250)); 31 | let _ = instruction_sender.send(Instruction::UnflashSpaceFreed); 32 | let _ = instruction_sender.send(Instruction::Render); 33 | } 34 | Event::AppExit => { 35 | break; 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/messages/instruction.rs: -------------------------------------------------------------------------------- 1 | use ::std::fs::Metadata; 2 | use ::std::path::PathBuf; 3 | use ::std::sync::mpsc::Receiver; 4 | 5 | use ::tui::backend::Backend; 6 | use crossterm::event::Event as BackEvent; 7 | 8 | use crate::input::{ 9 | handle_keypress_delete_file_mode, handle_keypress_error_message, handle_keypress_exiting_mode, 10 | handle_keypress_loading_mode, handle_keypress_normal_mode, handle_keypress_screen_too_small, 11 | handle_keypress_warning_message, 12 | }; 13 | use crate::{App, UiMode}; 14 | 15 | pub enum Instruction { 16 | SetPathToRed, 17 | ResetCurrentPathColor, 18 | FlashSpaceFreed, 19 | UnflashSpaceFreed, 20 | AddEntryToBaseFolder((Metadata, PathBuf)), 21 | StartUi, 22 | ToggleScanningVisualIndicator, 23 | RenderAndUpdateBoard, 24 | Render, 25 | ResetUiMode, 26 | Keypress(BackEvent), 27 | IncrementFailedToRead, 28 | } 29 | 30 | pub fn handle_instructions(app: &mut App, receiver: Receiver) 31 | where 32 | B: Backend, 33 | { 34 | loop { 35 | let instruction = receiver 36 | .recv() 37 | .expect("failed to receive instruction on channel"); 38 | match instruction { 39 | Instruction::SetPathToRed => { 40 | app.set_path_to_red(); 41 | } 42 | Instruction::ResetCurrentPathColor => { 43 | app.reset_current_path_color(); 44 | } 45 | Instruction::FlashSpaceFreed => { 46 | app.flash_space_freed(); 47 | } 48 | Instruction::UnflashSpaceFreed => { 49 | app.unflash_space_freed(); 50 | } 51 | Instruction::AddEntryToBaseFolder((file_metadata, entry)) => { 52 | app.add_entry_to_base_folder(&file_metadata, entry); 53 | } 54 | Instruction::StartUi => { 55 | app.start_ui(); 56 | } 57 | Instruction::ToggleScanningVisualIndicator => { 58 | app.increment_loading_progress_indicator(); 59 | } 60 | Instruction::RenderAndUpdateBoard => { 61 | app.render_and_update_board(); 62 | } 63 | Instruction::Render => { 64 | app.render(); 65 | } 66 | Instruction::ResetUiMode => { 67 | app.reset_ui_mode(); 68 | } 69 | Instruction::Keypress(evt) => { 70 | match &app.ui_mode { 71 | UiMode::Loading => { 72 | handle_keypress_loading_mode(evt, app); 73 | } 74 | UiMode::Normal => { 75 | handle_keypress_normal_mode(evt, app); 76 | } 77 | UiMode::ScreenTooSmall => { 78 | handle_keypress_screen_too_small(evt, app); 79 | } 80 | UiMode::DeleteFile(file_to_delete) => { 81 | let file_to_delete = file_to_delete.clone(); 82 | handle_keypress_delete_file_mode(evt, app, file_to_delete); 83 | } 84 | UiMode::ErrorMessage(_) => { 85 | handle_keypress_error_message(evt, app); 86 | } 87 | UiMode::Exiting { app_loaded: _ } => { 88 | handle_keypress_exiting_mode(evt, app); 89 | } 90 | UiMode::WarningMessage(_) => { 91 | handle_keypress_warning_message(evt, app); 92 | } 93 | } 94 | if !app.is_running { 95 | break; 96 | } 97 | } 98 | Instruction::IncrementFailedToRead => { 99 | app.increment_failed_to_read(); 100 | } 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/messages/mod.rs: -------------------------------------------------------------------------------- 1 | mod event; 2 | mod instruction; 3 | 4 | pub use event::*; 5 | pub use instruction::*; 6 | -------------------------------------------------------------------------------- /src/os/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(target_os = "windows")] 2 | pub(crate) mod windows; 3 | 4 | #[cfg(not(target_os = "windows"))] 5 | pub(crate) mod unix; 6 | -------------------------------------------------------------------------------- /src/os/unix.rs: -------------------------------------------------------------------------------- 1 | use nix::unistd::geteuid; 2 | 3 | pub(crate) fn is_user_admin() -> bool { 4 | geteuid().is_root() 5 | } 6 | -------------------------------------------------------------------------------- /src/os/windows.rs: -------------------------------------------------------------------------------- 1 | #[cfg(not(test))] 2 | use winapi::um::winnt::{ 3 | DOMAIN_ALIAS_RID_ADMINS, PVOID, SECURITY_BUILTIN_DOMAIN_RID, SECURITY_NT_AUTHORITY, 4 | SID_IDENTIFIER_AUTHORITY, 5 | }; 6 | 7 | #[cfg(not(test))] 8 | use winapi::um::securitybaseapi::{AllocateAndInitializeSid, CheckTokenMembership}; 9 | // https://stackoverflow.com/questions/4230602/detect-if-program-is-running-with-full-administrator-rights 10 | #[cfg(not(test))] 11 | pub(crate) fn is_user_admin() -> bool { 12 | let mut auth_nt = SID_IDENTIFIER_AUTHORITY { 13 | Value: SECURITY_NT_AUTHORITY, 14 | }; 15 | let mut admingroup = 0 as PVOID; 16 | let ismember = unsafe { 17 | assert!( 18 | AllocateAndInitializeSid( 19 | &mut auth_nt, 20 | 2, 21 | SECURITY_BUILTIN_DOMAIN_RID, 22 | DOMAIN_ALIAS_RID_ADMINS, 23 | 0, 24 | 0, 25 | 0, 26 | 0, 27 | 0, 28 | 0, 29 | &mut admingroup, 30 | ) != 0 31 | ); 32 | let mut member: i32 = 0; 33 | assert!(CheckTokenMembership(0 as PVOID, admingroup, &mut member) != 0); 34 | member != 0 35 | }; 36 | ismember 37 | } 38 | #[cfg(test)] 39 | pub(crate) fn is_user_admin() -> bool { 40 | false 41 | } 42 | -------------------------------------------------------------------------------- /src/state/file_to_delete.rs: -------------------------------------------------------------------------------- 1 | use ::std::ffi::OsString; 2 | use ::std::path::PathBuf; 3 | 4 | use crate::state::tiles::FileType; 5 | 6 | #[derive(Clone)] 7 | pub struct FileToDelete { 8 | pub path_in_filesystem: PathBuf, 9 | pub path_to_file: Vec, 10 | pub file_type: FileType, 11 | pub num_descendants: Option, 12 | pub size: u128, 13 | } 14 | 15 | impl FileToDelete { 16 | pub fn full_path(&self) -> PathBuf { 17 | let mut full_path = self.path_in_filesystem.clone(); 18 | for component in &self.path_to_file { 19 | full_path.push(component); 20 | } 21 | full_path 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/state/files/file_tree.rs: -------------------------------------------------------------------------------- 1 | use ::std::ffi::{OsStr, OsString}; 2 | use ::std::fs::Metadata; 3 | use ::std::path::{Path, PathBuf}; 4 | 5 | use crate::state::files::{FileOrFolder, Folder}; 6 | use crate::state::FileToDelete; 7 | 8 | pub struct FileTree { 9 | pub current_folder_names: Vec, 10 | pub space_freed: u128, 11 | pub failed_to_read: u64, 12 | pub path_in_filesystem: PathBuf, 13 | base_folder: Folder, 14 | show_apparent_size: bool, 15 | } 16 | 17 | impl FileTree { 18 | pub fn new(base_folder: Folder, path_in_filesystem: PathBuf, show_apparent_size: bool) -> Self { 19 | FileTree { 20 | base_folder, 21 | current_folder_names: Vec::new(), 22 | path_in_filesystem, 23 | space_freed: 0, 24 | failed_to_read: 0, 25 | show_apparent_size, 26 | } 27 | } 28 | pub fn get_total_size(&self) -> u128 { 29 | self.base_folder.size 30 | } 31 | pub fn get_total_descendants(&self) -> u64 { 32 | self.base_folder.num_descendants 33 | } 34 | pub fn get_current_folder(&self) -> &Folder { 35 | if self.current_folder_names.is_empty() { 36 | &self.base_folder 37 | } else if let Some(FileOrFolder::Folder(current_folder)) = 38 | self.base_folder.path(self.current_folder_names.clone()) 39 | { 40 | ¤t_folder 41 | } else { 42 | // here we have something in current_folder_names but the last 43 | // one is somehow not a folder... this is a corrupted state 44 | unreachable!("couldn't find current folder size") 45 | } 46 | } 47 | pub fn get_current_folder_size(&self) -> u128 { 48 | self.get_current_folder().size 49 | } 50 | pub fn get_current_path(&self) -> PathBuf { 51 | let mut full_path = PathBuf::from(&self.path_in_filesystem); 52 | for folder in &self.current_folder_names { 53 | full_path.push(&folder) 54 | } 55 | full_path 56 | } 57 | pub fn item_in_current_folder(&self, item_name: &OsStr) -> Option<&FileOrFolder> { 58 | let current_folder = &self.get_current_folder(); 59 | current_folder.path(vec![item_name.to_os_string()]) 60 | } 61 | pub fn enter_folder(&mut self, folder_name: &OsStr) { 62 | self.current_folder_names.push(folder_name.to_os_string()); 63 | } 64 | pub fn leave_folder(&mut self) -> bool { 65 | // true => succeeded, false => at base folder 66 | self.current_folder_names.pop().is_some() 67 | } 68 | pub fn delete_file(&mut self, file_to_delete: &FileToDelete) { 69 | let path_to_delete = &file_to_delete.path_to_file; 70 | self.base_folder.delete_path(&path_to_delete); 71 | } 72 | pub fn add_entry(&mut self, entry_metadata: &Metadata, entry_full_path: &Path) { 73 | let base_path_length = self.path_in_filesystem.components().count(); 74 | let mut relative_path = PathBuf::new(); 75 | for dir in entry_full_path.components().skip(base_path_length) { 76 | relative_path.push(dir); 77 | } 78 | self.base_folder 79 | .add_entry(entry_metadata, relative_path, self.show_apparent_size); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/state/files/mod.rs: -------------------------------------------------------------------------------- 1 | mod file_or_folder; 2 | mod file_tree; 3 | 4 | pub use file_or_folder::*; 5 | pub use file_tree::*; 6 | -------------------------------------------------------------------------------- /src/state/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod file_to_delete; 2 | pub mod files; 3 | pub mod tiles; 4 | pub mod ui_effects; 5 | 6 | pub use file_to_delete::*; 7 | pub use ui_effects::*; 8 | -------------------------------------------------------------------------------- /src/state/tiles/files_in_folder.rs: -------------------------------------------------------------------------------- 1 | use ::std::ffi::OsString; 2 | 3 | use crate::state::files::{FileOrFolder, Folder}; 4 | 5 | #[derive(Copy, Clone, Debug, PartialEq)] 6 | pub enum FileType { 7 | File, 8 | Folder, 9 | } 10 | 11 | #[derive(Debug, Clone)] 12 | pub struct FileMetadata { 13 | pub name: OsString, 14 | pub size: u128, 15 | pub descendants: Option, 16 | pub percentage: f64, // 1.0 is 100% (0.5 is 50%, etc.) 17 | pub file_type: FileType, 18 | } 19 | 20 | fn calculate_percentage(size: u128, total_size: u128, total_files_in_parent: usize) -> f64 { 21 | if size == 0 && total_size == 0 { 22 | // if all files in the folder are of size 0, we'll want to display them all as 23 | // the same size 24 | 1.0 / total_files_in_parent as f64 25 | } else { 26 | size as f64 / total_size as f64 27 | } 28 | } 29 | 30 | pub fn files_in_folder(folder: &Folder, offset: usize) -> Vec { 31 | let mut files = Vec::new(); 32 | let total_size = folder.size; 33 | for (name, file_or_folder) in &folder.contents { 34 | files.push({ 35 | let size = file_or_folder.size(); 36 | let name = name.clone(); 37 | let (descendants, file_type) = match file_or_folder { 38 | FileOrFolder::Folder(folder) => (Some(folder.num_descendants), FileType::Folder), 39 | FileOrFolder::File(_file) => (None, FileType::File), 40 | }; 41 | let percentage = calculate_percentage(size, total_size, folder.contents.len()); 42 | FileMetadata { 43 | size, 44 | name, 45 | descendants, 46 | percentage, 47 | file_type, 48 | } 49 | }); 50 | } 51 | files.sort_by(|a, b| { 52 | if a.percentage == b.percentage { 53 | a.name.partial_cmp(&b.name).expect("could not compare name") 54 | } else { 55 | b.percentage 56 | .partial_cmp(&a.percentage) 57 | .expect("could not compare percentage") 58 | } 59 | }); 60 | if offset > 0 { 61 | let removed_items = files.drain(..offset); 62 | let number_of_files_without_removed_contents = folder.contents.len() - removed_items.len(); 63 | let removed_size = removed_items.fold(0, |acc, file| acc + file.size); 64 | let size_without_removed_items = total_size - removed_size; 65 | for i in 0..files.len() { 66 | files[i].percentage = calculate_percentage( 67 | files[i].size, 68 | size_without_removed_items, 69 | number_of_files_without_removed_contents, 70 | ); 71 | } 72 | } 73 | files 74 | } 75 | -------------------------------------------------------------------------------- /src/state/tiles/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod board; 2 | pub mod files_in_folder; 3 | pub mod rect_float; 4 | pub mod tile; 5 | pub mod treemap; 6 | 7 | pub use board::*; 8 | pub use files_in_folder::*; 9 | pub use rect_float::*; 10 | pub use tile::*; 11 | pub use treemap::*; 12 | -------------------------------------------------------------------------------- /src/state/tiles/rect_float.rs: -------------------------------------------------------------------------------- 1 | use ::tui::layout::Rect; 2 | 3 | #[derive(Clone, Debug)] 4 | pub struct RectFloat { 5 | pub x: f64, 6 | pub y: f64, 7 | pub width: f64, 8 | pub height: f64, 9 | } 10 | 11 | impl RectFloat { 12 | pub fn new(rect: &Rect) -> Self { 13 | RectFloat { 14 | x: rect.x as f64, 15 | y: rect.y as f64, 16 | height: rect.height as f64, 17 | width: rect.width as f64, 18 | } 19 | } 20 | pub fn round(&self) -> Rect { 21 | let rounded_x = self.x.round(); 22 | let rounded_y = self.y.round(); 23 | let mut rect = Rect { 24 | x: rounded_x as u16, 25 | y: rounded_y as u16, 26 | width: ((self.x - rounded_x) + self.width).round() as u16, 27 | height: ((self.y - rounded_y) + self.height).round() as u16, 28 | }; 29 | 30 | // fix rounding errors 31 | if (self.x + self.width).round() as u16 > rect.x + rect.width { 32 | rect.width += 1; 33 | } 34 | if (self.y + self.height).round() as u16 > rect.y + rect.height { 35 | rect.height += 1; 36 | } 37 | rect 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/state/tiles/tile.rs: -------------------------------------------------------------------------------- 1 | use ::std::ffi::OsString; 2 | 3 | use crate::state::tiles::{FileMetadata, FileType, RectFloat}; 4 | 5 | #[derive(Clone, Debug)] 6 | pub struct Tile { 7 | pub x: u16, 8 | pub y: u16, 9 | pub width: u16, 10 | pub height: u16, 11 | pub name: OsString, 12 | pub size: u128, 13 | pub descendants: Option, 14 | pub percentage: f64, 15 | pub file_type: FileType, 16 | } 17 | 18 | impl Tile { 19 | pub fn new(rect: &RectFloat, file_metadata: &FileMetadata) -> Self { 20 | let rounded = rect.round(); 21 | Tile { 22 | x: rounded.x, 23 | y: rounded.y, 24 | width: rounded.width, 25 | height: rounded.height, 26 | name: file_metadata.name.clone(), 27 | size: file_metadata.size, 28 | descendants: file_metadata.descendants, 29 | percentage: file_metadata.percentage, 30 | file_type: file_metadata.file_type, 31 | } 32 | } 33 | pub fn is_directly_right_of(&self, other: &Tile) -> bool { 34 | self.x == other.x + other.width 35 | } 36 | 37 | pub fn is_directly_left_of(&self, other: &Tile) -> bool { 38 | self.x + self.width == other.x 39 | } 40 | 41 | pub fn is_directly_below(&self, other: &Tile) -> bool { 42 | self.y == other.y + other.height 43 | } 44 | 45 | pub fn is_directly_above(&self, other: &Tile) -> bool { 46 | self.y + self.height == other.y 47 | } 48 | 49 | pub fn horizontally_overlaps_with(&self, other: &Tile) -> bool { 50 | (self.y >= other.y && self.y <= (other.y + other.height)) 51 | || ((self.y + self.height) <= (other.y + other.height) 52 | && (self.y + self.height) > other.y) 53 | || (self.y <= other.y && (self.y + self.height >= (other.y + other.height))) 54 | || (other.y <= self.y && (other.y + other.height >= (self.y + self.height))) 55 | } 56 | 57 | pub fn vertically_overlaps_with(&self, other: &Tile) -> bool { 58 | (self.x >= other.x && self.x <= (other.x + other.width)) 59 | || ((self.x + self.width) <= (other.x + other.width) && (self.x + self.width) > other.x) 60 | || (self.x <= other.x && (self.x + self.width >= (other.x + other.width))) 61 | || (other.x <= self.x && (other.x + other.width >= (self.x + self.width))) 62 | } 63 | 64 | pub fn get_vertical_overlap_with(&self, other: &Tile) -> u16 { 65 | std::cmp::min(self.x + self.width, other.x + other.width) - std::cmp::max(self.x, other.x) 66 | } 67 | 68 | pub fn get_horizontal_overlap_with(&self, other: &Tile) -> u16 { 69 | std::cmp::min(self.y + self.height, other.y + other.height) - std::cmp::max(self.y, other.y) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/state/ui_effects.rs: -------------------------------------------------------------------------------- 1 | use ::std::path::PathBuf; 2 | 3 | pub struct UiEffects { 4 | pub flash_space_freed: bool, 5 | pub current_path_is_red: bool, 6 | pub deletion_in_progress: bool, 7 | pub loading_progress_indicator: u64, 8 | pub last_read_path: Option, 9 | } 10 | 11 | impl UiEffects { 12 | pub fn new() -> Self { 13 | Self { 14 | flash_space_freed: false, 15 | current_path_is_red: false, 16 | deletion_in_progress: false, 17 | loading_progress_indicator: 0, 18 | last_read_path: None, 19 | } 20 | } 21 | pub fn increment_loading_progress_indicator(&mut self) { 22 | // increasing and decreasing this number will increase 23 | // the scanning text animation speed 24 | self.loading_progress_indicator += 3; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/tests/cases/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod test_utils; 2 | pub mod ui; 3 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/diskonaut__tests__cases__ui__cant_delete_file_with_term_too_small.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: "&terminal_draw_events_mirror[0]" 4 | --- 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | Terminal window is too small ¯\_(ツ )_/¯ 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/diskonaut__tests__cases__ui__delete_folder_small_window-10.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: "&terminal_draw_events_mirror[9]" 4 | --- 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ┌─────────────────────────────┐ 26 | │ │ 27 | │ │ 28 | │ Really quit? │ 29 | │ │ 30 | │ │ 31 | │ │ 32 | │ │ 33 | │ (y/n) │ 34 | │ │ 35 | └─────────────────────────────┘ 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/diskonaut__tests__cases__ui__delete_folder_small_window-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: "&terminal_draw_events_mirror[1]" 4 | --- 5 | 6 | 7 | ██████████████████████████████████████████████████████████ 8 | ██████████████████████████████████████████████████████████ 9 | ██████████████████████████████████████████████████████████ 10 | ██████████████████████████████████████████████████████████ 11 | ██████████████████████████████████████████████████████████ 12 | ██████████████████████████file2███████████████████████████ 13 | ██████████████████████████████████████████████████████████ 14 | ██████████████████████████████████████████████████████████ 15 | ████████████████████████4.0K (33%)████████████████████████ 16 | ██████████████████████████████████████████████████████████ 17 | ██████████████████████████████████████████████████████████ 18 | ██████████████████████████████████████████████████████████ 19 | ██████████████████████████████████████████████████████████ 20 | ██████████████████████████████████████████████████████████ 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | SELECTED: file2 (4.0K) 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/diskonaut__tests__cases__ui__delete_folder_small_window-3.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: "&terminal_draw_events_mirror[2]" 4 | --- 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | file2 13 | 14 | 15 | 4.0K (33%) 16 | 17 | 18 | 19 | 20 | 21 | 22 | ██████████████████████████████████████████████████████████ 23 | ██████████████████████████████████████████████████████████ 24 | ██████████████████████████████████████████████████████████ 25 | ██████████████████████████████████████████████████████████ 26 | ██████████████████████████████████████████████████████████ 27 | ██████████████████████████████████████████████████████████ 28 | ██████████████████████████file3███████████████████████████ 29 | ██████████████████████████████████████████████████████████ 30 | ████████████████████████4.0K (33%)████████████████████████ 31 | ██████████████████████████████████████████████████████████ 32 | ██████████████████████████████████████████████████████████ 33 | ██████████████████████████████████████████████████████████ 34 | ██████████████████████████████████████████████████████████ 35 | ██████████████████████████████████████████████████████████ 36 | ██████████████████████████████████████████████████████████ 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 3 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/diskonaut__tests__cases__ui__delete_folder_small_window-4.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: "&terminal_draw_events_mirror[3]" 4 | --- 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | file3 29 | 30 | 4.0K (33%) 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | ██████████████████████████████████████████████████████████ 39 | ██████████████████████████████████████████████████████████ 40 | ██████████████████████████████████████████████████████████ 41 | ██████████████████████████████████████████████████████████ 42 | ██████████████████████████████████████████████████████████ 43 | ███████████████subfolder1/ (+1 descendants)███████████████ 44 | ██████████████████████████████████████████████████████████ 45 | ██████████████████████████████████████████████████████████ 46 | ████████████████████████4.0K (33%)████████████████████████ 47 | ██████████████████████████████████████████████████████████ 48 | ██████████████████████████████████████████████████████████ 49 | ██████████████████████████████████████████████████████████ 50 | ██████████████████████████████████████████████████████████ 51 | ██████████████████████████████████████████████████████████ 52 | 53 | SELECTED: subfolder1 (4.0K, 1 files) 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/diskonaut__tests__cases__ui__delete_folder_small_window-5.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: "&terminal_draw_events_mirror[4]" 4 | --- 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ┌─────────────────────────────┐ 26 | │ │ 27 | │ Delete folder? │ 28 | │ │ 29 | │ │ 30 | │ subfolder1 │ 31 | │ │ 32 | │ │ 33 | │ (y/n) │ 34 | │ │ 35 | └─────────────────────────────┘ 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/diskonaut__tests__cases__ui__delete_folder_small_window-6.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: "&terminal_draw_events_mirror[5]" 4 | --- 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | Deleting 30 | 31 | subfolder1 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/diskonaut__tests__cases__ui__delete_folder_small_window-7.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: "&terminal_draw_events_mirror[6]" 4 | --- 5 | 8.0K | /tmp/diskonaut_tests/delete_folder_smal _window 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | file2 17 | 18 | 19 | 4.0K (50%) 20 | 21 | │ │ 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ├──────────────────────────────────────────────────────────┤ 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | │ │ 38 | 39 | file3 40 | 41 | 42 | 4.0K (50%) 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/diskonaut__tests__cases__ui__delete_folder_small_window-8.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: "&terminal_draw_events_mirror[7]" 4 | --- 5 | 8.0K 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/diskonaut__tests__cases__ui__delete_folder_small_window-9.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: "&terminal_draw_events_mirror[8]" 4 | --- 5 | 8.0K 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/diskonaut__tests__cases__ui__delete_folder_small_window.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: "&terminal_draw_events_mirror[0]" 4 | --- 5 | 12.0K | /tmp/diskonaut_tests/delete_folder_small_window 6 | ┌──────────────────────────────────────────────────────────┐ 7 | │ │ 8 | │ │ 9 | │ │ 10 | │ │ 11 | │ │ 12 | │ file2 │ 13 | │ │ 14 | │ │ 15 | │ 4.0K (33%) │ 16 | │ │ 17 | │ │ 18 | │ │ 19 | │ │ 20 | │ │ 21 | ├──────────────────────────────────────────────────────────┤ 22 | │ │ 23 | │ │ 24 | │ │ 25 | │ │ 26 | │ │ 27 | │ │ 28 | │ file3 │ 29 | │ │ 30 | │ 4.0K (33%) │ 31 | │ │ 32 | │ │ 33 | │ │ 34 | │ │ 35 | │ │ 36 | │ │ 37 | ├──────────────────────────────────────────────────────────┤ 38 | │ │ 39 | │ │ 40 | │ │ 41 | │ │ 42 | │ │ 43 | │ subfolder1/ (+1 descendants) │ 44 | │ │ 45 | │ │ 46 | │ 4.0K (33%) │ 47 | │ │ 48 | │ │ 49 | │ │ 50 | │ │ 51 | │ │ 52 | └──────────────────────────────────────────────────────────┘ 53 | 54 | ←↓↑→//: navigate, : del 55 | 56 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/diskonaut__tests__cases__ui__delete_folder_small_window_no_confirmation-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: "&terminal_draw_events_mirror[1]" 4 | --- 5 | 6 | 7 | ██████████████████████████████████████████████████████████ 8 | ██████████████████████████████████████████████████████████ 9 | ██████████████████████████████████████████████████████████ 10 | ██████████████████████████████████████████████████████████ 11 | ██████████████████████████████████████████████████████████ 12 | ██████████████████████████file2███████████████████████████ 13 | ██████████████████████████████████████████████████████████ 14 | ██████████████████████████████████████████████████████████ 15 | ████████████████████████4.0K (33%)████████████████████████ 16 | ██████████████████████████████████████████████████████████ 17 | ██████████████████████████████████████████████████████████ 18 | ██████████████████████████████████████████████████████████ 19 | ██████████████████████████████████████████████████████████ 20 | ██████████████████████████████████████████████████████████ 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | SELECTED: file2 (4.0K) 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/diskonaut__tests__cases__ui__delete_folder_small_window_no_confirmation-3.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: "&terminal_draw_events_mirror[2]" 4 | --- 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | file2 13 | 14 | 15 | 4.0K (33%) 16 | 17 | 18 | 19 | 20 | 21 | 22 | ██████████████████████████████████████████████████████████ 23 | ██████████████████████████████████████████████████████████ 24 | ██████████████████████████████████████████████████████████ 25 | ██████████████████████████████████████████████████████████ 26 | ██████████████████████████████████████████████████████████ 27 | ██████████████████████████████████████████████████████████ 28 | ██████████████████████████file3███████████████████████████ 29 | ██████████████████████████████████████████████████████████ 30 | ████████████████████████4.0K (33%)████████████████████████ 31 | ██████████████████████████████████████████████████████████ 32 | ██████████████████████████████████████████████████████████ 33 | ██████████████████████████████████████████████████████████ 34 | ██████████████████████████████████████████████████████████ 35 | ██████████████████████████████████████████████████████████ 36 | ██████████████████████████████████████████████████████████ 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 3 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/diskonaut__tests__cases__ui__delete_folder_small_window_no_confirmation-4.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: "&terminal_draw_events_mirror[3]" 4 | --- 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | file3 29 | 30 | 4.0K (33%) 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | ██████████████████████████████████████████████████████████ 39 | ██████████████████████████████████████████████████████████ 40 | ██████████████████████████████████████████████████████████ 41 | ██████████████████████████████████████████████████████████ 42 | ██████████████████████████████████████████████████████████ 43 | ███████████████subfolder1/ (+1 descendants)███████████████ 44 | ██████████████████████████████████████████████████████████ 45 | ██████████████████████████████████████████████████████████ 46 | ████████████████████████4.0K (33%)████████████████████████ 47 | ██████████████████████████████████████████████████████████ 48 | ██████████████████████████████████████████████████████████ 49 | ██████████████████████████████████████████████████████████ 50 | ██████████████████████████████████████████████████████████ 51 | ██████████████████████████████████████████████████████████ 52 | 53 | SELECTED: subfolder1 (4.0K, 1 files) 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/diskonaut__tests__cases__ui__delete_folder_small_window_no_confirmation-5.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: "&terminal_draw_events_mirror[4]" 4 | --- 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ┌─────────────────────────────┐ 26 | │ │ 27 | │ │ 28 | │ │ 29 | │ Deleting │ 30 | │ │ 31 | │ subfolder1 │ 32 | │ │ 33 | │ │ 34 | │ │ 35 | └─────────────────────────────┘ 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/diskonaut__tests__cases__ui__delete_folder_small_window_no_confirmation-6.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: "&terminal_draw_events_mirror[5]" 4 | --- 5 | 8.0K | /tmp/diskonaut_tests/de[. ]_window_no_confirmation 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | file2 17 | 18 | 19 | 4.0K (50%) 20 | 21 | │ │ 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ├──────────────────────────────────────────────────────────┤ 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | │ │ 38 | 39 | file3 40 | 41 | 42 | 4.0K (50%) 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/diskonaut__tests__cases__ui__delete_folder_small_window_no_confirmation-7.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: "&terminal_draw_events_mirror[7]" 4 | --- 5 | 8.0K 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/diskonaut__tests__cases__ui__delete_folder_small_window_no_confirmation-8.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: "&terminal_draw_events_mirror[8]" 4 | --- 5 | 8.0K 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/diskonaut__tests__cases__ui__delete_folder_small_window_no_confirmation-9.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: "&terminal_draw_events_mirror[9]" 4 | --- 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ┌─────────────────────────────┐ 26 | │ │ 27 | │ │ 28 | │ Really quit? │ 29 | │ │ 30 | │ │ 31 | │ │ 32 | │ │ 33 | │ (y/n) │ 34 | │ │ 35 | └─────────────────────────────┘ 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/diskonaut__tests__cases__ui__delete_folder_small_window_no_confirmation.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: "&terminal_draw_events_mirror[0]" 4 | --- 5 | 12.0K | /tmp/diskonaut_tests/de[...]_window_no_confirmation 6 | ┌──────────────────────────────────────────────────────────┐ 7 | │ │ 8 | │ │ 9 | │ │ 10 | │ │ 11 | │ │ 12 | │ file2 │ 13 | │ │ 14 | │ │ 15 | │ 4.0K (33%) │ 16 | │ │ 17 | │ │ 18 | │ │ 19 | │ │ 20 | │ │ 21 | ├──────────────────────────────────────────────────────────┤ 22 | │ │ 23 | │ │ 24 | │ │ 25 | │ │ 26 | │ │ 27 | │ │ 28 | │ file3 │ 29 | │ │ 30 | │ 4.0K (33%) │ 31 | │ │ 32 | │ │ 33 | │ │ 34 | │ │ 35 | │ │ 36 | │ │ 37 | ├──────────────────────────────────────────────────────────┤ 38 | │ │ 39 | │ │ 40 | │ │ 41 | │ │ 42 | │ │ 43 | │ subfolder1/ (+1 descendants) │ 44 | │ │ 45 | │ │ 46 | │ 4.0K (33%) │ 47 | │ │ 48 | │ │ 49 | │ │ 50 | │ │ 51 | │ │ 52 | └──────────────────────────────────────────────────────────┘ 53 | 54 | ←↓↑→//: navigate, : del 55 | 56 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/diskonaut__tests__cases__ui__enter_folder_medium_width-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: "&terminal_draw_events_mirror[1]" 4 | --- 5 | 6 | 7 | ████████████████████████████████████████████████████████████████████████████████████████ 8 | ████████████████████████████████████████████████████████████████████████████████████████ 9 | ████████████████████████████████████████████████████████████████████████████████████████ 10 | ████████████████████████████████████████████████████████████████████████████████████████ 11 | ████████████████████████████████████████████████████████████████████████████████████████ 12 | ████████████████████████████████████████████████████████████████████████████████████████ 13 | ████████████████████████████████████████████████████████████████████████████████████████ 14 | ████████████████████████████████████████████████████████████████████████████████████████ 15 | ████████████████████████████████████████████████████████████████████████████████████████ 16 | ██████████████████████████████subfolder1/ (+1 descendants)██████████████████████████████ 17 | ████████████████████████████████████████████████████████████████████████████████████████ 18 | ████████████████████████████████████████████████████████████████████████████████████████ 19 | ███████████████████████████████████████8.0K (50%)███████████████████████████████████████ 20 | ████████████████████████████████████████████████████████████████████████████████████████ 21 | ████████████████████████████████████████████████████████████████████████████████████████ 22 | ████████████████████████████████████████████████████████████████████████████████████████ 23 | ████████████████████████████████████████████████████████████████████████████████████████ 24 | ████████████████████████████████████████████████████████████████████████████████████████ 25 | ████████████████████████████████████████████████████████████████████████████████████████ 26 | ████████████████████████████████████████████████████████████████████████████████████████ 27 | ████████████████████████████████████████████████████████████████████████████████████████ 28 | ████████████████████████████████████████████████████████████████████████████████████████ 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | SELECTED: subfolder1 (8.0K, 1 files) 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/diskonaut__tests__cases__ui__enter_folder_medium_width-3.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: "&terminal_draw_events_mirror[2]" 4 | --- 5 | , freed: 0 | /tmp/diskonaut_te ts/en er_fold r_medium_width/subfolder1 (8.0K) 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | file1 29 | │ │ 30 | 8.0K (100%) 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | │ │ 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/diskonaut__tests__cases__ui__enter_folder_medium_width-4.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: "&terminal_draw_events_mirror[3]" 4 | --- 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ┌────────────────────────────────────────────┐ 26 | │ │ 27 | │ │ 28 | │ Are you sure you want to quit? │ 29 | │ │ 30 | │ │ 31 | │ │ 32 | │ │ 33 | │ (y/n) │ 34 | │ │ 35 | └────────────────────────────────────────────┘ 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/diskonaut__tests__cases__ui__enter_folder_medium_width-5.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: "&terminal_draw_events_mirror[4]" 4 | --- 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/diskonaut__tests__cases__ui__enter_folder_medium_width.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: "&terminal_draw_events_mirror[0]" 4 | --- 5 | Total: 16.0K (4 files), freed: 0 | /tmp/diskonaut_tests/enter_folder_medium_width 6 | ┌────────────────────────────────────────────────────────────────────────────────────────┐ 7 | │ │ 8 | │ │ 9 | │ │ 10 | │ │ 11 | │ │ 12 | │ │ 13 | │ │ 14 | │ │ 15 | │ │ 16 | │ subfolder1/ (+1 descendants) │ 17 | │ │ 18 | │ │ 19 | │ 8.0K (50%) │ 20 | │ │ 21 | │ │ 22 | │ │ 23 | │ │ 24 | │ │ 25 | │ │ 26 | │ │ 27 | │ │ 28 | │ │ 29 | ├────────────────────────────────────────────────────────────────────────────────────────┤ 30 | │ │ 31 | │ │ 32 | │ │ 33 | │ │ 34 | │ file2 │ 35 | │ │ 36 | │ 4.0K (25%) │ 37 | │ │ 38 | │ │ 39 | │ │ 40 | │ │ 41 | ├────────────────────────────────────────────────────────────────────────────────────────┤ 42 | │ │ 43 | │ │ 44 | │ │ 45 | │ file3 │ 46 | │ │ 47 | │ │ 48 | │ 4.0K (25%) │ 49 | │ │ 50 | │ │ 51 | │ │ 52 | └────────────────────────────────────────────────────────────────────────────────────────┘ 53 | 54 | ←↓↑→//: navigate, : del 55 | 56 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/diskonaut__tests__cases__ui__enter_folder_small_width-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: "&terminal_draw_events_mirror[1]" 4 | --- 5 | 6 | 7 | ██████████████████████████████████████████████████████████ 8 | ██████████████████████████████████████████████████████████ 9 | ██████████████████████████████████████████████████████████ 10 | ██████████████████████████████████████████████████████████ 11 | ██████████████████████████████████████████████████████████ 12 | ██████████████████████████████████████████████████████████ 13 | ██████████████████████████████████████████████████████████ 14 | ██████████████████████████████████████████████████████████ 15 | ██████████████████████████████████████████████████████████ 16 | ████subfolder_with_quite_a_long_name/ (+1 descendants)████ 17 | ██████████████████████████████████████████████████████████ 18 | ██████████████████████████████████████████████████████████ 19 | ████████████████████████8.0K (50%)████████████████████████ 20 | ██████████████████████████████████████████████████████████ 21 | ██████████████████████████████████████████████████████████ 22 | ██████████████████████████████████████████████████████████ 23 | ██████████████████████████████████████████████████████████ 24 | ██████████████████████████████████████████████████████████ 25 | ██████████████████████████████████████████████████████████ 26 | ██████████████████████████████████████████████████████████ 27 | ██████████████████████████████████████████████████████████ 28 | ██████████████████████████████████████████████████████████ 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | SELECTED: subfolder_with_quite_a_long_name (8.0K, 1 files) 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/diskonaut__tests__cases__ui__enter_folder_small_width-3.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: "&terminal_draw_events_mirror[2]" 4 | --- 5 | [..]mall_width/subfolder_[...]a_long_name 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | file1 29 | │ │ 30 | 8.0K (100%) 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | │ │ 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/diskonaut__tests__cases__ui__enter_folder_small_width-4.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: "&terminal_draw_events_mirror[3]" 4 | --- 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ┌─────────────────────────────┐ 26 | │ │ 27 | │ │ 28 | │ Really quit? │ 29 | │ │ 30 | │ │ 31 | │ │ 32 | │ │ 33 | │ (y/n) │ 34 | │ │ 35 | └─────────────────────────────┘ 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/diskonaut__tests__cases__ui__enter_folder_small_width-5.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: "&terminal_draw_events_mirror[4]" 4 | --- 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/diskonaut__tests__cases__ui__enter_folder_small_width.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: "&terminal_draw_events_mirror[0]" 4 | --- 5 | 16.0K | /tmp/diskonaut_tests/enter_folder_small_width 6 | ┌──────────────────────────────────────────────────────────┐ 7 | │ │ 8 | │ │ 9 | │ │ 10 | │ │ 11 | │ │ 12 | │ │ 13 | │ │ 14 | │ │ 15 | │ │ 16 | │ subfolder_with_quite_a_long_name/ (+1 descendants) │ 17 | │ │ 18 | │ │ 19 | │ 8.0K (50%) │ 20 | │ │ 21 | │ │ 22 | │ │ 23 | │ │ 24 | │ │ 25 | │ │ 26 | │ │ 27 | │ │ 28 | │ │ 29 | ├──────────────────────────────────────────────────────────┤ 30 | │ │ 31 | │ │ 32 | │ │ 33 | │ │ 34 | │ file2 │ 35 | │ │ 36 | │ 4.0K (25%) │ 37 | │ │ 38 | │ │ 39 | │ │ 40 | │ │ 41 | ├──────────────────────────────────────────────────────────┤ 42 | │ │ 43 | │ │ 44 | │ │ 45 | │ file3 │ 46 | │ │ 47 | │ │ 48 | │ 4.0K (25%) │ 49 | │ │ 50 | │ │ 51 | │ │ 52 | └──────────────────────────────────────────────────────────┘ 53 | 54 | ←↓↑→//: navigate, : del 55 | 56 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/diskonaut__tests__cases__ui__medium_width-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: "&terminal_draw_events_mirror[1]" 4 | --- 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ┌─────────────────────────────┐ 26 | │ │ 27 | │ │ 28 | │ Really quit? │ 29 | │ │ 30 | │ │ 31 | │ │ 32 | │ │ 33 | │ (y/n) │ 34 | │ │ 35 | └─────────────────────────────┘ 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/diskonaut__tests__cases__ui__medium_width.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: "&terminal_draw_events_mirror[0]" 4 | --- 5 | Total: 20.0K, freed: 0 | /tmp/diskonaut_tests/medium_width 6 | ┌──────────────────────────────────────────────────────────┐ 7 | │ │ 8 | │ │ 9 | │ │ 10 | │ │ 11 | │ │ 12 | │ │ 13 | │ │ 14 | │ file2 │ 15 | │ │ 16 | │ 8.0K (40%) │ 17 | │ │ 18 | │ │ 19 | │ │ 20 | │ │ 21 | │ │ 22 | │ │ 23 | │ │ 24 | ├──────────────────────────────────────────────────────────┤ 25 | │ │ 26 | │ │ 27 | │ │ 28 | │ │ 29 | │ │ 30 | │ │ 31 | │ │ 32 | │ file3 │ 33 | │ │ 34 | │ │ 35 | │ 8.0K (40%) │ 36 | │ │ 37 | │ │ 38 | │ │ 39 | │ │ 40 | │ │ 41 | │ │ 42 | │ │ 43 | ├──────────────────────────────────────────────────────────┤ 44 | │ │ 45 | │ │ 46 | │ file1 │ 47 | │ │ 48 | │ │ 49 | │ 4.0K (20%) │ 50 | │ │ 51 | │ │ 52 | └──────────────────────────────────────────────────────────┘ 53 | 54 | ←↓↑→//: navigate, : del 55 | 56 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/diskonaut__tests__cases__ui__small_files_with_x_as_zero-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: "&terminal_draw_events_mirror[1]" 4 | --- 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ┌────────────────────────┐ 26 | │ │ 27 | │ │ 28 | │ Really quit? │ 29 | │ │ 30 | │ │ 31 | │ │ 32 | │ │ 33 | │ (y/n) │ 34 | │ │ 35 | └────────────────────────┘ 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/diskonaut__tests__cases__ui__small_files_with_x_as_zero.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: "&terminal_draw_events_mirror[0]" 4 | --- 5 | 2.4M | /tmp/diskonaut_tes[..]les_with_x_as_zero 6 | ┌────────────────────────────────────────────────┐ 7 | │ │ 8 | │ │ 9 | │ │ 10 | │ │ 11 | │ │ 12 | │ │ 13 | │ │ 14 | │ file1 │ 15 | │ │ 16 | │ │ 17 | │ 1.0M (42%) │ 18 | │ │ 19 | │ │ 20 | │ │ 21 | │ │ 22 | │ │ 23 | │ │ 24 | │ │ 25 | ├────────────────────────────────────────────────┤ 26 | │ │ 27 | │ │ 28 | │ │ 29 | │ │ 30 | │ │ 31 | │ │ 32 | │ │ 33 | │ │ 34 | │ file2 │ 35 | │ │ 36 | │ 1.0M (42%) │ 37 | │ │ 38 | │ │ 39 | │ │ 40 | │ │ 41 | │ │ 42 | │ │ 43 | │ │ 44 | │ │ 45 | ├────────────────────────────────────────────────┤ 46 | │xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx│ 47 | │xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx│ 48 | │xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx│ 49 | │xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx│ 50 | │xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx│ 51 | │xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx│ 52 | └────────────────────────────────────────────────┘ 53 | (x = Small files) 54 | ←↓↑→//: navigate, : del 55 | 56 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/diskonaut__tests__cases__ui__small_width-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: "&terminal_draw_events_mirror[1]" 4 | --- 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ┌────────────────────────┐ 26 | │ │ 27 | │ │ 28 | │ Really quit? │ 29 | │ │ 30 | │ │ 31 | │ │ 32 | │ │ 33 | │ (y/n) │ 34 | │ │ 35 | └────────────────────────┘ 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/diskonaut__tests__cases__ui__small_width.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: "&terminal_draw_events_mirror[0]" 4 | --- 5 | Total: 20.0K | /tmp/diskonaut_tests/small_width 6 | ┌────────────────────────────────────────────────┐ 7 | │ │ 8 | │ │ 9 | │ │ 10 | │ │ 11 | │ │ 12 | │ │ 13 | │ │ 14 | │ file2 │ 15 | │ │ 16 | │ 8.0K (40%) │ 17 | │ │ 18 | │ │ 19 | │ │ 20 | │ │ 21 | │ │ 22 | │ │ 23 | │ │ 24 | ├────────────────────────────────────────────────┤ 25 | │ │ 26 | │ │ 27 | │ │ 28 | │ │ 29 | │ │ 30 | │ │ 31 | │ │ 32 | │ file3 │ 33 | │ │ 34 | │ │ 35 | │ 8.0K (40%) │ 36 | │ │ 37 | │ │ 38 | │ │ 39 | │ │ 40 | │ │ 41 | │ │ 42 | │ │ 43 | ├────────────────────────────────────────────────┤ 44 | │ │ 45 | │ │ 46 | │ file1 │ 47 | │ │ 48 | │ │ 49 | │ 4.0K (20%) │ 50 | │ │ 51 | │ │ 52 | └────────────────────────────────────────────────┘ 53 | 54 | ←↓↑→//: navigate, : del 55 | 56 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/diskonaut__tests__cases__ui__small_width_long_folder_name-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: "&terminal_draw_events_mirror[1]" 4 | --- 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ┌────────────────────────┐ 26 | │ │ 27 | │ │ 28 | │ Really quit? │ 29 | │ │ 30 | │ │ 31 | │ │ 32 | │ │ 33 | │ (y/n) │ 34 | │ │ 35 | └────────────────────────┘ 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/diskonaut__tests__cases__ui__small_width_long_folder_name.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: "&terminal_draw_events_mirror[0]" 4 | --- 5 | 20.0K | /tmp/diskonaut_tes[...]h_long_folder_name 6 | ┌────────────────────────────────────────────────┐ 7 | │ │ 8 | │ │ 9 | │ │ 10 | │ │ 11 | │ │ 12 | │ │ 13 | │ │ 14 | │ file2 │ 15 | │ │ 16 | │ 8.0K (40%) │ 17 | │ │ 18 | │ │ 19 | │ │ 20 | │ │ 21 | │ │ 22 | │ │ 23 | │ │ 24 | ├────────────────────────────────────────────────┤ 25 | │ │ 26 | │ │ 27 | │ │ 28 | │ │ 29 | │ │ 30 | │ │ 31 | │ │ 32 | │ file3 │ 33 | │ │ 34 | │ │ 35 | │ 8.0K (40%) │ 36 | │ │ 37 | │ │ 38 | │ │ 39 | │ │ 40 | │ │ 41 | │ │ 42 | │ │ 43 | ├────────────────────────────────────────────────┤ 44 | │ │ 45 | │ │ 46 | │ file1 │ 47 | │ │ 48 | │ │ 49 | │ 4.0K (20%) │ 50 | │ │ 51 | │ │ 52 | └────────────────────────────────────────────────┘ 53 | 54 | ←↓↑→//: navigate, : del 55 | 56 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/diskonaut__tests__cases__ui__too_small_height.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: "&terminal_draw_events_mirror[0]" 4 | --- 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Terminal window is too small ¯\_(ツ )_/¯ 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/diskonaut__tests__cases__ui__too_small_width_five.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: "&terminal_draw_events_mirror[0]" 4 | --- 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | !!! 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/diskonaut__tests__cases__ui__too_small_width_four.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: "&terminal_draw_events_mirror[0]" 4 | --- 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | ¯\_(ツ )_/¯ 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/diskonaut__tests__cases__ui__too_small_width_one.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: "&terminal_draw_events_mirror[0]" 4 | --- 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | Terminal window is too small ¯\_(ツ )_/¯ 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/diskonaut__tests__cases__ui__too_small_width_three.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: "&terminal_draw_events_mirror[0]" 4 | --- 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | too small ¯\_(ツ )_/ 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/tests/cases/snapshots/diskonaut__tests__cases__ui__too_small_width_two.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/tests/cases/ui.rs 3 | expression: "&terminal_draw_events_mirror[0]" 4 | --- 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | Window too small ¯\_(ツ )_/ 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/tests/cases/test_utils.rs: -------------------------------------------------------------------------------- 1 | use ::std::iter; 2 | use ::std::sync::{Arc, Mutex}; 3 | use crossterm::event::KeyModifiers; 4 | use crossterm::event::{Event, KeyCode, KeyEvent}; 5 | 6 | use crate::tests::fakes::{TerminalEvent, TerminalEvents, TestBackend}; 7 | 8 | macro_rules! key { 9 | (char $x:expr) => { 10 | Event::Key(KeyEvent { 11 | code: KeyCode::Char($x), 12 | modifiers: KeyModifiers::NONE, 13 | }) 14 | }; 15 | (ctrl $x:expr) => { 16 | Event::Key(KeyEvent { 17 | code: KeyCode::Char($x), 18 | modifiers: KeyModifiers::CONTROL, 19 | }) 20 | }; 21 | ($x:ident) => { 22 | Event::Key(KeyEvent { 23 | code: KeyCode::$x, 24 | modifiers: KeyModifiers::NONE, 25 | }) 26 | }; 27 | } 28 | 29 | pub fn sleep_and_quit_events(sleep_num: usize, quit_after_confirm: bool) -> Box { 30 | let mut events: Vec> = iter::repeat(None).take(sleep_num).collect(); 31 | events.push(Some(key!(ctrl 'c'))); 32 | if quit_after_confirm { 33 | events.push(None); 34 | events.push(Some(key!(char 'y'))); 35 | } 36 | Box::new(TerminalEvents::new(events)) 37 | } 38 | 39 | type BackendWithStreams = ( 40 | Arc>>, 41 | Arc>>, 42 | TestBackend, 43 | ); 44 | pub fn test_backend_factory(w: u16, h: u16) -> BackendWithStreams { 45 | let terminal_events: Arc>> = Arc::new(Mutex::new(Vec::new())); 46 | let terminal_draw_events: Arc>> = Arc::new(Mutex::new(Vec::new())); 47 | 48 | let backend = TestBackend::new( 49 | terminal_events.clone(), 50 | terminal_draw_events.clone(), 51 | Arc::new(Mutex::new(w)), 52 | Arc::new(Mutex::new(h)), 53 | ); 54 | (terminal_events, terminal_draw_events, backend) 55 | } 56 | -------------------------------------------------------------------------------- /src/tests/fakes/fake_input.rs: -------------------------------------------------------------------------------- 1 | use ::std::{thread, time}; 2 | use crossterm::event::Event; 3 | 4 | pub struct TerminalEvents { 5 | pub events: Vec>, 6 | } 7 | 8 | impl TerminalEvents { 9 | pub fn new(mut events: Vec>) -> Self { 10 | events.reverse(); // this is so that we do not have to shift the array 11 | TerminalEvents { events } 12 | } 13 | } 14 | impl Iterator for TerminalEvents { 15 | type Item = Event; 16 | fn next(&mut self) -> Option { 17 | match self.events.pop() { 18 | Some(ev) => match ev { 19 | Some(ev) => Some(ev), 20 | None => { 21 | thread::sleep(time::Duration::from_millis(200)); 22 | self.next() 23 | } 24 | }, 25 | None => None, 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/tests/fakes/fake_output.rs: -------------------------------------------------------------------------------- 1 | use ::std::collections::HashMap; 2 | use ::std::io; 3 | use ::std::sync::{Arc, Mutex}; 4 | use ::tui::backend::Backend; 5 | use ::tui::buffer::Cell; 6 | use ::tui::layout::Rect; 7 | 8 | #[derive(Hash, Debug, PartialEq)] 9 | pub enum TerminalEvent { 10 | Clear, 11 | HideCursor, 12 | ShowCursor, 13 | GetCursor, 14 | Flush, 15 | Draw, 16 | } 17 | 18 | pub struct TestBackend { 19 | pub events: Arc>>, 20 | pub draw_events: Arc>>, 21 | terminal_width: Arc>, 22 | terminal_height: Arc>, 23 | } 24 | 25 | impl TestBackend { 26 | pub fn new( 27 | log: Arc>>, 28 | draw_log: Arc>>, 29 | terminal_width: Arc>, 30 | terminal_height: Arc>, 31 | ) -> TestBackend { 32 | TestBackend { 33 | events: log, 34 | draw_events: draw_log, 35 | terminal_width, 36 | terminal_height, 37 | } 38 | } 39 | } 40 | 41 | #[derive(Hash, Eq, PartialEq)] 42 | struct Point { 43 | x: u16, 44 | y: u16, 45 | } 46 | 47 | impl Backend for TestBackend { 48 | fn clear(&mut self) -> io::Result<()> { 49 | self.events.lock().unwrap().push(TerminalEvent::Clear); 50 | Ok(()) 51 | } 52 | 53 | fn hide_cursor(&mut self) -> io::Result<()> { 54 | self.events.lock().unwrap().push(TerminalEvent::HideCursor); 55 | Ok(()) 56 | } 57 | 58 | fn show_cursor(&mut self) -> io::Result<()> { 59 | self.events.lock().unwrap().push(TerminalEvent::ShowCursor); 60 | Ok(()) 61 | } 62 | 63 | fn get_cursor(&mut self) -> io::Result<(u16, u16)> { 64 | self.events.lock().unwrap().push(TerminalEvent::GetCursor); 65 | Ok((0, 0)) 66 | } 67 | 68 | fn set_cursor(&mut self, _x: u16, _y: u16) -> io::Result<()> { 69 | Ok(()) 70 | } 71 | 72 | fn draw<'a, I>(&mut self, content: I) -> io::Result<()> 73 | where 74 | I: Iterator, 75 | { 76 | self.events.lock().unwrap().push(TerminalEvent::Draw); 77 | let mut string = String::with_capacity(content.size_hint().0 * 3); 78 | let mut coordinates = HashMap::new(); 79 | for (x, y, cell) in content { 80 | coordinates.insert(Point { x, y }, cell); 81 | } 82 | let terminal_height = self.terminal_height.lock().unwrap(); 83 | let terminal_width = self.terminal_width.lock().unwrap(); 84 | for y in 0..*terminal_height { 85 | for x in 0..*terminal_width { 86 | match coordinates.get(&Point { x, y }) { 87 | Some(cell) => { 88 | // this will contain no style information at all 89 | // should be good enough for testing 90 | string.push_str(&cell.symbol); 91 | } 92 | None => { 93 | string.push_str(" "); 94 | } 95 | } 96 | } 97 | string.push_str("\n"); 98 | } 99 | self.draw_events.lock().unwrap().push(string); 100 | Ok(()) 101 | } 102 | 103 | fn size(&self) -> io::Result { 104 | let terminal_height = self.terminal_height.lock().unwrap(); 105 | let terminal_width = self.terminal_width.lock().unwrap(); 106 | 107 | Ok(Rect::new(0, 0, *terminal_width, *terminal_height)) 108 | } 109 | 110 | fn flush(&mut self) -> io::Result<()> { 111 | self.events.lock().unwrap().push(TerminalEvent::Flush); 112 | Ok(()) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/tests/fakes/mod.rs: -------------------------------------------------------------------------------- 1 | mod fake_input; 2 | mod fake_output; 3 | 4 | pub use fake_input::*; 5 | pub use fake_output::*; 6 | -------------------------------------------------------------------------------- /src/tests/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod cases; 2 | pub mod fakes; 3 | -------------------------------------------------------------------------------- /src/ui/bottom_line.rs: -------------------------------------------------------------------------------- 1 | use ::std::path::PathBuf; 2 | use ::tui::buffer::Buffer; 3 | use ::tui::layout::Rect; 4 | use ::tui::style::{Color, Modifier, Style}; 5 | use ::tui::widgets::Widget; 6 | 7 | use crate::state::tiles::{FileType, Tile}; 8 | use crate::ui::format::{truncate_middle, DisplaySize}; 9 | 10 | fn render_currently_selected(buf: &mut Buffer, currently_selected: &Tile, max_len: u16, y: u16) { 11 | let file_name = currently_selected.name.to_string_lossy(); 12 | let size = DisplaySize(currently_selected.size as f64); 13 | let descendants = currently_selected.descendants; 14 | let (style, lines) = match currently_selected.file_type { 15 | FileType::File => ( 16 | Style::default().add_modifier(Modifier::BOLD), 17 | vec![ 18 | format!("SELECTED: {} ({})", file_name, size), 19 | format!("SELECTED: {}", file_name), 20 | format!("{}", file_name), 21 | ], 22 | ), 23 | FileType::Folder => ( 24 | Style::default() 25 | .fg(Color::Blue) 26 | .add_modifier(Modifier::BOLD), 27 | vec![ 28 | format!( 29 | "SELECTED: {} ({}, {} files)", 30 | file_name, 31 | size, 32 | descendants.expect("a folder should have descendants") 33 | ), 34 | format!("SELECTED: {} ({})", file_name, size), 35 | format!("SELECTED: {}", file_name), 36 | format!("{}", file_name), 37 | ], 38 | ), 39 | }; 40 | for line in lines { 41 | if (line.chars().count() as u16) < max_len { 42 | buf.set_string(1, y, line, style); 43 | break; 44 | } 45 | } 46 | } 47 | 48 | fn render_last_read_path(buf: &mut Buffer, last_read_path: &PathBuf, max_len: u16, y: u16) { 49 | let last_read_path = last_read_path.to_string_lossy(); 50 | if (last_read_path.chars().count() as u16) < max_len { 51 | buf.set_string(1, y, last_read_path, Style::default()); 52 | } else { 53 | buf.set_string( 54 | 1, 55 | y, 56 | truncate_middle(&last_read_path, max_len), 57 | Style::default(), 58 | ); 59 | } 60 | } 61 | 62 | fn render_controls_legend(buf: &mut Buffer, hide_delete: bool, max_len: u16, y: u16) { 63 | let (long_controls_line, short_controls_line) = if hide_delete { 64 | ( 65 | String::from( 66 | " - move around, - enter folder, - parent folder, <+/-/0> - zoom in/out/reset, - quit", 67 | ), 68 | String::from("←↓↑→//: navigate"), 69 | ) 70 | } else { 71 | ( 72 | String::from(" - move around, - enter folder, - parent folder, - delete, <+/-/0> - zoom in/out/reset, - quit"), 73 | String::from("←↓↑→//: navigate, : del") 74 | ) 75 | }; 76 | let too_small_line = "(...)"; 77 | if max_len >= long_controls_line.chars().count() as u16 { 78 | buf.set_string( 79 | 1, 80 | y, 81 | long_controls_line, 82 | Style::default().add_modifier(Modifier::BOLD), 83 | ); 84 | } else if max_len >= short_controls_line.chars().count() as u16 { 85 | buf.set_string( 86 | 1, 87 | y, 88 | short_controls_line, 89 | Style::default().add_modifier(Modifier::BOLD), 90 | ); 91 | } else { 92 | buf.set_string( 93 | 1, 94 | y, 95 | too_small_line, 96 | Style::default().add_modifier(Modifier::BOLD), 97 | ); 98 | } 99 | } 100 | 101 | fn render_small_files_legend(buf: &mut Buffer, x: u16, y: u16, small_files_legend: &str) { 102 | buf.set_string( 103 | x, 104 | y, 105 | small_files_legend, 106 | Style::default() 107 | .fg(Color::Reset) 108 | .bg(Color::Reset) 109 | .remove_modifier(Modifier::all()), 110 | ); 111 | let small_files_legend_character = buf.get_mut(x + 1, y); 112 | small_files_legend_character.set_style(Style::default().bg(Color::White).fg(Color::Black)); 113 | } 114 | 115 | pub struct BottomLine<'a> { 116 | hide_delete: bool, 117 | hide_small_files_legend: bool, 118 | currently_selected: Option<&'a Tile>, 119 | last_read_path: Option<&'a PathBuf>, 120 | } 121 | 122 | impl<'a> BottomLine<'a> { 123 | pub fn new() -> Self { 124 | Self { 125 | hide_delete: false, 126 | hide_small_files_legend: false, 127 | currently_selected: None, 128 | last_read_path: None, 129 | } 130 | } 131 | pub fn hide_delete(mut self) -> Self { 132 | self.hide_delete = true; 133 | self 134 | } 135 | pub fn hide_small_files_legend(mut self, should_hide_small_files_legend: bool) -> Self { 136 | self.hide_small_files_legend = should_hide_small_files_legend; 137 | self 138 | } 139 | pub fn currently_selected(mut self, currently_selected: Option<&'a Tile>) -> Self { 140 | self.currently_selected = currently_selected; 141 | self 142 | } 143 | pub fn last_read_path(mut self, last_read_path: Option<&'a PathBuf>) -> Self { 144 | self.last_read_path = last_read_path; 145 | self 146 | } 147 | } 148 | 149 | impl<'a> Widget for BottomLine<'a> { 150 | fn render(self, area: Rect, buf: &mut Buffer) { 151 | let small_files_legend = "(x = Small files)"; 152 | let small_files_len = if self.hide_small_files_legend { 153 | 0 154 | } else { 155 | small_files_legend.chars().count() as u16 156 | }; 157 | let max_status_len = area.width - small_files_len - 1; 158 | let max_controls_len = area.width - 1; 159 | let status_line_y = area.y + area.height - 2; 160 | let controls_line_y = status_line_y + 1; 161 | if let Some(currently_selected) = self.currently_selected { 162 | render_currently_selected(buf, currently_selected, max_status_len, status_line_y); 163 | } else if let Some(last_read_path) = self.last_read_path { 164 | render_last_read_path(buf, last_read_path, max_status_len, status_line_y); 165 | } 166 | 167 | if !self.hide_small_files_legend { 168 | render_small_files_legend( 169 | buf, 170 | area.width - small_files_len - 1, 171 | status_line_y, 172 | small_files_legend, 173 | ); 174 | } 175 | 176 | render_controls_legend(buf, self.hide_delete, max_controls_len, controls_line_y); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/ui/format/display_size.rs: -------------------------------------------------------------------------------- 1 | use ::std::fmt; 2 | 3 | pub struct DisplaySize(pub f64); 4 | 5 | impl fmt::Display for DisplaySize { 6 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 7 | if self.0 > 999_999_999.0 { 8 | write!(f, "{:.1}G", self.0 / 1073741824.0) // 1024 * 1024 * 1024 9 | } else if self.0 > 999_999.0 { 10 | write!(f, "{:.1}M", self.0 / 1048576.0) // 1024 * 1024 11 | } else if self.0 > 999.0 { 12 | write!(f, "{:.1}K", self.0 / 1024.0) 13 | } else { 14 | write!(f, "{}", self.0) 15 | } 16 | } 17 | } 18 | 19 | pub struct DisplaySizeRounded(pub f64); 20 | 21 | impl fmt::Display for DisplaySizeRounded { 22 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 23 | if self.0 > 999_999_999.0 { 24 | write!(f, "{:.0}G", self.0 / 1073741824.0) // 1024 * 1024 * 1024 25 | } else if self.0 > 999_999.0 { 26 | write!(f, "{:.0}M", self.0 / 1048576.0) // 1024 * 1024 27 | } else if self.0 > 999.0 { 28 | write!(f, "{:.0}K", self.0 / 1024.0) 29 | } else { 30 | write!(f, "{}", self.0) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/ui/format/mod.rs: -------------------------------------------------------------------------------- 1 | mod display_size; 2 | mod truncate; 3 | 4 | pub use display_size::*; 5 | pub use truncate::*; 6 | -------------------------------------------------------------------------------- /src/ui/format/truncate.rs: -------------------------------------------------------------------------------- 1 | use ::std::iter::FromIterator; 2 | use ::unicode_width::UnicodeWidthChar; 3 | 4 | fn truncate_iter_to_unicode_width(iter: Input, width: usize) -> Collect 5 | where 6 | Input: Iterator, 7 | Collect: FromIterator, 8 | { 9 | let mut chunk_width = 0; 10 | iter.take_while(|ch| { 11 | chunk_width += ch.width().unwrap_or(0); 12 | chunk_width <= width 13 | }) 14 | .collect() 15 | } 16 | 17 | pub fn truncate_middle(row: &str, max_length: u16) -> String { 18 | if max_length < 6 { 19 | truncate_iter_to_unicode_width(row.chars(), max_length as usize) 20 | } else if row.len() as u16 > max_length { 21 | let split_point = (max_length as usize / 2) - 2; 22 | let first_slice = truncate_iter_to_unicode_width::<_, String>(row.chars(), split_point); 23 | let second_slice = 24 | truncate_iter_to_unicode_width::<_, Vec<_>>(row.chars().rev(), split_point) 25 | .into_iter() 26 | .rev() 27 | .collect::(); 28 | 29 | if max_length % 2 == 0 { 30 | format!("{}[...]{}", first_slice, second_slice) 31 | } else { 32 | format!("{}[..]{}", first_slice, second_slice) 33 | } 34 | } else { 35 | row.to_string() 36 | } 37 | } 38 | 39 | pub fn truncate_end(row: &str, max_len: u16) -> String { 40 | if row.chars().count() > max_len as usize { 41 | let mut truncated = String::from(row); 42 | truncated.truncate(max_len as usize - 3); 43 | format!("{}...", truncated) 44 | } else { 45 | row.to_string() 46 | } 47 | } 48 | 49 | #[cfg(test)] 50 | mod tests { 51 | use super::*; 52 | 53 | #[test] 54 | fn truncate_middle_char_boundary() { 55 | assert_eq!( 56 | truncate_middle("굿걸 - 누가 방송국을 털었나 E06.mp4", 44), 57 | "굿걸 - 누가 방송국을[...]국을 털었나 E06.mp4", 58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/ui/grid/mod.rs: -------------------------------------------------------------------------------- 1 | mod draw_next_symbol; 2 | mod draw_rect; 3 | mod rectangle_grid; 4 | 5 | pub use draw_next_symbol::*; 6 | pub use draw_rect::*; 7 | pub use rectangle_grid::*; 8 | -------------------------------------------------------------------------------- /src/ui/grid/rectangle_grid.rs: -------------------------------------------------------------------------------- 1 | use ::tui::buffer::Buffer; 2 | use ::tui::layout::Rect; 3 | use ::tui::style::{Color, Style}; 4 | use ::tui::widgets::Widget; 5 | 6 | use crate::state::tiles::Tile; 7 | use crate::ui::grid::{draw_rect_on_grid, draw_tile_text_on_grid}; 8 | 9 | fn draw_small_files_rect_on_grid(buf: &mut Buffer, rect: Rect) { 10 | for x in rect.x + 1..(rect.x + rect.width) { 11 | for y in rect.y + 1..(rect.y + rect.height) { 12 | let buf = buf.get_mut(x, y); 13 | buf.set_symbol("x"); 14 | buf.set_style(Style::default().bg(Color::White).fg(Color::Black)); 15 | } 16 | } 17 | draw_rect_on_grid(buf, (rect.x, rect.y), (rect.width, rect.height)); 18 | } 19 | 20 | fn draw_empty_folder(buf: &mut Buffer, area: Rect) { 21 | for x in area.x + 1..area.x + area.width { 22 | for y in area.y + 1..area.y + area.height { 23 | let buf = buf.get_mut(x, y); 24 | buf.set_symbol("█"); 25 | buf.set_style(Style::default().bg(Color::White).fg(Color::Black)); 26 | } 27 | } 28 | let empty_folder_line = "Folder is empty"; 29 | let text_length = empty_folder_line.len(); 30 | let text_style = Style::default(); 31 | let text_start_position = 32 | ((area.width - text_length as u16) as f64 / 2.0).ceil() as u16 + area.x; 33 | buf.set_string( 34 | text_start_position, 35 | (area.height / 2) + area.y - 1, 36 | empty_folder_line, 37 | text_style, 38 | ); 39 | draw_rect_on_grid(buf, (area.x, area.y), (area.width, area.height)); 40 | } 41 | 42 | #[derive(Clone)] 43 | pub struct RectangleGrid<'a> { 44 | rectangles: &'a [Tile], 45 | small_files_coordinates: Option<(u16, u16)>, 46 | selected_rect_index: Option, 47 | } 48 | 49 | impl<'a> RectangleGrid<'a> { 50 | pub fn new( 51 | rectangles: &'a [Tile], 52 | small_files_coordinates: Option<(u16, u16)>, 53 | selected_rect_index: Option, 54 | ) -> Self { 55 | RectangleGrid { 56 | rectangles, 57 | small_files_coordinates, 58 | selected_rect_index, 59 | } 60 | } 61 | } 62 | 63 | impl<'a> Widget for RectangleGrid<'a> { 64 | fn render(self, area: Rect, buf: &mut Buffer) { 65 | if self.rectangles.is_empty() { 66 | draw_empty_folder(buf, area); 67 | } else { 68 | for (index, tile) in self.rectangles.iter().enumerate() { 69 | let selected = if let Some(selected_rect_index) = self.selected_rect_index { 70 | index == selected_rect_index 71 | } else { 72 | false 73 | }; 74 | draw_tile_text_on_grid(buf, &tile, selected); 75 | draw_rect_on_grid(buf, (tile.x, tile.y), (tile.width, tile.height)); 76 | } 77 | } 78 | if let Some(coords) = self.small_files_coordinates { 79 | let (x, y) = coords; 80 | let width = (area.x + area.width) - x; 81 | let height = (area.y + area.height) - y; 82 | let small_files_rect = Rect { 83 | x, 84 | y, 85 | width, 86 | height, 87 | }; 88 | draw_small_files_rect_on_grid(buf, small_files_rect); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/ui/mod.rs: -------------------------------------------------------------------------------- 1 | mod bottom_line; 2 | mod display; 3 | mod format; 4 | mod grid; 5 | mod modals; 6 | mod term_too_small; 7 | mod title; 8 | 9 | pub use bottom_line::*; 10 | pub use display::*; 11 | pub use term_too_small::*; 12 | -------------------------------------------------------------------------------- /src/ui/modals/confirm_box.rs: -------------------------------------------------------------------------------- 1 | use ::tui::buffer::Buffer; 2 | use ::tui::layout::Rect; 3 | use ::tui::style::{Color, Modifier, Style}; 4 | use ::tui::widgets::Widget; 5 | 6 | use crate::ui::format::truncate_middle; 7 | use crate::ui::grid::draw_filled_rect; 8 | 9 | fn render_confirm_prompt(buf: &mut Buffer, confirm_rect: &Rect) { 10 | let text_style = Style::default() 11 | .bg(Color::Black) 12 | .fg(Color::White) 13 | .add_modifier(Modifier::BOLD); 14 | 15 | let possible_confirm_texts = [ 16 | "Are you sure you want to quit?", 17 | "Sure you want to quit?", 18 | "Really quit?", 19 | "Quit?", 20 | ]; 21 | // set default value of the confirm_text 22 | // to the longest one from possible_confirm_text array 23 | let mut confirm_text = String::from(possible_confirm_texts[0]); 24 | let mut confirm_text_start_position: u16 = 0; 25 | let text_max_length = confirm_rect.width - 4; 26 | for line in possible_confirm_texts.iter() { 27 | // "+10" here is to make sure confirm message has always some padding 28 | if confirm_rect.width >= (line.chars().count() as u16) + 10 { 29 | confirm_text = truncate_middle(line, text_max_length); 30 | confirm_text_start_position = 31 | ((confirm_rect.width - confirm_text.len() as u16) as f64 / 2.0).ceil() as u16 32 | + confirm_rect.x; 33 | break; 34 | } 35 | } 36 | 37 | let y_n_line = "(y/n)"; 38 | let y_n_line_start_position = 39 | ((confirm_rect.width - y_n_line.len() as u16) as f64 / 2.0).ceil() as u16 + confirm_rect.x; 40 | 41 | buf.set_string( 42 | confirm_text_start_position, 43 | confirm_rect.y + confirm_rect.height / 2 - 2, 44 | confirm_text, 45 | text_style, 46 | ); 47 | buf.set_string( 48 | y_n_line_start_position, 49 | confirm_rect.y + confirm_rect.height / 2 + 3, 50 | y_n_line, 51 | text_style, 52 | ); 53 | } 54 | 55 | pub struct ConfirmBox {} 56 | 57 | impl ConfirmBox { 58 | pub fn new() -> Self { 59 | Self {} 60 | } 61 | } 62 | 63 | impl<'a> Widget for ConfirmBox { 64 | fn render(self, area: Rect, buf: &mut Buffer) { 65 | let (width, height) = if area.width > 150 { 66 | (150, 10) 67 | } else if area.width >= 50 { 68 | (area.width / 2, 10) 69 | } else { 70 | unreachable!("app should not be rendered if window is so small") 71 | }; 72 | 73 | // position self in the middle of the self 74 | let x = ((area.x + area.width) / 2) - width / 2; 75 | let y = ((area.y + area.height) / 2) - height / 2; 76 | 77 | let confirm_rect = Rect { 78 | x, 79 | y, 80 | width, 81 | height, 82 | }; 83 | let fill_style = Style::default() 84 | .bg(Color::Black) 85 | .fg(Color::White) 86 | .add_modifier(Modifier::BOLD); 87 | 88 | draw_filled_rect(buf, fill_style, &confirm_rect); 89 | 90 | render_confirm_prompt(buf, &confirm_rect); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/ui/modals/error_box.rs: -------------------------------------------------------------------------------- 1 | use ::tui::buffer::Buffer; 2 | use ::tui::layout::Rect; 3 | use ::tui::style::{Color, Modifier, Style}; 4 | use ::tui::widgets::Widget; 5 | 6 | use crate::ui::format::truncate_end; 7 | use crate::ui::grid::draw_filled_rect; 8 | 9 | pub struct ErrorBox<'a> { 10 | error_message: &'a str, 11 | } 12 | 13 | impl<'a> ErrorBox<'a> { 14 | pub fn new(error_message: &'a str) -> Self { 15 | Self { error_message } 16 | } 17 | } 18 | 19 | impl<'a> Widget for ErrorBox<'a> { 20 | fn render(self, area: Rect, buf: &mut Buffer) { 21 | let (width, height) = if area.width > 150 { 22 | (150, 10) 23 | } else if area.width >= 50 { 24 | (area.width / 2, 10) 25 | } else { 26 | unreachable!("app should not be rendered if window is so small") 27 | }; 28 | 29 | // position self in the middle of the rect 30 | let x = ((area.x + area.width) / 2) - width / 2; 31 | let y = ((area.y + area.height) / 2) - height / 2; 32 | 33 | let message_rect = Rect { 34 | x, 35 | y, 36 | width, 37 | height, 38 | }; 39 | let fill_style = Style::default() 40 | .bg(Color::Black) 41 | .fg(Color::Red) 42 | .add_modifier(Modifier::BOLD); 43 | let text_max_length = message_rect.width - 4; 44 | 45 | // here we truncate the end and not the middle because 46 | // when dealing with error messages, the beginning tends 47 | // to be the important part 48 | let error_text = truncate_end(self.error_message, text_max_length); 49 | let error_text_start_position = 50 | ((message_rect.width - error_text.chars().count() as u16) as f64 / 2.0).ceil() as u16 51 | + message_rect.x; 52 | 53 | let controls_text = ["(Press to dismiss)", "( to dismiss)"]; 54 | 55 | draw_filled_rect(buf, fill_style, &message_rect); 56 | buf.set_string( 57 | error_text_start_position, 58 | message_rect.y + message_rect.height / 2 - 2, 59 | error_text, 60 | fill_style, 61 | ); 62 | 63 | for line in controls_text.iter() { 64 | if text_max_length >= line.chars().count() as u16 { 65 | let start_position = 66 | ((message_rect.width - line.chars().count() as u16) as f64 / 2.0).ceil() as u16 67 | + message_rect.x; 68 | buf.set_string( 69 | start_position, 70 | message_rect.y + message_rect.height / 2 + 2, 71 | line, 72 | fill_style, 73 | ); 74 | break; 75 | } 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/ui/modals/message_box.rs: -------------------------------------------------------------------------------- 1 | use ::tui::buffer::Buffer; 2 | use ::tui::layout::Rect; 3 | use ::tui::style::{Color, Modifier, Style}; 4 | use ::tui::widgets::Widget; 5 | 6 | use crate::state::tiles::FileType; 7 | use crate::state::FileToDelete; 8 | use crate::ui::format::truncate_middle; 9 | use crate::ui::grid::draw_filled_rect; 10 | 11 | fn truncated_file_name_line(file_to_delete: &FileToDelete, max_len: u16) -> String { 12 | let full_path = file_to_delete 13 | .full_path() 14 | .into_os_string() 15 | .into_string() 16 | .expect("could not convert os string to string"); 17 | let file_name = file_to_delete 18 | .path_to_file 19 | .last() 20 | .expect("could not find file to delete") 21 | .to_string_lossy(); 22 | #[cfg(test)] 23 | let full_path = str::replace(&full_path, "\\", "/"); 24 | if max_len > full_path.len() as u16 { 25 | full_path 26 | } else { 27 | truncate_middle(&file_name, max_len) 28 | } 29 | } 30 | 31 | fn render_deletion_prompt(buf: &mut Buffer, message_rect: &Rect, file_to_delete: &FileToDelete) { 32 | let max_text_len = message_rect.width - 4; 33 | let file_name_line = truncated_file_name_line(file_to_delete, max_text_len); 34 | let text_style = Style::default() 35 | .bg(Color::Black) 36 | .fg(Color::Red) 37 | .add_modifier(Modifier::BOLD); 38 | let question_line = match file_to_delete.file_type { 39 | FileType::File => { 40 | if max_text_len >= 17 { 41 | "Delete this file?".to_string() 42 | } else if max_text_len >= 3 { 43 | "Delete?".to_string() 44 | } else { 45 | unreachable!("should not render if terminal is so small"); 46 | } 47 | } 48 | FileType::Folder => { 49 | let children = file_to_delete 50 | .num_descendants 51 | .expect("folder should have descendants"); 52 | let full_line = format!("Delete folder with {} children?", children); 53 | let short_line = "Delete folder?".to_string(); 54 | if max_text_len >= full_line.len() as u16 { 55 | full_line 56 | } else if max_text_len >= short_line.len() as u16 { 57 | short_line 58 | } else { 59 | unreachable!("should not render if terminal is so small"); 60 | } 61 | } 62 | }; 63 | let y_n_line = "(y/n)"; 64 | let question_line_start_position = 65 | ((message_rect.width - question_line.len() as u16) as f64 / 2.0).ceil() as u16 66 | + message_rect.x; 67 | let file_name_line_start_position = 68 | ((message_rect.width - file_name_line.len() as u16) as f64 / 2.0).ceil() as u16 69 | + message_rect.x; 70 | let y_n_line_start_position = 71 | ((message_rect.width - y_n_line.len() as u16) as f64 / 2.0).ceil() as u16 + message_rect.x; 72 | buf.set_string( 73 | question_line_start_position, 74 | message_rect.y + message_rect.height / 2 - 3, 75 | question_line, 76 | text_style, 77 | ); 78 | buf.set_string( 79 | file_name_line_start_position, 80 | message_rect.y + message_rect.height / 2, 81 | file_name_line, 82 | text_style, 83 | ); 84 | buf.set_string( 85 | y_n_line_start_position, 86 | message_rect.y + message_rect.height / 2 + 3, 87 | y_n_line, 88 | text_style, 89 | ); 90 | } 91 | 92 | fn render_deletion_in_progress( 93 | buf: &mut Buffer, 94 | message_rect: &Rect, 95 | file_to_delete: &FileToDelete, 96 | ) { 97 | let max_text_len = message_rect.width - 4; 98 | let file_name_line = truncated_file_name_line(file_to_delete, max_text_len); 99 | let deleting_line = "Deleting"; 100 | let text_style = Style::default() 101 | .bg(Color::Black) 102 | .fg(Color::Red) 103 | .add_modifier(Modifier::BOLD); 104 | let deleting_line_start_position = 105 | ((message_rect.width - deleting_line.len() as u16) as f64 / 2.0).ceil() as u16 106 | + message_rect.x; 107 | let file_line_start_position = ((message_rect.width - file_name_line.len() as u16) as f64 / 2.0) 108 | .ceil() as u16 109 | + message_rect.x; 110 | buf.set_string( 111 | deleting_line_start_position, 112 | message_rect.y + message_rect.height / 2 - 1, 113 | deleting_line, 114 | text_style, 115 | ); 116 | buf.set_string( 117 | file_line_start_position, 118 | message_rect.y + message_rect.height / 2 + 1, 119 | file_name_line, 120 | text_style, 121 | ); 122 | } 123 | 124 | pub struct MessageBox<'a> { 125 | file_to_delete: &'a FileToDelete, 126 | deletion_in_progress: bool, 127 | } 128 | 129 | impl<'a> MessageBox<'a> { 130 | pub fn new(file_to_delete: &'a FileToDelete, deletion_in_progress: bool) -> Self { 131 | Self { 132 | file_to_delete, 133 | deletion_in_progress, 134 | } 135 | } 136 | } 137 | 138 | impl<'a> Widget for MessageBox<'a> { 139 | fn render(self, area: Rect, buf: &mut Buffer) { 140 | let (width, height) = if area.width > 150 { 141 | (150, 10) 142 | } else if area.width >= 50 { 143 | (area.width / 2, 10) 144 | } else { 145 | unreachable!("app should not be rendered if window is so small") 146 | }; 147 | 148 | // position self in the middle of the rect 149 | let x = ((area.x + area.width) / 2) - width / 2; 150 | let y = ((area.y + area.height) / 2) - height / 2; 151 | 152 | let message_rect = Rect { 153 | x, 154 | y, 155 | width, 156 | height, 157 | }; 158 | let fill_style = Style::default() 159 | .bg(Color::Black) 160 | .fg(Color::Red) 161 | .add_modifier(Modifier::BOLD); 162 | 163 | draw_filled_rect(buf, fill_style, &message_rect); 164 | if self.deletion_in_progress { 165 | render_deletion_in_progress(buf, &message_rect, &self.file_to_delete); 166 | } else { 167 | render_deletion_prompt(buf, &message_rect, &self.file_to_delete); 168 | } 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/ui/modals/mod.rs: -------------------------------------------------------------------------------- 1 | mod confirm_box; 2 | mod error_box; 3 | mod message_box; 4 | mod warning_box; 5 | 6 | pub use confirm_box::*; 7 | pub use error_box::*; 8 | pub use message_box::*; 9 | pub use warning_box::*; 10 | -------------------------------------------------------------------------------- /src/ui/modals/warning_box.rs: -------------------------------------------------------------------------------- 1 | use tui::buffer::Buffer; 2 | use tui::layout::Rect; 3 | use tui::style::{Color, Modifier, Style}; 4 | use tui::widgets::Widget; 5 | 6 | use crate::ui::format::truncate_end; 7 | use crate::ui::grid::draw_filled_rect; 8 | 9 | pub struct WarningBox {} 10 | 11 | impl<'a> WarningBox { 12 | pub fn new() -> Self { 13 | Self {} 14 | } 15 | } 16 | 17 | impl<'a> Widget for WarningBox { 18 | fn render(self, area: Rect, buf: &mut Buffer) { 19 | let (width, height) = if area.width > 150 { 20 | (150, 10) 21 | } else if area.width >= 50 { 22 | (area.width / 2, 10) 23 | } else { 24 | unreachable!("app should not be rendered if window is so small") 25 | }; 26 | 27 | // position self in the middle of the rect 28 | let x = ((area.x + area.width) / 2) - width / 2; 29 | let y = ((area.y + area.height) / 2) - height / 2; 30 | 31 | let warning_rect = Rect { 32 | x, 33 | y, 34 | width, 35 | height, 36 | }; 37 | let fill_style = Style::default() 38 | .bg(Color::Black) 39 | .fg(Color::Yellow) 40 | .add_modifier(Modifier::BOLD); 41 | let text_max_length = warning_rect.width - 4; 42 | let mut warning_text_start_position: u16 = 0; 43 | 44 | let possible_warning_texts = [ 45 | "Sorry, deletion is only allowed once the scanning has completed", 46 | "Deletion is not allowed while scanning", 47 | "Can't delete while scanning", 48 | "Can't delete now", 49 | ]; 50 | // set default value of the warning_text 51 | // to the longest one from possible_warning_texts array 52 | let mut warning_text = String::from(possible_warning_texts[0]); 53 | for line in possible_warning_texts.iter() { 54 | // "+5" here is to make sure confirm message has always some padding 55 | if warning_rect.width >= (line.chars().count() as u16) + 5 { 56 | // here we truncate the end and not the middle because 57 | // when dealing with warning messages, the beginning tends 58 | // to be the important part 59 | warning_text = truncate_end(line, text_max_length); 60 | warning_text_start_position = 61 | ((warning_rect.width - warning_text.len() as u16) as f64 / 2.0).ceil() as u16 62 | + warning_rect.x; 63 | break; 64 | } 65 | } 66 | 67 | let controls_text = ["(Press any key to dismiss)", "(any key to dismiss)"]; 68 | 69 | draw_filled_rect(buf, fill_style, &warning_rect); 70 | buf.set_string( 71 | warning_text_start_position, 72 | warning_rect.y + warning_rect.height / 2 - 2, 73 | warning_text, 74 | fill_style, 75 | ); 76 | 77 | for line in controls_text.iter() { 78 | if text_max_length >= line.chars().count() as u16 { 79 | let start_position = 80 | ((warning_rect.width - line.chars().count() as u16) as f64 / 2.0).ceil() as u16 81 | + warning_rect.x; 82 | buf.set_string( 83 | start_position, 84 | warning_rect.y + warning_rect.height / 2 + 2, 85 | line, 86 | fill_style, 87 | ); 88 | break; 89 | } 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/ui/term_too_small.rs: -------------------------------------------------------------------------------- 1 | use ::tui::buffer::Buffer; 2 | use ::tui::layout::Rect; 3 | use ::tui::style::{Modifier, Style}; 4 | use ::tui::widgets::Widget; 5 | 6 | pub struct TermTooSmall {} 7 | 8 | impl TermTooSmall { 9 | pub fn new() -> Self { 10 | Self {} 11 | } 12 | } 13 | 14 | impl<'a> Widget for TermTooSmall { 15 | fn render(self, area: Rect, buf: &mut Buffer) { 16 | let text = [ 17 | "Terminal window is too small ¯\\_(ツ)_/¯", 18 | "Window too small ¯\\_(ツ)_/¯", 19 | "too small ¯\\_(ツ)_/¯", 20 | "¯\\_(ツ)_/¯", 21 | "!!!", 22 | ]; 23 | for line in text.iter() { 24 | if area.width >= line.chars().count() as u16 { 25 | buf.set_string( 26 | ((area.x + area.width) / 2) as u16 - ((line.chars().count() / 2) as u16), 27 | area.y + area.height / 2, 28 | line, 29 | Style::default().add_modifier(Modifier::BOLD), 30 | ); 31 | break; 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/ui/title/mod.rs: -------------------------------------------------------------------------------- 1 | mod title_line; 2 | mod title_telescope; 3 | 4 | pub use title_line::*; 5 | pub use title_telescope::*; 6 | --------------------------------------------------------------------------------