├── .github └── workflows │ ├── ci.yaml │ └── release.yaml ├── .gitignore ├── .gitmodules ├── CMakeLists.txt ├── LICENSE.txt ├── README.md ├── app-gui ├── CMakeLists.txt ├── include │ ├── customstyle.h │ ├── customtheme.h │ ├── fileentrypopup.h │ ├── fileentryview.h │ ├── filetooltip.h │ ├── fileviewdb.h │ ├── inotifydialog.h │ ├── logdialog.h │ ├── mainapp.h │ ├── mainwindow.h │ ├── resources.h │ ├── spaceview.h │ ├── statusview.h │ └── utils-gui.h └── src │ ├── customstyle.cpp │ ├── customtheme.cpp │ ├── fileentrypopup.cpp │ ├── fileentryview.cpp │ ├── filetooltip.cpp │ ├── fileviewdb.cpp │ ├── inotifydialog.cpp │ ├── logdialog.cpp │ ├── main.cpp │ ├── mainapp.cpp │ ├── mainwindow.cpp │ ├── resources.cpp │ ├── spaceview.cpp │ ├── statusview.cpp │ └── utils-gui.cpp ├── build-lnx.sh ├── build-win-mingw.sh ├── build-win-msvc14_x64.bat ├── build-win-msvc15_x64.bat ├── ci ├── linux-build.sh ├── linux-coverage.sh └── linux-install.sh ├── clion-code-style.xml ├── cmake-modules └── CodeCoverage.cmake ├── cmake_win_path.example.txt ├── codecov.yml ├── deps └── CMakeLists.txt ├── images ├── linux-dark.png └── win-light.png ├── lib ├── CMakeLists.txt ├── include │ ├── FileIterator.h │ ├── FileManager.h │ ├── PriorityCache.h │ ├── filedb.h │ ├── fileentry.h │ ├── filepath.h │ ├── logger.h │ ├── platformutils.h │ ├── spacescanner.h │ ├── spacewatcher.h │ └── utils.h ├── private │ ├── LinuxFileIterator.cpp │ ├── LinuxFileIterator.h │ ├── LinuxFileManager.cpp │ ├── LinuxPlatformUtils.cpp │ ├── LinuxSpaceWatcher.cpp │ ├── LinuxSpaceWatcher.h │ ├── WinFileIterator.cpp │ ├── WinFileIterator.h │ ├── WinFileManager.cpp │ ├── WinPlatformUtils.cpp │ ├── WinSpaceWatcher.cpp │ └── WinSpaceWatcher.h └── src │ ├── filedb.cpp │ ├── fileentry.cpp │ ├── filepath.cpp │ ├── logger.cpp │ ├── spacescanner.cpp │ ├── spacewatcher.cpp │ └── utils.cpp ├── res ├── CMakeLists.txt ├── icons │ ├── appicon.ico │ └── svg │ │ ├── appicon-bw.svg │ │ ├── appicon.svg │ │ ├── arrow-back.svg │ │ ├── arrow-forward.svg │ │ ├── folder-navigate-up.svg │ │ ├── folder-refresh.svg │ │ ├── home.svg │ │ ├── new-scan.svg │ │ ├── notify-new.svg │ │ ├── notify.svg │ │ ├── pause.svg │ │ ├── refresh.svg │ │ ├── smooth-mode.svg │ │ ├── space-free.svg │ │ ├── space-unknown.svg │ │ ├── zoom-in.svg │ │ └── zoom-out.svg ├── resources.json ├── spacedisplay.desktop └── win-res.rc └── tests ├── CMakeLists.txt ├── DirHelper.cpp ├── DirHelper.h ├── FileDBTest.cpp ├── FileEntryTest.cpp ├── FileIteratorTest.cpp ├── FilePathTest.cpp ├── LoggerTest.cpp ├── PriorityCacheTest.cpp ├── ScannerTest.cpp ├── SpaceWatcherTest.cpp └── UtilsTest.cpp /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | push: 4 | branches: [ master ] 5 | pull_request: 6 | branches: [ master ] 7 | release: 8 | types: [ published ] 9 | 10 | jobs: 11 | build-win: 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | include: 16 | - qt_arch: win64_msvc2019_64 17 | arch: x64 18 | - qt_arch: win32_msvc2019 19 | arch: Win32 20 | 21 | runs-on: windows-2019 22 | steps: 23 | - uses: actions/checkout@v2 24 | with: 25 | submodules: 'true' 26 | 27 | - name: Cache Qt 28 | id: cache-qt 29 | uses: actions/cache@v1 # not v2! 30 | with: 31 | path: ${{github.workspace}}/qt 32 | key: ${{runner.os}}-Qt5-${{ matrix.arch }} 33 | 34 | - name: Install Qt 35 | uses: jurplel/install-qt-action@v2 36 | with: 37 | version: '5.15.2' 38 | host: 'windows' 39 | target: 'desktop' 40 | arch: ${{ matrix.qt_arch }} 41 | dir: '${{github.workspace}}/qt/' 42 | modules: 'qtsvg' 43 | cached: ${{steps.cache-qt.outputs.cache-hit}} 44 | 45 | - name: Create Build Environment 46 | run: cmake -E make_directory build 47 | 48 | - name: Configure CMake 49 | working-directory: ${{github.workspace}}/build 50 | run: cmake -DQT_WIN_PATH='${{env.Qt5_Dir}}' -DBUILD_TESTS=true -DWIN32_CONSOLE="Off" -G "Visual Studio 16 2019" -A ${{ matrix.arch }} .. 51 | 52 | - name: Build 53 | working-directory: ${{github.workspace}}/build 54 | run: cmake --build . --config Release 55 | 56 | - name: Run tests 57 | working-directory: ${{github.workspace}}/build/tests/Release 58 | run: ./spacedisplay_test 59 | 60 | - name: Copy necessary dlls 61 | working-directory: ${{github.workspace}}/build/app-gui/Release 62 | run: | 63 | cp ${{env.Qt5_Dir}}/bin/Qt5Core.dll Qt5Core.dll 64 | cp ${{env.Qt5_Dir}}/bin/Qt5Gui.dll Qt5Gui.dll 65 | cp ${{env.Qt5_Dir}}/bin/Qt5Widgets.dll Qt5Widgets.dll 66 | cp ${{env.Qt5_Dir}}/bin/Qt5Svg.dll Qt5Svg.dll 67 | mkdir platforms 68 | cp ${{env.Qt5_Dir}}/plugins/platforms/qwindows.dll platforms/qwindows.dll 69 | 70 | # upload binaries 71 | - name: Upload windows binary 72 | uses: actions/upload-artifact@v2 73 | with: 74 | name: spacedisplay-msvc19-${{ matrix.arch }} 75 | path: ${{github.workspace}}/build/app-gui/Release 76 | 77 | build-linux: 78 | runs-on: ubuntu-18.04 79 | steps: 80 | - uses: actions/checkout@v2 81 | with: 82 | submodules: 'true' 83 | 84 | - name: Install tools 85 | run: ./ci/linux-install.sh 86 | 87 | - name: Run coverage 88 | run: ./ci/linux-coverage.sh 89 | 90 | - name: Build 91 | run: ./ci/linux-build.sh 92 | 93 | - name: Run tests 94 | working-directory: ${{github.workspace}}/build/tests 95 | run: ./spacedisplay_test 96 | 97 | - name: Upload AppImage 98 | uses: actions/upload-artifact@v2 99 | with: 100 | name: SpaceDisplay-x86_64.AppImage 101 | path: ${{github.workspace}}/bin/SpaceDisplay-x86_64.AppImage 102 | 103 | - name: Upload coverage to codecov 104 | uses: codecov/codecov-action@v2 105 | with: 106 | files: ${{ github.workspace }}/build_cov/coverage_xml.xml 107 | 108 | - name: Upload coverage artifact 109 | uses: actions/upload-artifact@v2 110 | with: 111 | name: coverage 112 | path: ${{ github.workspace }}/build_cov/coverage_html 113 | 114 | add-release-assets: 115 | runs-on: ubuntu-latest 116 | if: github.event_name == 'release' 117 | needs: [ build-win, build-linux ] 118 | steps: 119 | - uses: actions/download-artifact@v2 120 | with: 121 | path: artifacts 122 | 123 | - name: Display structure of downloaded files 124 | working-directory: artifacts 125 | run: ls -R 126 | 127 | - name: Pack windows x64 128 | working-directory: artifacts/spacedisplay-msvc19-x64 129 | run: zip -r SpaceDisplay-msvc19-x64.zip * 130 | 131 | - name: Pack windows x86 132 | working-directory: artifacts/spacedisplay-msvc19-Win32 133 | run: zip -r SpaceDisplay-msvc19-Win32.zip * 134 | 135 | - uses: svenstaro/upload-release-action@v2 136 | with: 137 | repo_token: ${{ secrets.GITHUB_TOKEN }} 138 | file: artifacts/spacedisplay-msvc19-Win32/SpaceDisplay-msvc19-Win32.zip 139 | tag: ${{ github.ref }} 140 | asset_name: SpaceDisplay-msvc19-Win32.zip 141 | overwrite: true 142 | 143 | - uses: svenstaro/upload-release-action@v2 144 | with: 145 | repo_token: ${{ secrets.GITHUB_TOKEN }} 146 | file: artifacts/spacedisplay-msvc19-x64/SpaceDisplay-msvc19-x64.zip 147 | tag: ${{ github.ref }} 148 | asset_name: SpaceDisplay-msvc19-x64.zip 149 | overwrite: true 150 | 151 | - uses: svenstaro/upload-release-action@v2 152 | with: 153 | repo_token: ${{ secrets.GITHUB_TOKEN }} 154 | file: artifacts/SpaceDisplay-x86_64.AppImage/SpaceDisplay-x86_64.AppImage 155 | tag: ${{ github.ref }} 156 | asset_name: SpaceDisplay-x86_64.AppImage 157 | overwrite: true 158 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | tags: 5 | - '*' 6 | 7 | jobs: 8 | create-release: 9 | if: startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/tags/latest' 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: version 13 | run: echo "::set-output name=version::${GITHUB_REF##*/}" 14 | id: version 15 | 16 | - name: Release 17 | uses: softprops/action-gh-release@v1 18 | with: 19 | token: ${{ secrets.RELEASE_ACCESS_TOKEN }} 20 | name: SpaceDisplay ${{ steps.version.outputs.version }} 21 | draft: false 22 | prerelease: true 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | *build*/ 3 | res/build/ 4 | bin/ 5 | bind/ 6 | tools/ 7 | cmake_win_path.txt 8 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "deps/resource-builder"] 2 | path = deps/resource-builder 3 | url = https://github.com/funbiscuit/resource-builder.git 4 | [submodule "deps/pfd"] 5 | path = deps/pfd 6 | url = https://github.com/funbiscuit/portable-file-dialogs.git 7 | [submodule "deps/crc"] 8 | path = deps/crc 9 | url = https://github.com/panzi/CRC-and-checksum-functions.git 10 | [submodule "deps/catch2"] 11 | path = deps/catch2 12 | url = https://github.com/catchorg/Catch2.git 13 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.8...3.17) 2 | 3 | project(spacedisplay LANGUAGES C CXX) 4 | list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/cmake-modules") 5 | 6 | option(BUILD_TESTS "Build test programs" OFF) 7 | option(TESTS_COV "Run coverage on tests" OFF) 8 | 9 | 10 | if (${TESTS_COV}) 11 | include(CodeCoverage) 12 | append_coverage_compiler_flags() 13 | endif () 14 | # add dependencies 15 | add_subdirectory(deps) 16 | 17 | # add spacescanner library 18 | add_subdirectory(lib) 19 | 20 | # add resources library 21 | add_subdirectory(res) 22 | 23 | # add gui executable 24 | add_subdirectory(app-gui) 25 | 26 | # add tests 27 | if (${BUILD_TESTS}) 28 | add_subdirectory(tests) 29 | if (${TESTS_COV}) 30 | setup_target_for_coverage_gcovr_xml( 31 | NAME coverage_xml 32 | DEPENDENCIES spacedisplay_test spacedisplay_lib 33 | EXCLUDE "app-gui/*" "deps/*" "res/*" "tests/*" 34 | "${PROJECT_BINARY_DIR}/app-gui/*" 35 | EXECUTABLE spacedisplay_test) 36 | setup_target_for_coverage_gcovr_html( 37 | NAME coverage_html 38 | DEPENDENCIES spacedisplay_test spacedisplay_lib 39 | EXCLUDE "app-gui/*" "deps/*" "res/*" "tests/*" 40 | "${PROJECT_BINARY_DIR}/app-gui/*" 41 | EXECUTABLE spacedisplay_test) 42 | endif () 43 | endif () 44 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Sviatoslav Kokurin (funbiscuit) 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 | Space Display 2 | ------------- 3 | [![build](https://github.com/funbiscuit/spacedisplay/actions/workflows/ci.yaml/badge.svg?branch=master)](https://github.com/funbiscuit/spacedisplay/actions/workflows/ci.yaml) 4 | [![codecov](https://codecov.io/gh/funbiscuit/spacedisplay/branch/master/graph/badge.svg)](https://codecov.io/gh/funbiscuit/spacedisplay) 5 | 6 | Almost cross-platform (no macOS support yet) way to analyze used disk space. 7 | 8 | Downloads 9 | --------- 10 | 11 | Check out [GitHub Releases](https://github.com/funbiscuit/spacedisplay/releases) page for the latest build. 12 | 13 | Screenshots 14 | ----------- 15 | 16 | Light theme in Windows 10: 17 | ![Windows-Light-Theme](images/win-light.png) 18 | 19 | Dark theme in Ubuntu 18.04: 20 | ![Linux-Dark-Theme](images/linux-dark.png) 21 | 22 | Basic usage 23 | ----------- 24 | 25 | Run the binary `spacedisplay`. Right click on empty space (or click new icon) and select desired device. 26 | Wait until scan is finished. 27 | Each rectangle represents file (if it is blue) or directory (if it is yellow). 28 | If rectangle is big enough, it will display file/directory name and its size. You can navigate by clicking 29 | on any directory and by using navigation buttons in toolbar. From directory context menu you can 30 | rescan it or open in default file manager. You can also show files in file manager from their context menu. 31 | 32 | Controls 33 | -------- 34 | Mouse wheel - increase/decrease depth of displayed structure 35 | Left mouse click - go to clicked directory 36 | Right mouse click - context menu 37 | F5 - rescan current view 38 | Alt+Left and Alt+Right (or mouse buttons back and forward) - navigate back/forward 39 | Ctrl+N - start a new scan 40 | 41 | Performance 42 | ---------- 43 | 44 | SpaceDisplay is written with speed in mind so it should be quite fast (even faster than default file manager). 45 | First scan might seem slow but it is mainly because OS is not that fast accessing information about files. 46 | It is also much slower to scan an HDD than an SSD. 47 | After the first run OS caches information about files in memory so subsequent scans are much faster. 48 | It might take minutes for first scan of HDD but after that new scan will take just a few seconds. 49 | Here are some test results with time in seconds that takes to fully scan root partition. 50 | 51 | | Platform | Files | SpaceDisplay | File Manager | 52 | |:-----------:|-------|:------------:|:------------:| 53 | | Windows 10 | 510K | 13s | 43s | 54 | | Manjaro KDE | 300K | 1.1s | 2.3s | 55 | 56 | In tests above default file manager is Explorer in Windows and Dolphin in Manjaro KDE. 57 | In default file manager all folders inside root partition (`C:\ ` for windows and `/` for manjaro) where selected and 58 | total size was requested via properties. This step was repeated until time didn't decrease between runs. 59 | 60 | SpaceDisplay is also lightweight in terms of memory usage. 61 | To scan 500k files it uses about 150MB of RAM in 64bit version and 110MB in 32bit version. 62 | Numbers are measured in Windows 10 while scanning drive C:\ with 510k files. 63 | 64 | Requirements 65 | ------------ 66 | 67 | To build from source you will need `qt5`, `python3` and `cmake`. 68 | 69 | Supported compilers are `gcc` (Linux and MinGW) and `msvc` 70 | (tested with Visual Studio 2015 Community Edition) 71 | 72 | Linux is covered in [Linux](#Linux) section. 73 | Windows is covered in [Windows](#Windows) section. 74 | 75 | Building from source 76 | -------------------- 77 | 78 | ### Linux 79 | 80 | Since distributions have their own repositories, check them for following packages (for qt5 names could be different): 81 | 82 | > gcc 83 | > make 84 | > cmake 85 | > qt5-base 86 | > qt5-svg 87 | 88 | For example, if you're using Arch/Manjaro, first you should update existing packages via `pacman -Syu`. 89 | Then install/update required packages: 90 | ```bash 91 | pacman --needed -S gcc make cmake qt5-base qt5-svg 92 | ``` 93 | `gcc` and `make` should be already installed in any Linux distribution. 94 | So you will need to install only `cmake`, `qt5-base` and `qt5-svg` (if they are not installed already). 95 | After installing all required packages you can just build executable using helper file `build-lnx.sh`: 96 | ```bash 97 | chmod +x build-lnx.sh 98 | ./build-lnx.sh 99 | ``` 100 | Just make sure you are at the project root directory. 101 | If it builds successfully, output will be stored in `bin` directory. 102 | 103 | ### Windows 104 | 105 | #### MinGW 106 | 107 | 1. Install [MSYS2](https://www.msys2.org) somewhere, prefer to install it to `C:\msys64` to avoid any problems 108 | 109 | 2. Launch MSYS2 console: `C:\msysXX\msys2.exe` (assuming MSYS2 was installed to `C:\msysXX`). 110 | 111 | 3. Update system by executing `pacman -Syu` 112 | (run this command several times until it will display that there is nothing to update) 113 | 114 | 4. The compilers are not installed by default, so install them and required packages: 115 | 116 | ```bash 117 | pacman -S mingw-w64-x86_64-gcc mingw-w64-x86_64-make mingw-w64-x86_64-cmake mingw-w64-x86_64-qt5 118 | ``` 119 | Now you should be able to build application with mingw64. Launch mingw64 console: 120 | `C:\msysXX\mingw64.exe` (assuming MSYS2 was installed to `C:\msysXX`). 121 | 122 | After that just execute `build-win.sh` helper script while located in project directory to build the app. 123 | If it builds successfully, output will be stored in `bin` directory. It can be run directly from mingw console. 124 | But if you launch it from explorer, you'll need to put required dll's to bin folder. Here's the list: 125 | 126 | > libbz2-1.dll 127 | > libdouble-conversion.dll 128 | > libfreetype-6.dll 129 | > libgcc_s_seh-1.dll 130 | > libglib-2.0-0.dll 131 | > libgraphite2.dll 132 | > libharfbuzz-0.dll 133 | > libiconv-2.dll 134 | > libicudt65.dll 135 | > libicuin65.dll 136 | > libicuuc65.dll 137 | > libintl-8.dll 138 | > libpcre-1.dll 139 | > libpcre2-16-0.dll 140 | > libpng16-16.dll 141 | > libstdc++-6.dll 142 | > libwinpthread-1.dll 143 | > libzstd.dll 144 | > Qt5Core.dll 145 | > Qt5Gui.dll 146 | > Qt5Svg.dll 147 | > Qt5Widgets.dll 148 | > zlib1.dll 149 | 150 | You also need to copy required qt5 plugins from `C:\msysXX\mingw64\share\qt5\plugins` (assuming MSYS2 was installed to `C:\msysXX`): 151 | > platforms/qwindows.dll 152 | 153 | After that you can launch `spacedisplay.exe` from explorer. 154 | 155 | #### Visual Studio 156 | 157 | _You will need to download `qt5` and provide path to visual studio prebuilt binaries when running Cmake. 158 | If you use sample build scripts, you'll have to edit the part "-DQT_WIN_PATH=" so it points to your 159 | installation_ 160 | 161 | 1. Download Cmake 3.8 or higher (for example, to `C:\cmake\cmake-3.8.2-win32-x86`), latest version recommended 162 | and write path to its cmake.exe in `cmake_win_path.txt` (see `cmake_win_path.example.txt` for an example) 163 | 164 | 2. Start command prompt or PowerShell in project directory (using Shift + Right Mouse Click) and run `build-win-msvcXX_x64.bat` where XX is 165 | version of installed Visual Studio. If it's not 14 or 15, use existing `build-win-msvc14_x64.bat` as an example 166 | 167 | 3. Builded binary should be in bin folder, you'll need to put following Qt5 dll's to the bin folder: 168 | 169 | > Qt5Core.dll 170 | > Qt5Gui.dll 171 | > Qt5Svg.dll 172 | > Qt5Widgets.dll 173 | > platforms/qwindows.dll 174 | 175 | Acknowledgements 176 | ---------------- 177 | Space Display uses following open source software: 178 | 179 | Qt5, LGPLv3, 180 | 181 | resource-builder, MIT license, 182 | 183 | Catch2, Boost license, 184 | 185 | crc, public domain, 186 | 187 | portable file dialogs, WTFPL, 188 | -------------------------------------------------------------------------------- /app-gui/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | if (WIN32) 4 | set(QT_WIN_PATH "C:\\" CACHE STRING "Path to installed qt binaries") 5 | message(STATUS "Looking for installed Qt5 at the following path:\n" ${QT_WIN_PATH}) 6 | set(CMAKE_PREFIX_PATH ${QT_WIN_PATH}) 7 | endif () 8 | 9 | find_package(Qt5 COMPONENTS Core Widgets Svg REQUIRED) 10 | 11 | add_executable(spacedisplay_gui 12 | ${CMAKE_CURRENT_SOURCE_DIR}/src/main.cpp 13 | ) 14 | 15 | # sources for gui 16 | add_library(spacedisplay_gui_lib STATIC 17 | ${CMAKE_CURRENT_SOURCE_DIR}/src/mainapp.cpp 18 | ${CMAKE_CURRENT_SOURCE_DIR}/src/customtheme.cpp 19 | ${CMAKE_CURRENT_SOURCE_DIR}/src/customstyle.cpp 20 | ${CMAKE_CURRENT_SOURCE_DIR}/src/fileentrypopup.cpp 21 | ${CMAKE_CURRENT_SOURCE_DIR}/src/fileentryview.cpp 22 | ${CMAKE_CURRENT_SOURCE_DIR}/src/filetooltip.cpp 23 | ${CMAKE_CURRENT_SOURCE_DIR}/src/fileviewdb.cpp 24 | ${CMAKE_CURRENT_SOURCE_DIR}/src/mainwindow.cpp 25 | ${CMAKE_CURRENT_SOURCE_DIR}/src/resources.cpp 26 | ${CMAKE_CURRENT_SOURCE_DIR}/src/spaceview.cpp 27 | ${CMAKE_CURRENT_SOURCE_DIR}/src/statusview.cpp 28 | ${CMAKE_CURRENT_SOURCE_DIR}/src/inotifydialog.cpp 29 | ${CMAKE_CURRENT_SOURCE_DIR}/src/logdialog.cpp 30 | ${CMAKE_CURRENT_SOURCE_DIR}/src/utils-gui.cpp 31 | ) 32 | 33 | # headers for moc generation 34 | target_sources(spacedisplay_gui_lib PRIVATE 35 | ${CMAKE_CURRENT_SOURCE_DIR}/include/fileentrypopup.h 36 | ${CMAKE_CURRENT_SOURCE_DIR}/include/mainwindow.h 37 | ${CMAKE_CURRENT_SOURCE_DIR}/include/spaceview.h 38 | ${CMAKE_CURRENT_SOURCE_DIR}/include/statusview.h 39 | ${CMAKE_CURRENT_SOURCE_DIR}/include/customstyle.h 40 | ${CMAKE_CURRENT_SOURCE_DIR}/include/inotifydialog.h 41 | ${CMAKE_CURRENT_SOURCE_DIR}/include/logdialog.h 42 | ) 43 | 44 | target_compile_features(spacedisplay_gui_lib PUBLIC cxx_std_11) 45 | set_target_properties(spacedisplay_gui_lib PROPERTIES 46 | CXX_EXTENSIONS OFF 47 | AUTOMOC ON) 48 | 49 | target_include_directories(spacedisplay_gui_lib 50 | PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include 51 | ) 52 | 53 | target_link_libraries(spacedisplay_gui_lib PUBLIC 54 | Qt5::Widgets Qt5::Svg portable_file_dialogs 55 | SpacedisplayRes::SpacedisplayRes spacedisplay_lib 56 | ) 57 | 58 | target_link_libraries(spacedisplay_gui PRIVATE spacedisplay_gui_lib) 59 | 60 | if (WIN32) 61 | option(WIN32_CONSOLE "Enable default windows console" ON) 62 | # On Windows disable console if not needed 63 | if (NOT ${WIN32_CONSOLE}) 64 | message(STATUS "Console is disabled") 65 | if (MSVC) # msvc compiler 66 | set_target_properties(spacedisplay_gui PROPERTIES 67 | LINK_FLAGS "/SUBSYSTEM:WINDOWS /ENTRY:mainCRTStartup" 68 | ) 69 | else () # gcc compiler 70 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mwindows") 71 | endif () 72 | endif () 73 | endif () 74 | 75 | install(TARGETS spacedisplay_gui 76 | DESTINATION bin) 77 | -------------------------------------------------------------------------------- /app-gui/include/customstyle.h: -------------------------------------------------------------------------------- 1 | #ifndef SPACEDISPLAY_CUSTOMSTYLE_H 2 | #define SPACEDISPLAY_CUSTOMSTYLE_H 3 | 4 | #include 5 | 6 | class CustomStyle : public QCommonStyle { 7 | Q_OBJECT 8 | 9 | public: 10 | CustomStyle() = default; 11 | 12 | ~CustomStyle() override = default; 13 | 14 | void drawPrimitive(PrimitiveElement element, const QStyleOption *option, 15 | QPainter *painter, const QWidget *widget) const override; 16 | 17 | void drawComplexControl(ComplexControl control, const QStyleOptionComplex *option, 18 | QPainter *painter, const QWidget *widget) const override; 19 | 20 | void drawControl(ControlElement control, const QStyleOption *option, 21 | QPainter *painter, const QWidget *widget) const override; 22 | 23 | int pixelMetric(PixelMetric metric, const QStyleOption *option, 24 | const QWidget *widget) const override; 25 | 26 | QSize sizeFromContents(ContentsType ct, const QStyleOption *opt, 27 | const QSize &contentsSize, const QWidget *widget) const override; 28 | 29 | QRect 30 | subControlRect(ComplexControl cc, const QStyleOptionComplex *opt, SubControl sc, const QWidget *w) const override; 31 | 32 | QRect subElementRect(SubElement r, const QStyleOption *opt, const QWidget *widget) const override; 33 | 34 | int styleHint(StyleHint sh, const QStyleOption *opt, 35 | const QWidget *w, QStyleHintReturn *shret) const override; 36 | 37 | }; 38 | 39 | 40 | #endif //SPACEDISPLAY_CUSTOMSTYLE_H 41 | -------------------------------------------------------------------------------- /app-gui/include/customtheme.h: -------------------------------------------------------------------------------- 1 | #ifndef SPACEDISPLAY_CUSTOMTHEME_H 2 | #define SPACEDISPLAY_CUSTOMTHEME_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include "resource_builder/resources.h" 9 | 10 | class CustomPalette { 11 | public: 12 | enum class Theme { 13 | DARK, 14 | LIGHT 15 | }; 16 | 17 | explicit CustomPalette(Theme theme = Theme::LIGHT); 18 | 19 | void setTheme(Theme theme); 20 | 21 | bool isDark(); 22 | 23 | const QPalette &getPalette() const; 24 | 25 | QColor getViewDirFill(); 26 | 27 | QColor getViewDirLine(); 28 | 29 | QColor getViewFileFill(); 30 | 31 | QColor getViewFileLine(); 32 | 33 | QColor getViewAvailableFill(); 34 | 35 | QColor getViewAvailableLine(); 36 | 37 | QColor getViewUnknownFill(); 38 | 39 | QColor getViewUnknownLine(); 40 | 41 | QColor bgBlend(const QColor &src, double factor); 42 | 43 | static QColor getTextColorFor(const QColor &bg); 44 | 45 | QIcon createIcon(ResourceBuilder::ResId id); 46 | 47 | private: 48 | QPalette palette; 49 | 50 | QColor viewDirFill; 51 | QColor viewFileFill; 52 | QColor viewUnknownFill; 53 | QColor viewAvailableFill; 54 | }; 55 | 56 | #endif //SPACEDISPLAY_CUSTOMTHEME_H 57 | -------------------------------------------------------------------------------- /app-gui/include/fileentrypopup.h: -------------------------------------------------------------------------------- 1 | #ifndef SPACEDISPLAY_FILEENTRYPOPUP_H 2 | #define SPACEDISPLAY_FILEENTRYPOPUP_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | QT_BEGIN_NAMESPACE 11 | class QWidget; 12 | 13 | QT_END_NAMESPACE 14 | 15 | class SpaceScanner; 16 | 17 | class FilePath; 18 | 19 | class FileEntryPopup : public QObject { 20 | Q_OBJECT 21 | 22 | public: 23 | FileEntryPopup(QWidget *_parent); 24 | 25 | ~FileEntryPopup(); 26 | 27 | void popup(std::unique_ptr path); 28 | 29 | void updateActions(const SpaceScanner &scanner); 30 | 31 | std::function onRescanListener = nullptr; 32 | 33 | private slots: 34 | 35 | void onRescan(); 36 | 37 | void onShow(); 38 | 39 | void onOpen(); 40 | 41 | void onDeleteDir(); 42 | 43 | void onDeleteFile(); 44 | 45 | void onProperties(); 46 | 47 | 48 | protected: 49 | std::unique_ptr currentEntryPath; 50 | std::string currentEntryName; 51 | 52 | QWidget *parent; 53 | 54 | //entry popup actions 55 | std::unique_ptr rescanAct; 56 | std::unique_ptr openInFMAct; 57 | std::unique_ptr showInFMAct; 58 | std::unique_ptr deleteDirAct; 59 | std::unique_ptr deleteFileAct; 60 | std::unique_ptr propertiesAct; 61 | }; 62 | 63 | 64 | #endif //SPACEDISPLAY_FILEENTRYPOPUP_H 65 | -------------------------------------------------------------------------------- /app-gui/include/fileentryview.h: -------------------------------------------------------------------------------- 1 | 2 | #ifndef SPACEDISPLAY_FILEENTRYVIEW_H 3 | #define SPACEDISPLAY_FILEENTRYVIEW_H 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include "utils.h" 11 | 12 | class FileEntryView; 13 | 14 | class FilePath; 15 | 16 | class FileEntry; 17 | 18 | /** 19 | * This class is intended to be used only inside shared pointer object! 20 | */ 21 | typedef std::shared_ptr FileEntryViewPtr; // nice short alias 22 | class FileEntryView { 23 | 24 | public: 25 | enum CreateCopyFlags : uint16_t { 26 | DEFAULT = 0x00, 27 | INCLUDE_AVAILABLE_SPACE = 0x01, 28 | INCLUDE_UNKNOWN_SPACE = 0x02 29 | }; 30 | struct ViewOptions { 31 | int nestLevel = 3; 32 | int64_t minSize = 0; 33 | int64_t unknownSpace = 0; //make positive to include 34 | int64_t freeSpace = 0; //make positive to include 35 | }; 36 | enum class EntryType { 37 | DIRECTORY, 38 | FILE, 39 | AVAILABLE_SPACE, 40 | UNKNOWN_SPACE 41 | }; 42 | 43 | FileEntryView(const FileEntryView &) = delete; 44 | 45 | ~FileEntryView(); 46 | 47 | static FileEntryViewPtr createView(const FileEntry *entry, const ViewOptions &options); 48 | 49 | static void updateView(FileEntryViewPtr ©, const FileEntry *entry, const ViewOptions &options); 50 | 51 | const char *getName() const; 52 | 53 | /** 54 | * Writes path to this entry to provided local root. 55 | * For example, if local hierarchy is {this entry}<-{dir1}<-{dir2}<-{dir3} 56 | * Then provided root should have path to {dir3}. And returned path will be {root}/dir2/dir1/{entry name} 57 | * @param root - FilePath to local root. 58 | */ 59 | void getPath(FilePath &root) const; 60 | 61 | int64_t get_size() const { 62 | return size; 63 | } 64 | 65 | FileEntryView *get_parent() const { 66 | return parent; 67 | } 68 | 69 | EntryType get_type() const { 70 | return entryType; 71 | } 72 | 73 | Utils::RectI get_draw_area() const { 74 | return drawArea; 75 | } 76 | 77 | FileEntryView *getHoveredView(int mouseX, int mouseY); 78 | 79 | /** 80 | * Returns closest view to specified path. For example, 81 | * if C:\Windows\System32 is specified, but only C:\Windows exist, then it 82 | * will be returned. 83 | * @param filepath 84 | * @param maxDepth 85 | * @return 86 | */ 87 | FileEntryView *getClosestView(const FilePath &filepath, int maxDepth); 88 | 89 | std::string getFormattedSize() const; 90 | 91 | std::string get_tooltip() const; 92 | 93 | const std::vector &get_children() const; 94 | 95 | uint64_t getId() const; 96 | 97 | bool is_dir() const { 98 | return entryType == EntryType::DIRECTORY; 99 | } 100 | 101 | bool is_file() const { 102 | return entryType == EntryType::FILE; 103 | } 104 | 105 | /** 106 | * Allocates this view and all its children inside specified rectangle. 107 | * Reserves space for titlebar of specified height. 108 | * @param rect - rectangle inside of which view should be allocated 109 | * @param titleHeight - how much space to reserve for title 110 | */ 111 | void allocate_view(Utils::RectI rect, int titleHeight); 112 | 113 | private: 114 | /** 115 | * Makes a copy of existing object as shared pointer 116 | * Copy will not be erased when original entry is removed (so it is kept even after file entry pool reset) 117 | * @param entry original entry that is copied 118 | * @param nestLevel amount of nesting to copy (0 - only entry copied, 1 - entry+children, etc) 119 | * @param minSize minimum size of entry that should be copied. All other entries will not be copied 120 | */ 121 | FileEntryView(const FileEntry *entry, const ViewOptions &options); 122 | 123 | explicit FileEntryView(); 124 | 125 | void reconstruct_from(const FileEntry *entry, const ViewOptions &options); 126 | 127 | void init_from(const FileEntry *entry); 128 | 129 | /** 130 | * Allocates children of this view inside specified rectangle 131 | * Allocates only children in range from start to end (both inclusive) 132 | * @param start 133 | * @param end 134 | * @param rect 135 | */ 136 | void allocate_children(size_t start, size_t end, Utils::RectI &rect); 137 | 138 | void set_child_rect(const FileEntryViewPtr &child, Utils::RectI &rect); 139 | 140 | Utils::RectI drawArea; 141 | 142 | FileEntryView *parent{}; 143 | std::vector children; 144 | int64_t size = 0; 145 | uint64_t id = 0; 146 | std::string name; 147 | EntryType entryType = EntryType::FILE; 148 | 149 | static uint64_t idCounter; 150 | }; 151 | 152 | #endif //SPACEDISPLAY_FILEENTRYVIEW_H 153 | -------------------------------------------------------------------------------- /app-gui/include/filetooltip.h: -------------------------------------------------------------------------------- 1 | #ifndef SPACEDISPLAY_FILETOOLTIP_H 2 | #define SPACEDISPLAY_FILETOOLTIP_H 3 | 4 | #include 5 | #include 6 | 7 | class FileTooltip { 8 | public: 9 | FileTooltip() = default; 10 | 11 | ~FileTooltip(); 12 | 13 | /** 14 | * Set delay for showing tooltip. 15 | * @param ms 16 | */ 17 | void setDelay(int ms); 18 | 19 | /** 20 | * Set tooltip data. 21 | * If tooltip with the same text is already shown, nothing changes. 22 | * If tooltip with the same text is about to be shown, its coordinates will be updated. 23 | * If no tooltip is shown, this tooltip will be show after delay. 24 | * If tooltip with different text was shown before, it will be hidden immediately. 25 | * @param globalX 26 | * @param globalY 27 | * @param text_ 28 | */ 29 | void setTooltip(int globalX, int globalY, const std::string &text_); 30 | 31 | /** 32 | * Hide tooltip if any is shown 33 | */ 34 | void hideTooltip(); 35 | 36 | /** 37 | * Should be called from time to time so tooltip is shown after specified delay 38 | */ 39 | void onTimer(); 40 | 41 | private: 42 | 43 | // time when request for showing tooltip is sent 44 | std::chrono::time_point requestTimepoint; 45 | 46 | std::string text; 47 | int x = 0; 48 | int y = 0; 49 | 50 | int tooltipDelayMs = 0; 51 | 52 | bool tooltipPending = false; 53 | bool isShown = false; 54 | 55 | 56 | void showTooltip(); 57 | 58 | }; 59 | 60 | 61 | #endif //SPACEDISPLAY_FILETOOLTIP_H 62 | -------------------------------------------------------------------------------- /app-gui/include/fileviewdb.h: -------------------------------------------------------------------------------- 1 | #ifndef SPACEDISPLAY_FILEVIEWDB_H 2 | #define SPACEDISPLAY_FILEVIEWDB_H 3 | 4 | #include 5 | #include 6 | #include "utils.h" 7 | 8 | class FileDB; 9 | 10 | class FilePath; 11 | 12 | class FileEntryView; 13 | 14 | /** 15 | * Class for storing FileViewEntries, not thread-safe 16 | * so should be accessed only from GUI thread 17 | */ 18 | class FileViewDB { 19 | 20 | public: 21 | /** 22 | * Updates file views with files from db. Returns true if anything changed 23 | * If any pointers were saved to db entries before update, they should be cleared 24 | * It is not safe to access any pre-update pointers after update! 25 | * Pointers are valid only until the next update 26 | * @param db - FileDB to use to access data 27 | * @param includeUnknown - whether to include file which represents unknown/unscanned space 28 | * @param includeAvailable - whether to include file which represents available/free space 29 | * @return 30 | */ 31 | bool update(const FileDB &db, bool includeUnknown, bool includeAvailable); 32 | 33 | void onThemeChanged(); 34 | 35 | void setViewArea(Utils::RectI rect); 36 | 37 | void setViewPath(const FilePath &filepath); 38 | 39 | void setViewDepth(int depth); 40 | 41 | /** 42 | * Used for calculating required height for file views 43 | * @param depth 44 | */ 45 | void setTextHeight(int height); 46 | 47 | uint64_t getFilesSize() const; 48 | 49 | /** 50 | * Let's you access entry at process (display) it 51 | * If db is not initialized, func will not be called and false is returned 52 | * @param func 53 | * @return false if db not initialized 54 | */ 55 | bool processEntry(const std::function &func); 56 | 57 | FileEntryView *getHoveredView(int mouseX, int mouseY); 58 | 59 | FileEntryView *getClosestView(const FilePath &filepath, int maxDepth); 60 | 61 | private: 62 | 63 | std::shared_ptr rootFile; 64 | 65 | std::unique_ptr viewPath; 66 | Utils::RectI viewRect; 67 | int viewDepth; 68 | int textHeight; 69 | 70 | }; 71 | 72 | 73 | #endif //SPACEDISPLAY_FILEVIEWDB_H 74 | -------------------------------------------------------------------------------- /app-gui/include/inotifydialog.h: -------------------------------------------------------------------------------- 1 | #ifndef SPACEDISPLAY_INOTIFYDIALOG_H 2 | #define SPACEDISPLAY_INOTIFYDIALOG_H 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | 10 | class Logger; 11 | 12 | class InotifyDialog : public QDialog { 13 | Q_OBJECT 14 | 15 | public: 16 | InotifyDialog(int64_t oldWatchLimit, int64_t newWatchLimit, Logger *logger); 17 | 18 | 19 | private: 20 | 21 | std::unique_ptr mainLayout; 22 | 23 | std::string tempCmd; 24 | std::string permCmdArch; 25 | std::string permCmdUbuntu; 26 | 27 | }; 28 | 29 | #endif //SPACEDISPLAY_INOTIFYDIALOG_H 30 | -------------------------------------------------------------------------------- /app-gui/include/logdialog.h: -------------------------------------------------------------------------------- 1 | #ifndef SPACEDISPLAY_LOGDIALOG_H 2 | #define SPACEDISPLAY_LOGDIALOG_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | class Logger; 13 | 14 | class LogDialog : public QDialog { 15 | Q_OBJECT 16 | 17 | public: 18 | explicit LogDialog(std::shared_ptr logger); 19 | 20 | ~LogDialog() override; 21 | 22 | /** 23 | * Sets callback that will be called when log data changes. 24 | * For example, when new data is available, or when data is viewed 25 | * @param callback (bool) - whether new data is available or not 26 | * @return 27 | */ 28 | void setOnDataChanged(std::function callback); 29 | 30 | protected: 31 | void timerEvent(QTimerEvent *event) override; 32 | 33 | 34 | private: 35 | 36 | std::unique_ptr mainLayout; 37 | 38 | std::shared_ptr logger; 39 | std::shared_ptr textEdit; 40 | 41 | int timerId; 42 | std::function onDataChanged; 43 | }; 44 | 45 | #endif //SPACEDISPLAY_LOGDIALOG_H 46 | -------------------------------------------------------------------------------- /app-gui/include/mainapp.h: -------------------------------------------------------------------------------- 1 | #ifndef SPACEDISPLAY_MAINAPP_H 2 | #define SPACEDISPLAY_MAINAPP_H 3 | 4 | class MainApp { 5 | public: 6 | int run(int argc, char *argv[]); 7 | }; 8 | 9 | #endif //SPACEDISPLAY_MAINAPP_H 10 | -------------------------------------------------------------------------------- /app-gui/include/mainwindow.h: -------------------------------------------------------------------------------- 1 | 2 | #ifndef SPACEDISPLAY_MAINWINDOW_H 3 | #define SPACEDISPLAY_MAINWINDOW_H 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | #include "customtheme.h" 10 | 11 | 12 | QT_BEGIN_NAMESPACE 13 | class QAction; 14 | 15 | class QMenu; 16 | 17 | QT_END_NAMESPACE 18 | 19 | class SpaceView; 20 | 21 | class StatusView; 22 | 23 | class SpaceScanner; 24 | 25 | class Logger; 26 | 27 | class LogDialog; 28 | 29 | 30 | class MainWindow : public QMainWindow { 31 | Q_OBJECT 32 | 33 | public: 34 | MainWindow(); 35 | 36 | ~MainWindow() override; 37 | 38 | enum class ActionMask : uint32_t { 39 | NEW_SCAN = 1U << 0U, 40 | BACK = 1U << 1U, 41 | FORWARD = 1U << 2U, 42 | NAVIGATE_UP = 1U << 3U, 43 | HOME = 1U << 4U, 44 | PAUSE = 1U << 5U, 45 | REFRESH = 1U << 6U, 46 | LESS_DETAIL = 1U << 7U, 47 | MORE_DETAIL = 1U << 8U, 48 | TOGGLE_FREE = 1U << 9U, 49 | TOGGLE_UNKNOWN = 1U << 10U, 50 | }; 51 | 52 | void updateAvailableActions(); 53 | 54 | void updateStatusView(); 55 | 56 | protected: 57 | void closeEvent(QCloseEvent *event) override; 58 | 59 | void wheelEvent(QWheelEvent *event) override; 60 | 61 | void mouseReleaseEvent(QMouseEvent *event) override; 62 | 63 | void mouseMoveEvent(QMouseEvent *event) override; 64 | 65 | public slots: 66 | 67 | void newScan(); 68 | 69 | void goBack(); 70 | 71 | void goForward(); 72 | 73 | void goUp(); 74 | 75 | void goHome(); 76 | 77 | void togglePause(); 78 | 79 | void refreshView(); 80 | 81 | void lessDetail(); 82 | 83 | void moreDetail(); 84 | 85 | void toggleFree(); 86 | 87 | void toggleUnknown(); 88 | 89 | void switchTheme(); 90 | 91 | void about(); 92 | 93 | void showLog(); 94 | 95 | private: 96 | void createActions(); 97 | 98 | void createStatusBar(); 99 | 100 | void readSettings(); 101 | 102 | void writeSettings(); 103 | 104 | void setTheme(bool isDark, bool updateIcons); 105 | 106 | void onScanUpdate(); 107 | 108 | void startScan(const std::string &path); 109 | 110 | 111 | void setEnabledActions(ActionMask actions); 112 | 113 | void enableActions(ActionMask actions); 114 | 115 | void disableActions(ActionMask actions); 116 | 117 | void updateIcons(); 118 | 119 | void updateLogIcon(bool hasNew); 120 | 121 | //settings keys 122 | const char *SETTINGS_GEOMETRY = "geometry"; 123 | const char *SETTINGS_THEME = "dark_theme"; 124 | 125 | const char *LOG_ICON_STATE = "log_icon"; 126 | 127 | //actions 128 | std::unique_ptr newAct; 129 | std::unique_ptr backAct; 130 | std::unique_ptr forwardAct; 131 | std::unique_ptr upAct; 132 | std::unique_ptr homeAct; 133 | std::unique_ptr pauseAct; 134 | std::unique_ptr rescanAct; 135 | std::unique_ptr lessDetailAct; 136 | std::unique_ptr moreDetailAct; 137 | std::unique_ptr toggleFreeAct; 138 | std::unique_ptr toggleUnknownAct; 139 | std::unique_ptr themeAct; 140 | std::unique_ptr logAct; 141 | 142 | 143 | ActionMask enabledActions; 144 | bool isRootScanned = false; 145 | 146 | bool watchLimitReported = false; 147 | bool watchLimitExceeded = false; 148 | 149 | std::unique_ptr layout; 150 | CustomPalette customPalette; 151 | 152 | std::unique_ptr spaceWidget; 153 | std::unique_ptr logWindow; 154 | std::shared_ptr logger; 155 | StatusView *statusView; 156 | }; 157 | 158 | #endif //SPACEDISPLAY_MAINWINDOW_H 159 | -------------------------------------------------------------------------------- /app-gui/include/resources.h: -------------------------------------------------------------------------------- 1 | 2 | #ifndef CURVEDETECT_RESOURCES_H 3 | #define CURVEDETECT_RESOURCES_H 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include "resource_builder/resources.h" 10 | 11 | 12 | class Resources { 13 | private: 14 | Resources() = default; 15 | 16 | public: 17 | Resources(const Resources &) = delete; 18 | 19 | Resources &operator=(Resources &) = delete; 20 | 21 | static Resources &get(); 22 | 23 | //qt uses implicit sharing so its safe and efficient to pass pixmap by value 24 | QPixmap get_vector_pixmap(ResourceBuilder::ResId id, int width, const QColor &color, qreal strength = 1.0); 25 | 26 | QPixmap get_vector_pixmap(ResourceBuilder::ResId id, int width); 27 | 28 | private: 29 | 30 | QImage tint(const QImage &src, const QColor &color, qreal strength = 1.0); 31 | 32 | }; 33 | 34 | #endif //CURVEDETECT_RESOURCES_H 35 | -------------------------------------------------------------------------------- /app-gui/include/spaceview.h: -------------------------------------------------------------------------------- 1 | #ifndef SPACEDISPLAY_SPACEVIEW_H 2 | #define SPACEDISPLAY_SPACEVIEW_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "customtheme.h" 11 | #include "PriorityCache.h" 12 | 13 | class SpaceScanner; 14 | 15 | class FileEntryPopup; 16 | 17 | class FilePath; 18 | 19 | class FileViewDB; 20 | 21 | class FileEntryView; 22 | 23 | class FileTooltip; 24 | 25 | class PixmapTextKey { 26 | public: 27 | std::string text; 28 | QRgb color; 29 | 30 | PixmapTextKey(std::string text, QRgb color) : text(std::move(text)), color(color) {} 31 | 32 | friend bool operator==(const PixmapTextKey &a, const PixmapTextKey &b) { 33 | return a.text == b.text && a.color == b.color; 34 | } 35 | }; 36 | 37 | namespace std { 38 | template<> 39 | struct hash { 40 | size_t operator()(const PixmapTextKey &k) const { 41 | size_t h = 17; 42 | h = h * 31 + hash()(k.color); 43 | h = h * 31 + hash()(k.text); 44 | return h; 45 | } 46 | }; 47 | } 48 | 49 | class SpaceView : public QWidget { 50 | Q_OBJECT 51 | public: 52 | SpaceView(); 53 | 54 | ~SpaceView() override; 55 | 56 | void setScanner(std::unique_ptr _scanner); 57 | 58 | void setCustomPalette(const CustomPalette &palette); 59 | 60 | bool getWatcherLimits(int64_t &watchedNow, int64_t &watchLimit); 61 | 62 | void onScanUpdate(); 63 | 64 | bool canRefresh(); 65 | 66 | bool isAtRoot(); 67 | 68 | void rescanDir(const FilePath &dir_path); 69 | 70 | void rescanCurrentView(); 71 | 72 | bool isScanOpen(); 73 | 74 | int getScanProgress(); 75 | 76 | /** 77 | * Returns true if it possible to determine scan progress. 78 | * For example, if we scan partition and can get total occupied space. 79 | * @return 80 | */ 81 | bool isProgressKnown(); 82 | 83 | bool getSpace(int64_t &scannedVisible, int64_t &scannedHidden, int64_t &available, int64_t &total); 84 | 85 | int64_t getScannedFiles(); 86 | 87 | int64_t getScannedDirs(); 88 | 89 | /** 90 | * Sets callback that will be called when any action occurs. 91 | * For example, user navigates to different directory or presses rescan dir 92 | * menu item. 93 | * @param callback 94 | * @return 95 | */ 96 | void setOnActionCallback(std::function callback); 97 | 98 | /** 99 | * Sets callback that will be called when watch limit is exceeded. 100 | * This happens on linux when there is not big enough user limit for inotify watches 101 | * @param callback 102 | * @return 103 | */ 104 | void setOnWatchLimitCallback(std::function callback); 105 | 106 | /** 107 | * Sets callback that will be called when this view requests new scan dialog 108 | * This happens when user opens context menu in empty space 109 | * menu item. 110 | * @param callback 111 | * @return 112 | */ 113 | void setOnNewScanRequestCallback(std::function callback); 114 | 115 | /** 116 | * Navigates to the parent directory 117 | * @return whether after navigating up we're still not at the root directory 118 | * so if true - we can navigate up more 119 | */ 120 | bool navigateUp(); 121 | 122 | /** 123 | * Navigates to the root directory 124 | */ 125 | void navigateHome(); 126 | 127 | /** 128 | * Navigates to the previous view 129 | */ 130 | void navigateBack(); 131 | 132 | /** 133 | * Navigates to the next view 134 | */ 135 | void navigateForward(); 136 | 137 | void increaseDetail(); 138 | 139 | void decreaseDetail(); 140 | 141 | 142 | /** 143 | * @return whether we still have something in history 144 | * so if true - we can navigate back 145 | */ 146 | bool canNavigateBack(); 147 | 148 | /** 149 | * @return whether we still have something in history (in future) 150 | * so if true - we can navigate forward 151 | */ 152 | bool canNavigateForward(); 153 | 154 | /** 155 | * @return whether we can increase detalization (show deeper) 156 | */ 157 | bool canIncreaseDetail(); 158 | 159 | /** 160 | * @return whether we can decrease detalization (show less deep) 161 | */ 162 | bool canDecreaseDetail(); 163 | 164 | bool canTogglePause(); 165 | 166 | bool isPaused(); 167 | 168 | void setPause(bool paused); 169 | 170 | void setShowFreeSpace(bool showFree); 171 | 172 | void setShowUnknownSpace(bool showUnknown); 173 | 174 | void clearHistory(); 175 | 176 | protected: 177 | const int MIN_DEPTH = 1; 178 | const int MAX_DEPTH = 9; 179 | 180 | CustomPalette customPalette; 181 | 182 | QPixmap bgIcon; 183 | 184 | int mouseX = -1; 185 | int mouseY = -1; 186 | 187 | std::unique_ptr entryPopup; 188 | std::unique_ptr fileTooltip; 189 | 190 | /** 191 | * History contains all paths we can get back to. historyPointer points to currentPath 192 | * but it is not safe to access history at pointer, it might be null (it is temporarily 193 | * moved to currentPath. And after navigating back or forward currentPath is moved back 194 | * to history and another path is moved to currentPath 195 | */ 196 | std::vector> pathHistory; 197 | size_t pathHistoryPointer = 0; 198 | 199 | int currentDepth = 5; 200 | std::unique_ptr currentPath; 201 | std::unique_ptr viewDB; 202 | std::unique_ptr hoveredPath; 203 | std::unique_ptr currentScannedPath; 204 | uint64_t hoveredId = 0; 205 | uint64_t currentScannedId = 0; 206 | std::unique_ptr scanner; 207 | 208 | PriorityCache textPixmapCache; 209 | PriorityCache sizePixmapCache; 210 | 211 | /** 212 | * Id of timer that is used to check whether new information is available 213 | * in scanner 214 | */ 215 | int scanTimerId; 216 | 217 | int textHeight = 0; 218 | bool showAvailable = false; 219 | bool showUnknown = true; 220 | 221 | std::function onActionCallback; 222 | std::function onNewScanRequestCallback; 223 | std::function onWatchLimitCallback; 224 | 225 | void mousePressEvent(QMouseEvent *event) override; 226 | 227 | void mouseReleaseEvent(QMouseEvent *event) override; 228 | 229 | void mouseMoveEvent(QMouseEvent *event) override; 230 | 231 | void leaveEvent(QEvent *event) override; 232 | 233 | void paintEvent(QPaintEvent *event) override; 234 | 235 | void resizeEvent(QResizeEvent *event) override; 236 | 237 | void timerEvent(QTimerEvent *event) override; 238 | 239 | void historyPush(); 240 | 241 | void allocateEntries(); 242 | 243 | bool updateHoveredView(); 244 | 245 | void updateScannedView(); 246 | 247 | void drawView(QPainter &painter, const FileEntryView &file, int nestLevel, bool forceFill); 248 | 249 | void drawViewTitle(QPainter &painter, const QColor &bg, const FileEntryView &file); 250 | 251 | void drawViewText(QPainter &painter, const QColor &bg, const FileEntryView &file); 252 | 253 | bool drawViewBg(QPainter &painter, QColor &bg_out, const FileEntryView &file, bool fillDir); 254 | 255 | QPixmap createTextPixmap(const std::string &text, QRgb color); 256 | }; 257 | 258 | #endif //SPACEDISPLAY_SPACEVIEW_H 259 | -------------------------------------------------------------------------------- /app-gui/include/statusview.h: -------------------------------------------------------------------------------- 1 | 2 | #ifndef SPACEDISPLAY_STATUSBAR_WIDGET_H 3 | #define SPACEDISPLAY_STATUSBAR_WIDGET_H 4 | 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | 11 | #include "customtheme.h" 12 | 13 | struct StatusPart { 14 | QColor color; 15 | bool isHidden = false; 16 | float weight = 0.f; 17 | std::string label; 18 | int textMargin = 10; 19 | }; 20 | 21 | class StatusView : public QWidget { 22 | Q_OBJECT 23 | public: 24 | // depending on what mode is selected, status will display differently 25 | enum class Mode { 26 | NO_SCAN, 27 | SCANNING_DEFINITE, //for scanning with known progress 28 | SCANNING_INDEFINITE, //for scanning with unknown progress 29 | SCAN_FINISHED 30 | }; 31 | 32 | void setSpace(float scannedVisible, float scannedHidden, float available, float unknown); 33 | 34 | /** 35 | * Set percent of scan progress (from 0 to 100) 36 | * But 100% will never be shown. If scan in progress, it will be limited to 99% 37 | * @param progress 38 | */ 39 | void setProgress(int progress); 40 | 41 | void setCustomPalette(const CustomPalette &palette); 42 | 43 | void setMode(Mode mode); 44 | 45 | /** 46 | * Set what space to highlight, otherwise it will be drawn but not highlighted 47 | * @param available 48 | * @param unknown 49 | */ 50 | void setSpaceHighlight(bool available, bool unknown); 51 | 52 | /** 53 | * If true, unknown and available spaces will be minimized 54 | * All remaining space will be filled by scanned space (it will be maximized) 55 | * @param minimize 56 | */ 57 | void setMaximizeSpace(bool maximize); 58 | 59 | /** 60 | * Set how much files are scanned 61 | * @param files 62 | */ 63 | void setScannedFiles(int64_t files); 64 | 65 | QSize minimumSizeHint() const override; 66 | 67 | protected: 68 | float scannedSpaceVisible = 0.f; 69 | float scannedSpaceHidden = 0.f; 70 | float availableSpace = 0.f; 71 | float unknownSpace = 1.f; 72 | 73 | int scanProgress = 0; 74 | 75 | int64_t scannedFiles = 0; 76 | 77 | Mode currentMode; 78 | 79 | bool highlightAvailable = false; 80 | bool highlightUnknown = false; 81 | bool maximizeSpace = false; 82 | 83 | const int textPadding = 3; 84 | 85 | CustomPalette customPalette; 86 | 87 | std::vector parts; 88 | 89 | std::string getScanStatusText(); 90 | 91 | int getStatusWidth(const QFontMetrics &fm); 92 | 93 | std::string getScannedFilesText(); 94 | 95 | void allocateParts(const QFontMetrics &fm, float width); 96 | 97 | void paintEvent(QPaintEvent *event) override; 98 | }; 99 | 100 | #endif //SPACEDISPLAY_STATUSBAR_WIDGET_H 101 | -------------------------------------------------------------------------------- /app-gui/include/utils-gui.h: -------------------------------------------------------------------------------- 1 | #ifndef SPACEDISPLAY_UTILS_GUI_H 2 | #define SPACEDISPLAY_UTILS_GUI_H 3 | 4 | #include 5 | 6 | #include 7 | 8 | namespace UtilsGui { 9 | 10 | /** 11 | * Let's user select desired folder with their default manager and returns its path 12 | * @param title of window for selecting folder 13 | * @return path to selected folder or empty string if cancelled 14 | */ 15 | std::string select_folder(const std::string &title); 16 | 17 | /** 18 | * Show's user some message text 19 | * @param title of message box 20 | * @param text in message box 21 | */ 22 | void message_box(const std::string &title, const std::string &text); 23 | 24 | /** 25 | * Blend fg color with bg color by bg_amount 26 | * @param fg 27 | * @param bg 28 | * @param bg_amount 29 | * @return fg*(1-bg_amount)+bg*bg_amount 30 | */ 31 | QColor blend(const QColor &fg, const QColor &bg, qreal bg_amount); 32 | 33 | bool isDark(const QColor &bg); 34 | } 35 | 36 | #endif //SPACEDISPLAY_UTILS_GUI_H 37 | -------------------------------------------------------------------------------- /app-gui/src/customtheme.cpp: -------------------------------------------------------------------------------- 1 | #include "customtheme.h" 2 | #include 3 | 4 | #include "resources.h" 5 | #include "utils-gui.h" 6 | 7 | CustomPalette::CustomPalette(Theme theme) { 8 | palette = QApplication::palette(); 9 | setTheme(theme); 10 | } 11 | 12 | const QPalette &CustomPalette::getPalette() const { 13 | return palette; 14 | } 15 | 16 | bool CustomPalette::isDark() { 17 | return UtilsGui::isDark(palette.window().color()); 18 | } 19 | 20 | void CustomPalette::setTheme(Theme theme) { 21 | if (theme == Theme::DARK) { 22 | palette.setColor(QPalette::Window, QColor(60, 63, 65)); 23 | palette.setColor(QPalette::WindowText, QColor(187, 187, 187)); 24 | palette.setColor(QPalette::Base, QColor(60, 63, 65)); 25 | palette.setColor(QPalette::Text, QColor(187, 187, 187)); 26 | 27 | palette.setColor(QPalette::Midlight, QColor(81, 81, 81)); 28 | palette.setColor(QPalette::Mid, QColor(92, 97, 100)); 29 | palette.setColor(QPalette::Dark, QColor(175, 177, 179)); 30 | palette.setColor(QPalette::Button, QColor(76, 80, 82)); 31 | 32 | palette.setColor(QPalette::Highlight, QColor(75, 110, 175)); 33 | palette.setColor(QPalette::HighlightedText, QColor(187, 187, 187)); 34 | 35 | palette.setColor(QPalette::ToolTipBase, QColor(75, 77, 77)); 36 | palette.setColor(QPalette::ToolTipText, QColor(191, 191, 191)); 37 | 38 | viewDirFill = QColor(150, 130, 95); 39 | viewFileFill = QColor(65, 85, 115); 40 | viewAvailableFill = QColor(73, 156, 84); 41 | viewUnknownFill = QColor(120, 120, 110); 42 | } else { 43 | palette.setColor(QPalette::Window, QColor(242, 242, 242)); 44 | palette.setColor(QPalette::WindowText, QColor(35, 35, 35)); 45 | palette.setColor(QPalette::Base, QColor(242, 242, 242)); 46 | palette.setColor(QPalette::Text, QColor(0, 0, 0)); 47 | 48 | palette.setColor(QPalette::Midlight, QColor(214, 214, 214)); 49 | palette.setColor(QPalette::Mid, QColor(207, 207, 207)); 50 | palette.setColor(QPalette::Dark, QColor(110, 110, 110)); 51 | palette.setColor(QPalette::Button, QColor(255, 255, 255)); 52 | 53 | palette.setColor(QPalette::Highlight, QColor(40, 120, 200)); 54 | palette.setColor(QPalette::HighlightedText, QColor(255, 255, 255)); 55 | 56 | palette.setColor(QPalette::ToolTipBase, QColor(247, 247, 247)); 57 | palette.setColor(QPalette::ToolTipText, QColor(35, 35, 35)); 58 | 59 | viewDirFill = QColor(255, 222, 173); 60 | viewFileFill = QColor(119, 158, 203); 61 | viewAvailableFill = QColor(119, 221, 119); 62 | viewUnknownFill = QColor(207, 207, 196); 63 | } 64 | } 65 | 66 | QColor CustomPalette::bgBlend(const QColor &src, double factor) { 67 | return UtilsGui::blend(src, palette.window().color(), factor); 68 | } 69 | 70 | QColor CustomPalette::getTextColorFor(const QColor &bg) { 71 | return bg.lightnessF() > 0.5f ? QColor(60, 60, 60) : QColor(230, 230, 230); 72 | } 73 | 74 | QIcon CustomPalette::createIcon(ResourceBuilder::ResId id) { 75 | auto &r = Resources::get(); 76 | 77 | auto color = palette.dark().color(); 78 | 79 | QIcon icon(r.get_vector_pixmap(id, 64, color)); 80 | icon.addPixmap(r.get_vector_pixmap(id, 64, bgBlend(color, 0.7)), QIcon::Disabled); 81 | 82 | return icon; 83 | } 84 | 85 | QColor CustomPalette::getViewDirFill() { 86 | return viewDirFill; 87 | } 88 | 89 | QColor CustomPalette::getViewDirLine() { 90 | return viewDirFill.darker(125); 91 | } 92 | 93 | QColor CustomPalette::getViewFileFill() { 94 | return viewFileFill; 95 | } 96 | 97 | QColor CustomPalette::getViewFileLine() { 98 | return viewFileFill.darker(125); 99 | } 100 | 101 | QColor CustomPalette::getViewAvailableFill() { 102 | return viewAvailableFill; 103 | } 104 | 105 | QColor CustomPalette::getViewAvailableLine() { 106 | return viewAvailableFill.darker(125); 107 | } 108 | 109 | QColor CustomPalette::getViewUnknownFill() { 110 | return viewUnknownFill; 111 | } 112 | 113 | QColor CustomPalette::getViewUnknownLine() { 114 | return viewUnknownFill.darker(125); 115 | } 116 | -------------------------------------------------------------------------------- /app-gui/src/fileentrypopup.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | #include 6 | #include "fileentrypopup.h" 7 | #include "spacescanner.h" 8 | #include "filepath.h" 9 | #include "utils.h" 10 | #include "platformutils.h" 11 | #include "FileManager.h" 12 | 13 | FileEntryPopup::FileEntryPopup(QWidget *_parent) : parent(_parent) { 14 | rescanAct = Utils::make_unique("Rescan folder", parent); 15 | rescanAct->setEnabled(false); 16 | connect(rescanAct.get(), &QAction::triggered, this, &FileEntryPopup::onRescan); 17 | 18 | openInFMAct = Utils::make_unique("Open in File Manager", parent); 19 | connect(openInFMAct.get(), &QAction::triggered, this, &FileEntryPopup::onOpen); 20 | 21 | showInFMAct = Utils::make_unique("Show in File Manager", parent); 22 | connect(showInFMAct.get(), &QAction::triggered, this, &FileEntryPopup::onShow); 23 | 24 | deleteDirAct = Utils::make_unique("Delete (recursively)", parent); 25 | deleteDirAct->setEnabled(false); 26 | connect(deleteDirAct.get(), &QAction::triggered, this, &FileEntryPopup::onDeleteDir); 27 | 28 | deleteFileAct = Utils::make_unique("Delete", parent); 29 | deleteFileAct->setEnabled(false); 30 | connect(deleteFileAct.get(), &QAction::triggered, this, &FileEntryPopup::onDeleteFile); 31 | 32 | propertiesAct = Utils::make_unique("Properties", parent); 33 | propertiesAct->setEnabled(false); 34 | connect(propertiesAct.get(), &QAction::triggered, this, &FileEntryPopup::onProperties); 35 | 36 | } 37 | 38 | FileEntryPopup::~FileEntryPopup() {} 39 | 40 | void FileEntryPopup::updateActions(const SpaceScanner &scanner) { 41 | rescanAct->setEnabled(true); 42 | //todo maybe allow to delete while scanning 43 | // deleteDirAct->setEnabled(scanner->can_refresh()); 44 | } 45 | 46 | void FileEntryPopup::onRescan() { 47 | if (onRescanListener) 48 | onRescanListener(*currentEntryPath); 49 | } 50 | 51 | void FileEntryPopup::onDeleteDir() { 52 | std::cout << "delete dir\n"; 53 | } 54 | 55 | void FileEntryPopup::onDeleteFile() { 56 | std::cout << "delete file\n"; 57 | } 58 | 59 | void FileEntryPopup::onProperties() { 60 | std::cout << "properties\n"; 61 | } 62 | 63 | void FileEntryPopup::onShow() { 64 | FileManager::showFile(currentEntryPath->getPath()); 65 | } 66 | 67 | void FileEntryPopup::onOpen() { 68 | FileManager::openFolder(currentEntryPath->getPath()); 69 | } 70 | 71 | void FileEntryPopup::popup(std::unique_ptr path) { 72 | currentEntryPath = std::move(path); 73 | currentEntryName = currentEntryPath->getName(); 74 | 75 | QMenu menu(parent); 76 | auto title = menu.addAction(currentEntryName.c_str()); 77 | title->setEnabled(false); 78 | menu.addSeparator(); 79 | if (currentEntryPath->isDir()) { 80 | menu.addAction(rescanAct.get()); 81 | menu.addAction(openInFMAct.get()); 82 | menu.addAction(deleteDirAct.get()); 83 | } else { 84 | menu.addAction(showInFMAct.get()); 85 | menu.addAction(deleteFileAct.get()); 86 | } 87 | menu.addAction(propertiesAct.get()); 88 | 89 | menu.exec(QCursor::pos() + QPoint(10, 10)); 90 | } 91 | -------------------------------------------------------------------------------- /app-gui/src/filetooltip.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include "filetooltip.h" 3 | #include 4 | 5 | FileTooltip::~FileTooltip() { 6 | hideTooltip(); 7 | } 8 | 9 | void FileTooltip::setDelay(int ms) { 10 | tooltipDelayMs = ms; 11 | } 12 | 13 | void FileTooltip::setTooltip(int globalX, int globalY, const std::string &text_) { 14 | // new tooltip is shown only if text is different 15 | //TODO probably should check also other things 16 | if (text != text_) { 17 | hideTooltip(); 18 | requestTimepoint = std::chrono::steady_clock::now(); 19 | tooltipPending = true; 20 | text = text_; 21 | } 22 | x = globalX; 23 | y = globalY; 24 | 25 | } 26 | 27 | void FileTooltip::hideTooltip() { 28 | tooltipPending = false; 29 | if (isShown && QToolTip::isVisible()) { 30 | QToolTip::hideText(); 31 | isShown = false; 32 | } 33 | } 34 | 35 | void FileTooltip::onTimer() { 36 | if (!tooltipPending) 37 | return; 38 | using namespace std::chrono; 39 | auto now = steady_clock::now(); 40 | if (duration_cast(now - requestTimepoint).count() > tooltipDelayMs) { 41 | // show tooltip after specified delay 42 | showTooltip(); 43 | } 44 | } 45 | 46 | void FileTooltip::showTooltip() { 47 | tooltipPending = false; 48 | isShown = true; 49 | QToolTip::showText(QPoint(x, y), text.c_str()); 50 | } 51 | -------------------------------------------------------------------------------- /app-gui/src/fileviewdb.cpp: -------------------------------------------------------------------------------- 1 | #include "fileviewdb.h" 2 | 3 | #include "filepath.h" 4 | #include "filedb.h" 5 | #include "fileentry.h" 6 | #include "fileentryview.h" 7 | 8 | 9 | bool FileViewDB::update(const FileDB &db, bool includeUnknown, bool includeAvailable) { 10 | if (!viewPath) 11 | return false; 12 | FileEntryView::ViewOptions options; 13 | options.nestLevel = viewDepth; 14 | 15 | int64_t totalSpace, usedSpace, freeSpace, unknownSpace; 16 | 17 | db.getSpace(usedSpace, freeSpace, totalSpace); 18 | unknownSpace = totalSpace - freeSpace - usedSpace; //db guarantees this to be >=0 19 | totalSpace = 0; //we will calculate it manually depending on what is included 20 | 21 | if (includeAvailable) { 22 | options.freeSpace = freeSpace; 23 | totalSpace += freeSpace; 24 | } 25 | if (includeUnknown) { 26 | options.unknownSpace = unknownSpace; 27 | totalSpace += unknownSpace; 28 | } 29 | 30 | bool hasChanges = false; 31 | 32 | db.processEntry(*viewPath, [this, &totalSpace, &options, &hasChanges](const FileEntry &entry) { 33 | if (!entry.isRoot()) { 34 | totalSpace = 0; 35 | //we don't show unknown and free space if child is viewed 36 | options.freeSpace = 0; 37 | options.unknownSpace = 0; 38 | } 39 | 40 | float minSizeRatio = (7.f * 7.f) / float(viewRect.h * viewRect.w); 41 | 42 | totalSpace += entry.getSize(); 43 | 44 | options.minSize = int64_t(float(totalSpace) * minSizeRatio); 45 | if (rootFile) 46 | FileEntryView::updateView(rootFile, &entry, options); 47 | else 48 | rootFile = FileEntryView::createView(&entry, options); 49 | //TODO check if anything actually changed? 50 | hasChanges = true; 51 | }); 52 | 53 | // rootFile should be valid if lambda was called but in case something went wrong 54 | if (hasChanges && rootFile) 55 | rootFile->allocate_view(viewRect, (textHeight * 3) / 2); 56 | 57 | return hasChanges; 58 | } 59 | 60 | void FileViewDB::onThemeChanged() { 61 | // just reset pointer to root so on next update it will create new pixmaps with proper color 62 | rootFile.reset(); 63 | } 64 | 65 | void FileViewDB::setViewArea(Utils::RectI rect) { 66 | viewRect = rect; 67 | } 68 | 69 | void FileViewDB::setViewPath(const FilePath &filepath) { 70 | viewPath = Utils::make_unique(filepath); 71 | } 72 | 73 | void FileViewDB::setViewDepth(int depth) { 74 | viewDepth = depth; 75 | } 76 | 77 | void FileViewDB::setTextHeight(int height) { 78 | textHeight = height; 79 | } 80 | 81 | uint64_t FileViewDB::getFilesSize() const { 82 | if (rootFile) 83 | return rootFile->get_size(); 84 | return 0; 85 | } 86 | 87 | bool FileViewDB::processEntry(const std::function &func) { 88 | if (!rootFile) 89 | return false; 90 | 91 | func(*rootFile); 92 | 93 | return true; 94 | } 95 | 96 | FileEntryView *FileViewDB::getHoveredView(int mouseX, int mouseY) { 97 | if (!rootFile) 98 | return nullptr; 99 | 100 | return rootFile->getHoveredView(mouseX, mouseY); 101 | } 102 | 103 | FileEntryView *FileViewDB::getClosestView(const FilePath &filepath, int maxDepth) { 104 | if (!rootFile) 105 | return nullptr; 106 | 107 | auto state = viewPath->compareTo(filepath); 108 | if (state == FilePath::CompareResult::DIFFERENT || state == FilePath::CompareResult::CHILD) 109 | return nullptr; 110 | 111 | if (state == FilePath::CompareResult::EQUAL) 112 | return rootFile.get(); 113 | 114 | auto relPath = filepath; 115 | if (!relPath.makeRelativeTo(*viewPath)) 116 | return nullptr; //should not happen 117 | 118 | return rootFile->getClosestView(relPath, maxDepth); 119 | } 120 | -------------------------------------------------------------------------------- /app-gui/src/inotifydialog.cpp: -------------------------------------------------------------------------------- 1 | #include "inotifydialog.h" 2 | 3 | #include 4 | 5 | #include "utils.h" 6 | #include "logger.h" 7 | #include 8 | 9 | InotifyDialog::InotifyDialog(int64_t oldWatchLimit, int64_t newWatchLimit, Logger *logger) { 10 | setWindowTitle("Inotify watches limit is reached"); 11 | 12 | auto inotifyCmd = Utils::strFormat("fs.inotify.max_user_watches=%d", newWatchLimit); 13 | tempCmd = Utils::strFormat("sudo sysctl %s", inotifyCmd.c_str()); 14 | permCmdArch = Utils::strFormat("echo %s | sudo tee /etc/sysctl.d/40-max-user-watches.conf" 15 | " && sudo sysctl --system", inotifyCmd.c_str()); 16 | permCmdUbuntu = Utils::strFormat("echo %s | sudo tee -a /etc/sysctl.conf &&" 17 | " sudo sysctl -p", inotifyCmd.c_str()); 18 | 19 | if (logger) { 20 | logger->log(Utils::strFormat("Current watch limit (%d) is reached, " 21 | "not all changes will be detected.", oldWatchLimit), "WATCH"); 22 | logger->log(Utils::strFormat("Increase limit to at least %d", newWatchLimit), "WATCH"); 23 | } 24 | 25 | 26 | auto labelMsg = new QLabel(); 27 | auto labelPermArch = new QLabel(); 28 | auto labelPermUbuntu = new QLabel(); 29 | labelMsg->setText(Utils::strFormat("User limit of inotify watches (%d) is reached, " 30 | "not all changes will be detected.
" 31 | "Increase limit to at least %d. To increase it temporary execute:", 32 | oldWatchLimit, newWatchLimit).c_str()); 33 | labelPermArch->setText("To permanently increase limit, if you're running Arch:"); 34 | labelPermUbuntu->setText("If you are running Debian, RedHat, or another similar Linux distribution:"); 35 | 36 | auto lineTemp = new QLineEdit(); 37 | auto linePermArch = new QLineEdit(); 38 | auto linePermUbuntu = new QLineEdit(); 39 | lineTemp->setReadOnly(true); 40 | linePermArch->setReadOnly(true); 41 | linePermUbuntu->setReadOnly(true); 42 | lineTemp->setText(tempCmd.c_str()); 43 | linePermArch->setText(permCmdArch.c_str()); 44 | linePermUbuntu->setText(permCmdUbuntu.c_str()); 45 | lineTemp->setCursorPosition(0); 46 | linePermArch->setCursorPosition(0); 47 | linePermUbuntu->setCursorPosition(0); 48 | 49 | auto copyTempCmd = new QPushButton("Copy"); 50 | auto copyPermArch = new QPushButton("Copy"); 51 | auto copyPermUbuntu = new QPushButton("Copy"); 52 | copyTempCmd->setAutoDefault(false); 53 | copyPermArch->setAutoDefault(false); 54 | copyPermUbuntu->setAutoDefault(false); 55 | 56 | mainLayout = Utils::make_unique(); 57 | 58 | mainLayout->addWidget(labelMsg, 0, 0); 59 | 60 | mainLayout->addWidget(lineTemp, 1, 0); 61 | mainLayout->addWidget(copyTempCmd, 1, 1); 62 | 63 | mainLayout->addWidget(labelPermArch, 2, 0); 64 | 65 | mainLayout->addWidget(linePermArch, 3, 0); 66 | mainLayout->addWidget(copyPermArch, 3, 1); 67 | 68 | mainLayout->addWidget(labelPermUbuntu, 4, 0); 69 | 70 | mainLayout->addWidget(linePermUbuntu, 5, 0); 71 | mainLayout->addWidget(copyPermUbuntu, 5, 1); 72 | 73 | setLayout(mainLayout.get()); 74 | 75 | connect(copyTempCmd, &QPushButton::clicked, this, [this]() { 76 | QApplication::clipboard()->setText(tempCmd.c_str()); 77 | }); 78 | connect(copyPermArch, &QPushButton::clicked, this, [this]() { 79 | QApplication::clipboard()->setText(permCmdArch.c_str()); 80 | }); 81 | connect(copyPermUbuntu, &QPushButton::clicked, this, [this]() { 82 | QApplication::clipboard()->setText(permCmdUbuntu.c_str()); 83 | }); 84 | } -------------------------------------------------------------------------------- /app-gui/src/logdialog.cpp: -------------------------------------------------------------------------------- 1 | #include "logdialog.h" 2 | 3 | #include 4 | 5 | #include "utils.h" 6 | #include "logger.h" 7 | 8 | #include 9 | 10 | LogDialog::LogDialog(std::shared_ptr logger) : logger(std::move(logger)) { 11 | setWindowTitle("SpaceDisplay Log"); 12 | setMinimumSize(500, 350); 13 | setFocusPolicy(Qt::ClickFocus); 14 | 15 | textEdit = std::make_shared(); 16 | textEdit->setReadOnly(true); 17 | 18 | auto clearLog = new QPushButton("Clear"); 19 | clearLog->setAutoDefault(false); 20 | 21 | mainLayout = Utils::make_unique(); 22 | 23 | mainLayout->addWidget(textEdit.get(), 1); 24 | mainLayout->addWidget(clearLog, 0, Qt::AlignRight); 25 | 26 | setLayout(mainLayout.get()); 27 | 28 | connect(clearLog, &QPushButton::clicked, this, [this]() { 29 | textEdit->clear(); 30 | if (this->logger) 31 | this->logger->clear(); 32 | }); 33 | 34 | timerId = startTimer(100); 35 | } 36 | 37 | LogDialog::~LogDialog() { 38 | killTimer(timerId); 39 | } 40 | 41 | void LogDialog::timerEvent(QTimerEvent *event) { 42 | if (!logger) 43 | return; 44 | 45 | if (logger->hasNew()) { 46 | std::stringstream str; 47 | auto logs = logger->getHistory(); 48 | for (auto &log : logs) 49 | str << log << '\n'; 50 | textEdit->setPlainText(str.str().c_str()); 51 | textEdit->moveCursor(QTextCursor::End); 52 | if (onDataChanged) 53 | onDataChanged(true); 54 | } 55 | } 56 | 57 | void LogDialog::setOnDataChanged(std::function callback) { 58 | onDataChanged = std::move(callback); 59 | } 60 | -------------------------------------------------------------------------------- /app-gui/src/main.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include "mainapp.h" 3 | 4 | int main(int argc, char *argv[]) { 5 | MainApp app; 6 | return app.run(argc, argv); 7 | } 8 | -------------------------------------------------------------------------------- /app-gui/src/mainapp.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include "mainapp.h" 3 | 4 | #include 5 | 6 | #include "mainwindow.h" 7 | #include "resources.h" 8 | 9 | #include "customstyle.h" 10 | 11 | int MainApp::run(int argc, char *argv[]) { 12 | QApplication app(argc, argv); 13 | QCoreApplication::setOrganizationName("com.github.funbiscuit"); 14 | QCoreApplication::setApplicationName("Space Display"); 15 | QCoreApplication::setApplicationVersion("1.0.0"); 16 | // QCommandLineParser parser; 17 | // parser.setApplicationDescription(QCoreApplication::applicationName()); 18 | // parser.addHelpOption(); 19 | // parser.addVersionOption(); 20 | // parser.addPositionalArgument("file", "The file to open."); 21 | // parser.process(app); 22 | 23 | 24 | using namespace ResourceBuilder; 25 | auto &r = Resources::get(); 26 | 27 | QIcon appIcon(r.get_vector_pixmap(ResId::__ICONS_SVG_APPICON_SVG, 16)); 28 | //add other sizes 29 | for (int sz = 32; sz <= 256; sz *= 2) 30 | appIcon.addPixmap(r.get_vector_pixmap(ResId::__ICONS_SVG_APPICON_SVG, sz)); 31 | 32 | QApplication::setWindowIcon(appIcon); 33 | QApplication::setStyle(new CustomStyle); 34 | auto f = QApplication::font(); 35 | f.setPointSize(11); 36 | QApplication::setFont(f); 37 | 38 | MainWindow mainWin; 39 | mainWin.show(); 40 | return QApplication::exec(); 41 | } 42 | 43 | -------------------------------------------------------------------------------- /app-gui/src/resources.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | 5 | #include "resources.h" 6 | #include "resource_builder/resources.h" 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | 16 | Resources &Resources::get() { 17 | static Resources instance; 18 | return instance; 19 | } 20 | 21 | QImage Resources::tint(const QImage &src, const QColor &color, qreal strength) { 22 | // FIXME tint result is not very good if source is complete black and target is complete wight 23 | if (src.isNull()) return QImage(); 24 | QGraphicsScene scene; 25 | QGraphicsPixmapItem item; 26 | item.setPixmap(QPixmap::fromImage(src)); 27 | QGraphicsColorizeEffect effect; 28 | effect.setColor(color); 29 | effect.setStrength(strength); 30 | item.setGraphicsEffect(&effect); 31 | scene.addItem(&item); 32 | QImage res(src); 33 | QPainter ptr(&res); 34 | scene.render(&ptr, QRectF(), src.rect()); 35 | return res; 36 | } 37 | 38 | QPixmap Resources::get_vector_pixmap(ResourceBuilder::ResId id, int width) { 39 | return get_vector_pixmap(id, width, QColor(Qt::white), 0.0); 40 | } 41 | 42 | QPixmap Resources::get_vector_pixmap(ResourceBuilder::ResId id, int width, const QColor &color, qreal strength) { 43 | auto data = ResourceBuilder::get_resource_data(id); 44 | auto size = ResourceBuilder::get_resource_size(id); 45 | 46 | QByteArray svgData = QByteArray::fromRawData((const char *) data, size); 47 | QSvgRenderer qSvgRenderer(svgData); 48 | auto sz = QSizeF(qSvgRenderer.defaultSize()); 49 | 50 | auto scale = double(width) / sz.width(); 51 | auto height = (int) std::round(sz.height() * scale); 52 | 53 | QImage img(width, height, QImage::Format_RGBA8888); 54 | 55 | img.fill(Qt::transparent); 56 | QPainter painter_pix(&img); 57 | qSvgRenderer.render(&painter_pix); 58 | 59 | return QPixmap::fromImage(tint(img, color, strength)); 60 | } 61 | -------------------------------------------------------------------------------- /app-gui/src/statusview.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | #include "utils.h" 5 | #include "statusview.h" 6 | 7 | void StatusView::setSpace(float scannedVisible, float scannedHidden, float available, float unknown) { 8 | scannedSpaceVisible = scannedVisible; 9 | scannedSpaceHidden = scannedHidden; 10 | availableSpace = available; 11 | unknownSpace = unknown; 12 | } 13 | 14 | void StatusView::setProgress(int progress) { 15 | scanProgress = progress; 16 | } 17 | 18 | void StatusView::setCustomPalette(const CustomPalette &palette) { 19 | customPalette = palette; 20 | } 21 | 22 | void StatusView::setMode(Mode mode) { 23 | currentMode = mode; 24 | } 25 | 26 | void StatusView::setSpaceHighlight(bool available, bool unknown) { 27 | highlightAvailable = available; 28 | highlightUnknown = unknown; 29 | } 30 | 31 | void StatusView::setMaximizeSpace(bool maximize) { 32 | maximizeSpace = maximize; 33 | } 34 | 35 | void StatusView::setScannedFiles(int64_t files) { 36 | scannedFiles = files; 37 | } 38 | 39 | std::string StatusView::getScanStatusText() { 40 | if (currentMode == Mode::NO_SCAN || currentMode == Mode::SCAN_FINISHED) 41 | return "Ready"; 42 | // If we are scanning a directory, we don't actually know how much we need to scan 43 | // so don't display any numbers 44 | if (currentMode == Mode::SCANNING_INDEFINITE) 45 | return "Scanning..."; 46 | 47 | // progress can't be 100% because then it will be ready 48 | if (scanProgress > 99) 49 | scanProgress = 99; 50 | return Utils::strFormat("%d%%", scanProgress); 51 | } 52 | 53 | int StatusView::getStatusWidth(const QFontMetrics &fm) { 54 | return fm.size(0, "Ready").width(); 55 | } 56 | 57 | std::string StatusView::getScannedFilesText() { 58 | if (currentMode == Mode::NO_SCAN) 59 | return ""; 60 | 61 | return Utils::strFormat("%d files", scannedFiles); 62 | } 63 | 64 | void StatusView::paintEvent(QPaintEvent *event) { 65 | QPainter painter(this); 66 | int width = size().width() - 1; 67 | int height = size().height() - 1; 68 | 69 | int textMargin = 5; 70 | 71 | auto fm = painter.fontMetrics(); 72 | auto scanStatusStr = getScanStatusText(); 73 | auto scannedFilesStr = getScannedFilesText(); 74 | 75 | auto filesStrWidth = fm.size(0, scannedFilesStr.c_str()).width() + textMargin; 76 | auto statusStrWidth = getStatusWidth(fm); 77 | 78 | int availableWidth = width - statusStrWidth - textMargin - filesStrWidth; 79 | 80 | QRect textRect{0, 0, size().width(), size().height()}; 81 | 82 | painter.setPen(palette().windowText().color()); 83 | painter.drawText(textRect, Qt::AlignVCenter | Qt::AlignRight, scanStatusStr.c_str()); 84 | painter.drawText(textRect, Qt::AlignVCenter | Qt::AlignLeft, scannedFilesStr.c_str()); 85 | 86 | //don't render actual bars if scan is not opened 87 | if (currentMode == Mode::NO_SCAN) 88 | return; 89 | 90 | parts.clear(); 91 | 92 | StatusPart partScannedVisible, partScannedHidden, partFree, partUnknown; 93 | 94 | partScannedVisible.color = customPalette.getViewFileFill(); 95 | partScannedVisible.isHidden = false; 96 | partScannedVisible.label = Utils::formatSize((int64_t) scannedSpaceVisible); 97 | partScannedVisible.weight = scannedSpaceVisible; 98 | 99 | partScannedHidden.color = customPalette.getViewFileFill(); 100 | partScannedHidden.isHidden = true; 101 | partScannedHidden.label = Utils::formatSize((int64_t) scannedSpaceHidden); 102 | partScannedHidden.weight = scannedSpaceHidden; 103 | 104 | partFree.color = customPalette.getViewAvailableFill(); 105 | partFree.isHidden = !highlightAvailable; 106 | partFree.label = Utils::formatSize((int64_t) availableSpace); 107 | partFree.weight = maximizeSpace ? -1.f : availableSpace; 108 | 109 | partUnknown.color = customPalette.getViewUnknownFill(); 110 | partUnknown.isHidden = !highlightUnknown; 111 | partUnknown.label = Utils::formatSize((int64_t) unknownSpace); 112 | partUnknown.weight = maximizeSpace ? -1.f : unknownSpace; 113 | 114 | parts.push_back(partScannedVisible); 115 | parts.push_back(partScannedHidden); 116 | parts.push_back(partFree); 117 | parts.push_back(partUnknown); 118 | 119 | allocateParts(fm, float(availableWidth)); 120 | 121 | 122 | int start = filesStrWidth; 123 | int end = start + availableWidth; 124 | auto start1 = float(start); 125 | int end1 = end; 126 | int endAllVisible = end1; 127 | 128 | for (auto &part : parts) { 129 | float barWidth = float(availableWidth) * part.weight; 130 | end1 = int(start1 + barWidth); 131 | if (barWidth > 0) { 132 | if (part.isHidden) { 133 | //the same as just setting alpha, but we need actual final color to 134 | //correctly determine appropriate color for text 135 | part.color = customPalette.bgBlend(part.color, 0.5); 136 | } 137 | QRectF rect(start1, 0, barWidth, height); 138 | QRectF rectTxt(start1, 0, barWidth, size().height()); 139 | 140 | painter.fillRect(rect, part.color); 141 | 142 | painter.setPen(CustomPalette::getTextColorFor(part.color)); 143 | painter.drawText(rectTxt, Qt::AlignCenter, part.label.c_str()); 144 | 145 | if (!part.isHidden) 146 | endAllVisible = end1; 147 | } 148 | start1 += barWidth; 149 | } 150 | 151 | painter.setPen(palette().windowText().color()); 152 | painter.setBrush(Qt::transparent); 153 | if (endAllVisible != end1) 154 | painter.drawRect(filesStrWidth, 0, endAllVisible - filesStrWidth, height - 1); 155 | } 156 | 157 | void StatusView::allocateParts(const QFontMetrics &fm, float width) { 158 | // holds width of each label in pixels so we allocate enough space for it 159 | std::vector partLabelSizes; 160 | // minimum weight of each part so each label is shown 161 | std::vector minWeights; 162 | // holds weight of each part that is available (can be removed from part) 163 | // it will be negative if default weight is less than minimum weight 164 | std::vector availWeights; 165 | 166 | float totalWeight = 0.f; 167 | // calculate total weight of all parts, remove parts that should not be shown 168 | for (auto it = parts.begin(); it != parts.end();) { 169 | //remove empty parts, we don't need them 170 | if (it->weight == 0.f) { 171 | it = parts.erase(it); 172 | continue; 173 | } else if (it->weight < 0.f) 174 | it->weight = 0.f; //this part should be minimized, but kept in place 175 | totalWeight += it->weight; 176 | 177 | ++it; 178 | } 179 | 180 | minWeights.resize(parts.size()); 181 | availWeights.resize(parts.size()); 182 | partLabelSizes.resize(parts.size()); 183 | 184 | float totalAvailable = 0.f; 185 | float overdraw = -1.f; 186 | for (size_t i = 0; i < parts.size(); ++i) { 187 | partLabelSizes[i] = fm.size(0, parts[i].label.c_str()).width(); 188 | partLabelSizes[i] += parts[i].textMargin * 2; 189 | 190 | parts[i].weight /= totalWeight; 191 | minWeights[i] = partLabelSizes[i] / width; 192 | availWeights[i] = parts[i].weight - minWeights[i]; 193 | 194 | if (parts[i].weight >= 0.f) { 195 | if (availWeights[i] < 0.f) 196 | parts[i].weight -= availWeights[i]; 197 | else 198 | totalAvailable += availWeights[i]; 199 | overdraw += parts[i].weight; 200 | } 201 | } 202 | 203 | float a = overdraw / totalAvailable; 204 | 205 | for (size_t i = 0; i < parts.size(); ++i) 206 | if (availWeights[i] > 0.f) 207 | parts[i].weight -= availWeights[i] * a; 208 | 209 | //sort parts in such a way that all shown are on left and all hidden are on right 210 | std::sort(parts.begin(), parts.end(), [](const StatusPart &lhs, const StatusPart &rhs) { 211 | return !lhs.isHidden && rhs.isHidden; 212 | }); 213 | } 214 | 215 | QSize StatusView::minimumSizeHint() const { 216 | return {0, fontMetrics().height() + textPadding * 2}; 217 | } 218 | -------------------------------------------------------------------------------- /app-gui/src/utils-gui.cpp: -------------------------------------------------------------------------------- 1 | #include "utils-gui.h" 2 | 3 | #include 4 | 5 | 6 | std::string UtilsGui::select_folder(const std::string &title) { 7 | // simple wrapper so we need less memory to compile 8 | return pfd::select_folder(title).result(); 9 | } 10 | 11 | void UtilsGui::message_box(const std::string &title, const std::string &text) { 12 | pfd::message(title, text, pfd::choice::ok).result(); 13 | } 14 | 15 | QColor UtilsGui::blend(const QColor &foreground, const QColor &background, qreal bg_amount) { 16 | return QColor::fromRgbF( 17 | foreground.redF() * (1.0 - bg_amount) + background.redF() * bg_amount, 18 | foreground.greenF() * (1.0 - bg_amount) + background.greenF() * bg_amount, 19 | foreground.blueF() * (1.0 - bg_amount) + background.blueF() * bg_amount); 20 | } 21 | 22 | bool UtilsGui::isDark(const QColor &bg) { 23 | return bg.lightnessF() < 0.5; 24 | } 25 | -------------------------------------------------------------------------------- /build-lnx.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | mkdir -p cmake-build-release 4 | cd cmake-build-release || exit 5 | cmake -DCMAKE_BUILD_TYPE=Release -G "CodeBlocks - Unix Makefiles" .. 6 | cmake --build . --target spacedisplay_gui 7 | cd .. 8 | mkdir -p bin 9 | cp cmake-build-release/app-gui/spacedisplay_gui bin/spacedisplay_gui 10 | -------------------------------------------------------------------------------- /build-win-mingw.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | mkdir -p cmake-build-release 4 | cd cmake-build-release || exit 5 | cmake -DCMAKE_BUILD_TYPE=Release -G "CodeBlocks - MinGW Makefiles" -DCMAKE_SH="CMAKE_SH-NOTFOUND" .. 6 | cmake --build . --target spacedisplay_gui 7 | cd .. 8 | mkdir -p bin 9 | cp cmake-build-release/app-gui/spacedisplay_gui.exe bin/spacedisplay_gui.exe 10 | -------------------------------------------------------------------------------- /build-win-msvc14_x64.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | if not exist "cmake_win_path.txt" ( 3 | echo Rename cmake_win_path.example.txt to cmake_win_path.txt and provide path to cmake.exe 4 | exit 5 | ) 6 | 7 | set /p cmake_path= -------------------------------------------------------------------------------- /cmake_win_path.example.txt: -------------------------------------------------------------------------------- 1 | C:\cmake\cmake-3.8.2-win32-x86\bin\cmake.exe 2 | Change this path to path to cmake executable on your PC (only first line is read) 3 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | range: "50...80" 3 | status: 4 | project: 5 | default: 6 | threshold: 2% 7 | patch: 8 | default: 9 | target: 80% 10 | -------------------------------------------------------------------------------- /deps/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | if (${BUILD_TESTS}) 2 | add_subdirectory(catch2) 3 | endif () 4 | 5 | add_subdirectory(pfd) 6 | 7 | # create library for crc which doesn't have cmake file 8 | add_library(crc crc/crc_16.c) 9 | target_include_directories(crc PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/crc) 10 | add_library(CRC::CRC ALIAS crc) 11 | -------------------------------------------------------------------------------- /images/linux-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funbiscuit/spacedisplay/963e1ce22abfec28af2529f25ebaeda0e82e3701/images/linux-dark.png -------------------------------------------------------------------------------- /images/win-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funbiscuit/spacedisplay/963e1ce22abfec28af2529f25ebaeda0e82e3701/images/win-light.png -------------------------------------------------------------------------------- /lib/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | 2 | find_package(Threads REQUIRED) 3 | 4 | add_library(spacedisplay_lib STATIC 5 | ${CMAKE_CURRENT_SOURCE_DIR}/src/fileentry.cpp 6 | ${CMAKE_CURRENT_SOURCE_DIR}/src/filedb.cpp 7 | ${CMAKE_CURRENT_SOURCE_DIR}/src/filepath.cpp 8 | ${CMAKE_CURRENT_SOURCE_DIR}/src/spacescanner.cpp 9 | ${CMAKE_CURRENT_SOURCE_DIR}/src/spacewatcher.cpp 10 | ${CMAKE_CURRENT_SOURCE_DIR}/src/utils.cpp 11 | ${CMAKE_CURRENT_SOURCE_DIR}/src/logger.cpp 12 | ) 13 | 14 | 15 | target_compile_features(spacedisplay_lib PUBLIC cxx_std_11) 16 | set_target_properties(spacedisplay_lib PROPERTIES 17 | CXX_EXTENSIONS OFF) 18 | 19 | 20 | target_include_directories(spacedisplay_lib 21 | PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include 22 | PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/private 23 | ) 24 | 25 | target_link_libraries(spacedisplay_lib PRIVATE 26 | Threads::Threads CRC::CRC) 27 | 28 | if (WIN32) 29 | target_sources(spacedisplay_lib PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/private/WinFileIterator.cpp) 30 | target_sources(spacedisplay_lib PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/private/WinFileManager.cpp) 31 | target_sources(spacedisplay_lib PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/private/WinPlatformUtils.cpp) 32 | target_sources(spacedisplay_lib PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/private/WinSpaceWatcher.cpp) 33 | else () 34 | target_sources(spacedisplay_lib PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/private/LinuxFileIterator.cpp) 35 | target_sources(spacedisplay_lib PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/private/LinuxFileManager.cpp) 36 | target_sources(spacedisplay_lib PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/private/LinuxPlatformUtils.cpp) 37 | target_sources(spacedisplay_lib PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/private/LinuxSpaceWatcher.cpp) 38 | endif () 39 | -------------------------------------------------------------------------------- /lib/include/FileIterator.h: -------------------------------------------------------------------------------- 1 | #ifndef SPACEDISPLAY_FILEITERATOR_H 2 | #define SPACEDISPLAY_FILEITERATOR_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | class FileIterator { 9 | protected: 10 | FileIterator() = default; 11 | 12 | public: 13 | virtual ~FileIterator() = default; 14 | 15 | /** 16 | * Create platform specific iterator at provided path 17 | * @param path 18 | * @return pointer to created iterator 19 | */ 20 | static std::unique_ptr create(const std::string &path); 21 | 22 | // can't copy 23 | FileIterator(const FileIterator &) = delete; 24 | 25 | FileIterator &operator=(const FileIterator &) = delete; 26 | 27 | /** 28 | * Moves iterator to next file/dir 29 | * @throws std:out_of_range if iterator was not valid 30 | */ 31 | virtual void operator++() = 0; 32 | 33 | /** 34 | * Check if iterator is valid, otherwise its members are not valid 35 | */ 36 | virtual bool isValid() const = 0; 37 | 38 | /** 39 | * @return name of current directory/file or empty if not valid 40 | * @throws std:out_of_range if iterator was not valid 41 | */ 42 | virtual const std::string &getName() const = 0; 43 | 44 | /** 45 | * @return true if currently pointing to directory, false otherwise 46 | * @throws std:out_of_range if iterator was not valid 47 | */ 48 | virtual bool isDir() const = 0; 49 | 50 | /** 51 | * @return size of current file, 0 otherwise 52 | * @throws std:out_of_range if iterator was not valid 53 | */ 54 | virtual int64_t getSize() const = 0; 55 | 56 | /** 57 | * Assert that this iterator is valid, otherwise throw an error 58 | * @throws std::out_of_range if iterator not valid 59 | */ 60 | void assertValid() const { 61 | if (!isValid()) 62 | throw std::out_of_range("Iterator not valid"); 63 | } 64 | }; 65 | 66 | #endif //SPACEDISPLAY_FILEITERATOR_H 67 | -------------------------------------------------------------------------------- /lib/include/FileManager.h: -------------------------------------------------------------------------------- 1 | #ifndef SPACEDISPLAY_FILEMANAGER_H 2 | #define SPACEDISPLAY_FILEMANAGER_H 3 | 4 | #include 5 | 6 | namespace FileManager { 7 | /** 8 | * Opens folder in default file manager. 9 | * All slashes are converted to platform specific slashes 10 | * On Windows uses explorer 11 | * On Linux uses xdg-open to run default file manager 12 | * @param path - path to folder 13 | */ 14 | void openFolder(const std::string &path); 15 | 16 | /** 17 | * Selects file in default file manager. 18 | * All slashes are converted to platform specific slashes 19 | * If there is slash at the end, it will be removed 20 | * On Windows uses explorer 21 | * On Linux tries to detect default file manager first. 22 | * Can detect Dolphin, Nautilus and Nemo and uses appropriate command to select file. 23 | * Thunar doesn't seem to support selecting files. 24 | * If file manager was not detected, uses xdg-open to open parent directory (file itself is not selected) 25 | * @param path - path to file 26 | */ 27 | void showFile(const std::string &path); 28 | } 29 | 30 | #endif //SPACEDISPLAY_FILEMANAGER_H 31 | -------------------------------------------------------------------------------- /lib/include/PriorityCache.h: -------------------------------------------------------------------------------- 1 | #ifndef SPACEDISPLAY_PRIORITY_CACHE_H 2 | #define SPACEDISPLAY_PRIORITY_CACHE_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | /** 12 | * This cache allows to store values (of type V) by key (of type K) 13 | * time when key was accessed is treated as priority of this key 14 | * It allows to manually add values to cache or to provide supplier function 15 | * which will be evaluated whenever non cached value is requested 16 | * When trimming (reducing size of cache), keys with lowest priority (which 17 | * were accessed earlier then others) will be removed until size of cache is 18 | * reduced to desired value. 19 | * Trimming can be done manually or automatically (when max size is set) 20 | * @tparam K - type of key values, should be able to be a key for std::unordered_map 21 | * @tparam V - type of stored values, can be a smart pointer (both unique or shared) 22 | */ 23 | template 24 | class PriorityCache { 25 | public: 26 | explicit PriorityCache(std::function supplier = [](const K &) -> V { 27 | throw std::runtime_error("Key is missing and supplier not defined"); 28 | }); 29 | 30 | /** 31 | * Get value for specific key 32 | * If value is not in cache, supplier function is called, result 33 | * put in cache and returned 34 | * @param key 35 | * @return 36 | */ 37 | const V &get(const K &key); 38 | 39 | /** 40 | * Manually put value in cache 41 | * @param key 42 | * @param value 43 | */ 44 | void put(const K &key, V value); 45 | 46 | /** 47 | * Change capacity of internal structures to hold N elements 48 | * @param N 49 | */ 50 | void reserve(size_t N); 51 | 52 | /** 53 | * Check if specific key is cached 54 | * @param key 55 | * @return 56 | */ 57 | bool isCached(const K &key); 58 | 59 | /** 60 | * Reduce number of elements stored to given amount 61 | * If N is less than current number of elements, does nothing 62 | * @param N 63 | */ 64 | void trim(size_t N); 65 | 66 | /** 67 | * Set maximum amount of values in cache. If 0, no values will be added 68 | * to cache without limit 69 | * Cache will be trimmed after new values are added so at each time there is no more 70 | * than N elements in cache. Old ones (which were accessed earlier) will be removed 71 | * @param maxSize 72 | */ 73 | void setMaxSize(size_t maxSize); 74 | 75 | /** 76 | * Removes all stored values from cache 77 | */ 78 | void invalidate(); 79 | 80 | private: 81 | 82 | class KeyAccess { 83 | public: 84 | K key; 85 | 86 | /** 87 | * Value of access counter when this key was accessed last time 88 | */ 89 | uint64_t lastAccess; 90 | 91 | explicit KeyAccess(K key, uint64_t lastAccess) : key(std::move(key)), lastAccess(lastAccess) {} 92 | 93 | friend bool operator<(const KeyAccess &a, const KeyAccess &b) { 94 | return a.lastAccess < b.lastAccess; 95 | } 96 | }; 97 | 98 | class Value { 99 | public: 100 | V value; 101 | KeyAccess *key; 102 | 103 | explicit Value(V value, KeyAccess *key) : value(std::move(value)), key(key) {} 104 | }; 105 | 106 | std::unordered_map values; 107 | std::vector> keys; 108 | size_t maxSize = SIZE_MAX; 109 | uint64_t accessCounter = 0; 110 | 111 | std::function supplier; 112 | 113 | void addNewValue(const K &key, V value); 114 | }; 115 | 116 | template 117 | PriorityCache::PriorityCache(std::function supplier) : 118 | supplier(std::move(supplier)) { 119 | } 120 | 121 | template 122 | const V &PriorityCache::get(const K &key) { 123 | auto it = values.find(key); 124 | if (it == values.end()) { 125 | addNewValue(key, std::move(supplier(key))); 126 | it = values.find(key); 127 | } else { 128 | it->second.key->lastAccess = accessCounter; 129 | ++accessCounter; 130 | } 131 | 132 | return it->second.value; 133 | } 134 | 135 | template 136 | void PriorityCache::put(const K &key, V value) { 137 | auto it = values.find(key); 138 | if (it == values.end()) { 139 | addNewValue(key, std::move(value)); 140 | } else { 141 | it->second.key->lastAccess = accessCounter; 142 | ++accessCounter; 143 | it->second.value = std::move(value); 144 | } 145 | } 146 | 147 | template 148 | void PriorityCache::reserve(size_t N) { 149 | values.reserve(N); 150 | keys.reserve(N); 151 | } 152 | 153 | template 154 | bool PriorityCache::isCached(const K &key) { 155 | return values.find(key) != values.end(); 156 | } 157 | 158 | template 159 | void PriorityCache::trim(size_t N) { 160 | if (keys.size() <= N) 161 | return; 162 | 163 | std::sort(keys.begin(), keys.end(), [](const auto &a, const auto &b) { return *b < *a; }); 164 | 165 | for (size_t i = N; i < keys.size(); ++i) { 166 | values.erase(keys[i]->key); 167 | } 168 | keys.resize(N); 169 | } 170 | 171 | template 172 | void PriorityCache::setMaxSize(size_t maxSize_) { 173 | if (keys.size() > maxSize_) { 174 | trim(maxSize_); 175 | } 176 | maxSize = maxSize_; 177 | } 178 | 179 | template 180 | void PriorityCache::invalidate() { 181 | values.clear(); 182 | keys.clear(); 183 | } 184 | 185 | template 186 | void PriorityCache::addNewValue(const K &key, V value) { 187 | auto *keyPtr = new KeyAccess(key, accessCounter); 188 | ++accessCounter; 189 | auto keyAccess = std::unique_ptr(keyPtr); 190 | keys.emplace_back(std::move(keyAccess)); 191 | values.insert({key, std::move(Value(std::move(value), keyPtr))}); 192 | 193 | if (maxSize > 0 && keys.size() > maxSize) { 194 | trim(maxSize); 195 | } 196 | } 197 | 198 | #endif //SPACEDISPLAY_PRIORITY_CACHE_H 199 | -------------------------------------------------------------------------------- /lib/include/filedb.h: -------------------------------------------------------------------------------- 1 | #ifndef SPACEDISPLAY_FILEDB_H 2 | #define SPACEDISPLAY_FILEDB_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | class FileEntry; 12 | 13 | class FilePath; 14 | 15 | /** 16 | * Implements tree structure for files/directories 17 | * Also can store information about total and available space 18 | */ 19 | class FileDB { 20 | public: 21 | explicit FileDB(const std::string &path); 22 | 23 | /** 24 | * Sets space of mount point where files in this db are stored 25 | * @param totalSpace 26 | * @param availableSpace 27 | */ 28 | void setSpace(int64_t totalSpace, int64_t availableSpace); 29 | 30 | /** 31 | * Tries to find entry by specified path and updates its children to be the same as in 32 | * provided path. All existing children that are not listed in this vector, will be deleted. 33 | * If path doesn't exist - entries are destroyed and empty vector is returned 34 | * Paths to newly added directories are added to provided array (if not nullptr). 35 | * @param path 36 | * @param entries 37 | * @return 38 | */ 39 | bool setChildrenForPath(const FilePath &path, 40 | std::vector> entries, 41 | std::vector> *newPaths = nullptr); 42 | 43 | /** 44 | * Returns path to current root or null if db is not initialized 45 | * @return 46 | */ 47 | const FilePath &getRootPath() const; 48 | 49 | /** 50 | * Tries to find file/dir entry by given FilePath 51 | * @param path - path to entry that should be found 52 | * @return pointer to entry if it was found, nullptr otherwise 53 | */ 54 | const FileEntry *findEntry(const FilePath &path) const; 55 | 56 | /** 57 | * Let's you access entry at arbitrary path 58 | * Data is safe to access only inside callback func, do not save it 59 | * Database is locked during processing so don't spend much time 60 | * By processing root db assumes, you read all changes so hasChanges is set to false 61 | * If db is not initialized, func will not be called and false is returned 62 | * @param func 63 | * @return false if db not initialized 64 | */ 65 | bool processEntry(const FilePath &path, const std::function &func) const; 66 | 67 | bool hasChanges() const; 68 | 69 | void getSpace(int64_t &used, int64_t &available, int64_t &total) const; 70 | 71 | int64_t getFileCount() const; 72 | 73 | int64_t getDirCount() const; 74 | 75 | private: 76 | 77 | int64_t totalSpace = 0; 78 | int64_t availableSpace = 0; 79 | 80 | mutable std::mutex dbMtx; 81 | 82 | std::atomic usedSpace; 83 | std::atomic fileCount; 84 | std::atomic dirCount; 85 | 86 | //TODO make it possible to access "has changes" info by some caller id 87 | mutable std::atomic bHasChanges; 88 | 89 | std::unique_ptr rootFile; 90 | std::unique_ptr rootPath; 91 | 92 | //map key is crc of entry path, map value is vector of all children with the same crc of their name 93 | std::unordered_map> entriesMap; 94 | 95 | FileEntry *_findEntry(const FilePath &path) const; 96 | 97 | FileEntry *_findEntry(const char *entryName, uint16_t nameCrc, FileEntry *parent) const; 98 | 99 | /** 100 | * Deletes all items from entriesMap for this entry and all children (recursively) 101 | * Modifies global fileCount and dirCount by number of removed files and dirs 102 | * @param entry 103 | */ 104 | void _cleanupEntryCrc(const FileEntry &entry); 105 | 106 | }; 107 | 108 | 109 | #endif //SPACEDISPLAY_FILEDB_H 110 | -------------------------------------------------------------------------------- /lib/include/fileentry.h: -------------------------------------------------------------------------------- 1 | 2 | #ifndef SPACEDISPLAY_FILEENTRY_H 3 | #define SPACEDISPLAY_FILEENTRY_H 4 | 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | class FileEntry { 14 | public: 15 | /** 16 | * Constructs new FileEntry object. 17 | * Name must be non empty 18 | * @param name 19 | * @param isDir 20 | * @param size 21 | * @throws std::invalid_argument if name is empty 22 | */ 23 | FileEntry(const std::string &name, bool isDir, int64_t size = 0); 24 | 25 | /** 26 | * Sets new size for this entry. 27 | * If entry has a parent, this will be a slow operation 28 | * since parent will need to reorder this entry 29 | * @param newSize 30 | */ 31 | void setSize(int64_t newSize); 32 | 33 | int64_t getSize() const; 34 | 35 | const char *getName() const; 36 | 37 | const FileEntry *getParent() const; 38 | 39 | uint16_t getNameCrc() const; 40 | 41 | uint16_t getPathCrc() const; 42 | 43 | /** 44 | * Executes provided function for each child until all children are processed 45 | * If function returns false, processing is stopped 46 | * @param func 47 | * @return false if entry doesn't have children, true otherwise 48 | */ 49 | bool forEach(const std::function &func) const; 50 | 51 | /** 52 | * Adds child to children of this entry. 53 | * Adds to relevant place so all children are sorted by size (in decreasing order) 54 | * @param child 55 | */ 56 | void addChild(std::unique_ptr child); 57 | 58 | /** 59 | * Remove all children, that are marked for deletion 60 | * All removed children are put into provided vector 61 | */ 62 | void removePendingDelete(std::vector> &deletedChildren); 63 | 64 | /** 65 | * Mark all children for deletion. 66 | * All marked children will be deleted with call to removePendingDelete() 67 | * @param files - how much files are marked 68 | * @param dirs - how much directories are marked 69 | */ 70 | void markChildrenPendingDelete(int &files, int &dirs); 71 | 72 | /** 73 | * Unmark this entry so it is not deleted with removePendingDelete() 74 | */ 75 | void unmarkPendingDelete(); 76 | 77 | bool isDir() const; 78 | 79 | bool isRoot() const; 80 | 81 | private: 82 | 83 | void _addChild(std::unique_ptr child); 84 | 85 | void onChildSizeChanged(FileEntry *child, int64_t sizeChange); 86 | 87 | /** 88 | * Recalculates crc of path to this entry given path crc of entry parent 89 | * @param parentPathCrc 90 | */ 91 | void updatePathCrc(uint16_t parentPathCrc); 92 | 93 | struct EntryBin { 94 | int64_t size; 95 | // entries are in chain (all entries in chain have the same size) 96 | // to access next one use firstEntry->next 97 | std::unique_ptr firstEntry; 98 | 99 | explicit EntryBin(int64_t size, std::unique_ptr p = nullptr) : 100 | size(size), firstEntry(std::move(p)) {} 101 | 102 | bool operator<(const EntryBin &right) const { 103 | // use ordering in decreasing order 104 | return size > right.size; 105 | } 106 | }; 107 | 108 | FileEntry *parent; 109 | 110 | //used only inside EntryBin to point to the next entry with the same size 111 | std::unique_ptr nextEntry; 112 | std::set children; 113 | 114 | //used to mark entry to delete in function removePendingDelete 115 | bool pendingDelete; 116 | 117 | bool bIsDir; 118 | uint16_t nameCrc; 119 | 120 | // path crc is xor of all names in path (without trailing slashes, except root) 121 | uint16_t pathCrc; 122 | int64_t size; 123 | //not using std::string to reduce memory consumption (there are might be millions of entries so each byte counts) 124 | std::unique_ptr name; 125 | }; 126 | 127 | 128 | #endif //SPACEDISPLAY_FILEENTRY_H 129 | -------------------------------------------------------------------------------- /lib/include/filepath.h: -------------------------------------------------------------------------------- 1 | #ifndef SPACEDISPLAY_FILEPATH_H 2 | #define SPACEDISPLAY_FILEPATH_H 3 | 4 | #include 5 | #include 6 | 7 | class FilePath { 8 | public: 9 | enum class CompareResult { 10 | PARENT, 11 | CHILD, 12 | DIFFERENT, 13 | EQUAL 14 | }; 15 | enum class SlashHandling { 16 | REMOVE, // remove slash if present 17 | ADD, // add slash if not present 18 | LEAVE // do nothing 19 | }; 20 | 21 | /** 22 | * Constructs a path object given a root string 23 | * Path can be constructed only from root directory (slash at the end is optional). 24 | * Then other directories can be appended to it. 25 | * At the end file can be appended (then nothing else could be appended, until file is popped) 26 | * String will be normalized (all slashes replaced with platform specific slashes) 27 | * @param root - root for constructed path (e.g. `D:\`, `/` or `/home/user`) 28 | * @param crc - crc of given string 29 | * @throws std::invalid_argument if construction failed 30 | */ 31 | explicit FilePath(const std::string &root, uint16_t crc = 0); 32 | 33 | /** 34 | * Constructs new path by given full path and root 35 | * Root should be prefix of path 36 | * Path and root should be non empty 37 | * If path ends with slash, it is considered directory, otherwise it is a file. 38 | * Root may end with slash, but it is not required. 39 | * @param path - full path (e.g. /home/usr/test) 40 | * @param root - root for path (e.g. /home/) 41 | * @throws std::invalid_argument if construction failed 42 | */ 43 | explicit FilePath(const std::string &path, const std::string &root); 44 | 45 | /** 46 | * Returns correct path to this file or directory 47 | * Uses '/' on linux and '\' on windows 48 | * @param addDirSlash if true and this path is path to dir, than a closing slash will be added 49 | * @return 50 | */ 51 | std::string getPath(bool addDirSlash = true) const; 52 | 53 | std::string getRoot() const; 54 | 55 | const std::vector &getParts() const; 56 | 57 | /** 58 | * Return crc16 this path 59 | * Crc is calculated for each part of this path and then xor'ed together 60 | * @return 61 | */ 62 | uint16_t getPathCrc() const; 63 | 64 | /** 65 | * Returns name of file or directory this path is pointing to. 66 | * If already at root, the whole root would be returned. 67 | * Root returned "AS IS", otherwise slash at the end is removed. 68 | * @return 69 | */ 70 | std::string getName() const; 71 | 72 | /** 73 | * Adds directory to the end of the path. If path already ends with file, 74 | * throws an exception 75 | * @param name of the directory 76 | * @param crc - crc of given string 77 | * @throws std::invalid_argument if name is empty or path already ends 78 | * with file 79 | */ 80 | void addDir(const std::string &name, uint16_t crc = 0); 81 | 82 | /** 83 | * Adds file to the end of the path. If path already ends with file, exception is thrown. 84 | * After file is added, nothing else can be added. 85 | * @param name of the directory 86 | * @param crc - crc of given string 87 | * @throws std::invalid_argument if name is empty or path already ends 88 | * with file 89 | */ 90 | void addFile(const std::string &name, uint16_t crc = 0); 91 | 92 | /** 93 | * Checks if current path is pointing to directory or not 94 | * If path is invalid, returns false 95 | * @return true if directory, false otherwise 96 | */ 97 | bool isDir() const; 98 | 99 | /** 100 | * Checks if it is possible to navigate up 101 | * @return false if path is already at root, true otherwise 102 | */ 103 | bool canGoUp() const; 104 | 105 | /** 106 | * If it is possible to navigate up, will change path to its parent directory. 107 | * @return false if path is already at root, true otherwise 108 | */ 109 | bool goUp(); 110 | 111 | /** 112 | * Compares this path to specified path 113 | * @param path 114 | * @return 115 | */ 116 | CompareResult compareTo(const FilePath &path); 117 | 118 | /** 119 | * Converts this path to make it relative to provided path 120 | * Provided path should be parent of this path 121 | * If path can't be converted, false is returned 122 | * @param path 123 | * @return 124 | */ 125 | bool makeRelativeTo(const FilePath &parentPath); 126 | 127 | /** 128 | * Converts all slashes to platform correct ones. 129 | * @param path - path to normalize (will be edited in place) 130 | * @param slashHandling - what to do with slashes at the end: 131 | * REMOVE - all slashes at the end are removed (if any) 132 | * ADD - adds one slash at the end (if there were more then one - removes duplicates) 133 | * LEAVE - does nothing 134 | */ 135 | static void normalize(std::string &path, SlashHandling slashHandling = SlashHandling::LEAVE); 136 | private: 137 | 138 | /** 139 | * Contains parts of current path. Each part contains at most one slash - at the end. 140 | * Root part can have multiple slashes (e.g. /home/user/Desktop/) 141 | * Each part has slash at the end if it is a directory and doesn't have a slash if it is a file. 142 | * Only last part can be a file. 143 | * On Windows first part is X:\ where X is disk letter or some othe path (e.g. C:\Windows\System32\) 144 | * On Linux first part could be '/' or '/usr/lib/' or anything else of such format 145 | */ 146 | std::vector parts; 147 | 148 | std::vector pathCrcs; 149 | }; 150 | 151 | 152 | #endif //SPACEDISPLAY_FILEPATH_H 153 | -------------------------------------------------------------------------------- /lib/include/logger.h: -------------------------------------------------------------------------------- 1 | #ifndef SPACEDISPLAY_LOGGER_H 2 | #define SPACEDISPLAY_LOGGER_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | class Logger { 10 | 11 | public: 12 | 13 | Logger(); 14 | 15 | /** 16 | * Creates new log entry with tag (by default `LOG`) and adds it to history. 17 | * Sets hasNew property to true 18 | * @param msg 19 | * @param tag 20 | */ 21 | void log(const std::string &msg, const std::string &tag = "LOG"); 22 | 23 | /** 24 | * Returns all log entries in history and sets hasNew property to false 25 | * @return 26 | */ 27 | std::vector getHistory(); 28 | 29 | /** 30 | * Clears log history and sets hasNew property to false 31 | */ 32 | void clear(); 33 | 34 | /** 35 | * Return true if any entries were added since last call to getHistory 36 | * @return 37 | */ 38 | bool hasNew(); 39 | 40 | private: 41 | 42 | std::mutex logMtx; 43 | 44 | std::atomic hasNewLog; 45 | 46 | std::vector history; 47 | 48 | }; 49 | 50 | #endif //SPACEDISPLAY_LOGGER_H 51 | -------------------------------------------------------------------------------- /lib/include/platformutils.h: -------------------------------------------------------------------------------- 1 | #ifndef SPACEDISPLAY_PLATFORM_UTILS_H 2 | #define SPACEDISPLAY_PLATFORM_UTILS_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | /** 9 | * Collection of functions that a platform dependent 10 | */ 11 | namespace PlatformUtils { 12 | /** 13 | * Check if provided path exists and can be opened for scan 14 | * @param path to check 15 | * @return true on success, false otherwise 16 | */ 17 | bool can_scan_dir(const std::string &path); 18 | 19 | /** 20 | * Scan filesystem for available mount points (that can be scanned) 21 | * For example, on windows it will usually include "C:\" 22 | * On linux it will usually include "/" 23 | * @return vector containing paths of mount points that can be scanned 24 | */ 25 | std::vector getAvailableMounts(); 26 | 27 | /** 28 | * Scan filesystem for available excluded paths, 29 | * that should not be scanned (important for *nix since 30 | * we can't scan such paths as /proc or /sys) 31 | * On Windows it is empty 32 | * @return vector containing paths of excluded paths 33 | */ 34 | std::vector getExcludedPaths(); 35 | 36 | /** 37 | * Gets total and available space of filesystem mounted at specified path 38 | * @param path to mount point or any of its sub directories 39 | * @param totalSpace - total space in bytes that this filesystem has 40 | * @param availableSpace - available (free) space in bytes that this filesystem has 41 | * @return true if spaces are read successfully, false otherwise 42 | */ 43 | bool get_mount_space(const std::string &path, int64_t &totalSpace, int64_t &availableSpace); 44 | 45 | /** 46 | * Deletes directory and all files inside 47 | * Provided path should be global path, otherwise it is not thread-safe to call this. 48 | * Since working directory could change while function is executing. 49 | * @param path 50 | * @return true if was deleted, false otherwise 51 | */ 52 | bool deleteDir(const std::string &path); 53 | 54 | /** 55 | * filePathSeparator - file path separator that is native for the platform 56 | * invertedFilePathSeparator - file path separator that is not native for the platform 57 | */ 58 | #ifdef _WIN32 59 | const char filePathSeparator = '\\'; 60 | const char invertedFilePathSeparator = '/'; 61 | #else 62 | const char filePathSeparator = '/'; 63 | const char invertedFilePathSeparator = '\\'; 64 | #endif 65 | 66 | /** 67 | * Functions specific to certain platform 68 | */ 69 | #ifdef _WIN32 70 | 71 | /** 72 | * Convert utf8 encoded string to widechar encoded wstring 73 | * @param str - string in utf8 encoding 74 | * @return wstring in widechar encoding 75 | */ 76 | std::wstring str2wstr(std::string const &str); 77 | 78 | /** 79 | * Convert widechar wstring to utf8 encoded string 80 | * @param wstr - wstring in widechar encoding 81 | * @return string in utf8 encoding 82 | */ 83 | std::string wstr2str(std::wstring const &wstr); 84 | 85 | 86 | /** 87 | * Convert short path (utf-8 encoded) to long path (also utf-8). 88 | * Conversion is done "in place". If conversion failed, path is not changed 89 | * @param shortPath 90 | * @return true on success 91 | */ 92 | bool toLongPath(std::string &shortPath); 93 | 94 | #endif 95 | 96 | }; 97 | 98 | #endif //SPACEDISPLAY_PLATFORM_UTILS_H 99 | -------------------------------------------------------------------------------- /lib/include/spacescanner.h: -------------------------------------------------------------------------------- 1 | 2 | #ifndef SPACEDISPLAY_SPACESCANNER_H 3 | #define SPACEDISPLAY_SPACESCANNER_H 4 | 5 | 6 | #include 7 | #include 8 | #include // std::mutex 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | 16 | enum class ScannerStatus { 17 | IDLE, 18 | SCANNING, 19 | STOPPING, 20 | SCAN_PAUSED, 21 | }; 22 | 23 | class FileEntry; 24 | 25 | class FilePath; 26 | 27 | class FileDB; 28 | 29 | class SpaceWatcher; 30 | 31 | class Logger; 32 | 33 | class SpaceScanner { 34 | struct ScanRequest { 35 | std::unique_ptr path; 36 | bool recursive; 37 | }; 38 | public: 39 | 40 | /** 41 | * Starts scan of selected path 42 | * If path can't be scanned, throws an exception 43 | * @param path 44 | * @throws std::runtime_error if path can't be scanned 45 | */ 46 | explicit SpaceScanner(const std::string &path); 47 | 48 | ~SpaceScanner(); 49 | 50 | void stopScan(); 51 | 52 | /** 53 | * Pauses current scan and returns true if it was paused 54 | * If no scan is running returns false 55 | * @return 56 | */ 57 | bool pauseScan(); 58 | 59 | bool resumeScan(); 60 | 61 | bool canPause(); 62 | 63 | bool canResume(); 64 | 65 | void rescanPath(const FilePath &folder_path); 66 | 67 | const FileDB& getFileDB() const; 68 | 69 | std::unique_ptr getCurrentScanPath(); 70 | 71 | /** 72 | * Get current watcher limits 73 | * @param watchedNow - how many directories are already watched 74 | * @param watchLimit - limit on how many directories user can watch (-1 if no limit) 75 | * @return true if limits are exceeded, false otherwise 76 | */ 77 | bool getWatcherLimits(int64_t &watchedNow, int64_t &watchLimit); 78 | 79 | /** 80 | * Returns true if it possible to determine scan progress. 81 | * For example, if we scan partition and can get total occupied space. 82 | * @return 83 | */ 84 | bool isProgressKnown() const; 85 | 86 | int getScanProgress() const; 87 | 88 | bool hasChanges() const; 89 | 90 | const FilePath &getRootPath() const; 91 | 92 | void getSpace(int64_t &used, int64_t &available, int64_t &total) const; 93 | 94 | int64_t getFileCount() const; 95 | 96 | int64_t getDirCount() const; 97 | 98 | void setLogger(std::shared_ptr logger); 99 | 100 | private: 101 | std::thread workerThread; 102 | std::atomic runWorker; 103 | std::mutex scanMtx; 104 | std::unique_ptr currentScannedPath; 105 | 106 | // true if we scan mount point so we can get info about how big it should be 107 | bool isMountScanned; 108 | 109 | //edits to queue should be mutex protected 110 | std::list scanQueue; 111 | 112 | std::vector availableRoots; 113 | /** 114 | * Important for unix since we can't scan /proc /sys and some others 115 | */ 116 | std::vector excludedMounts; 117 | 118 | /** 119 | * If valid rootFile is available then this will update info about total and available space on this drive 120 | * Can be called multiple times, just need rootFile to be valid 121 | */ 122 | void updateDiskSpace(); 123 | 124 | std::atomic scannerStatus; 125 | std::unique_ptr db; 126 | 127 | std::unique_ptr watcher; 128 | std::atomic watcherLimitExceeded; 129 | 130 | std::shared_ptr logger; 131 | 132 | void worker_run(); 133 | 134 | void checkForEvents(); 135 | 136 | /** 137 | * Adds specified path to queue. If such path already exist in queue, 138 | * it will be moved to back (if toBack is true) or to front (otherwise). 139 | * All paths in queue that contain this path, will be removed from queue 140 | * This function must be called with locked scan mutex. 141 | * If there are any path in queue that is contained in this path, then this 142 | * path will not be added 143 | * @param path 144 | * @param toBack 145 | */ 146 | void addToQueue(std::unique_ptr path, bool recursiveScan, bool toBack = true); 147 | 148 | /** 149 | * Same as addToQueue but assumes that all paths in provided vector have common parent 150 | * @param paths 151 | * @param recursiveScan 152 | * @param toBack 153 | */ 154 | void addChildrenToQueue(std::vector> &paths, bool recursiveScan, bool toBack = true); 155 | 156 | /** 157 | * Performs a scan at given path, creates entry for each child and populates scannedEntries vector 158 | * Scan is not recursive, only direct children are scanned 159 | * @param path - FilePath where to perform scan 160 | * @param scannedEntries - vector reference where to add scanned children 161 | * @param newPaths - pointer to vector where to add paths to scanned dirs 162 | */ 163 | void scanChildrenAt(const FilePath &path, 164 | std::vector> &scannedEntries, 165 | std::vector> *newPaths = nullptr); 166 | }; 167 | 168 | 169 | #endif //SPACEDISPLAY_SPACESCANNER_H 170 | -------------------------------------------------------------------------------- /lib/include/spacewatcher.h: -------------------------------------------------------------------------------- 1 | #ifndef SPACEDISPLAY_SPACEWATCHER_H 2 | #define SPACEDISPLAY_SPACEWATCHER_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | 12 | class SpaceWatcher { 13 | public: 14 | enum class FileAction { 15 | ADDED = 1, 16 | REMOVED = -1, 17 | MODIFIED = 3, 18 | OLD_NAME = -6, 19 | NEW_NAME = 6 20 | }; 21 | 22 | enum class AddDirStatus { 23 | ADDED, 24 | DIR_LIMIT_REACHED, //used when unable to add inotify watch in linux 25 | ACCESS_DENIED, 26 | }; 27 | 28 | struct FileEvent { 29 | FileAction action; 30 | std::string filepath; 31 | std::string parentpath; 32 | }; 33 | 34 | /** 35 | * Create SpaceWatcher instance and start watching specific path 36 | * If platform doesn't support recursive watching, only this 37 | * directory will be watched. Add more with calls to addDir 38 | * @param path 39 | * @throws std::runtime_error if failed to start watching provided path 40 | * @return 41 | */ 42 | static std::unique_ptr create(const std::string &path); 43 | 44 | virtual ~SpaceWatcher(); 45 | 46 | /** 47 | * Get a limit for a number of watched directories. 48 | * Windows doesn't have such limit so -1 is returned. 49 | * Linux is limited by user limit of inotify watch descriptors. 50 | * @return limit of watched directories, -1 if there is no such limit. 51 | */ 52 | virtual int64_t getDirCountLimit() const = 0; 53 | 54 | /** 55 | * Get number of directories that are added to watch using addDir(); 56 | * If this watcher doesn't have a limit on watched dirs, this will always 57 | * return 1 58 | * @return 59 | */ 60 | int64_t getWatchedDirCount() const; 61 | 62 | /** 63 | * Returns event if any is present. 64 | * @return nullptr if there are no events 65 | */ 66 | std::unique_ptr popEvent(); 67 | 68 | /** 69 | * Adds directory to watch. Watch should be already started. 70 | * Used on platforms that can't watch recursively (e.g. linux) 71 | * @param path 72 | */ 73 | virtual AddDirStatus addDir(const std::string &path); 74 | 75 | /** 76 | * Adds directory to watch. Watch should be already started. 77 | * Used on platforms that can't watch recursively (e.g. linux) 78 | * @param path 79 | */ 80 | virtual void rmDir(const std::string &path); 81 | 82 | protected: 83 | SpaceWatcher(); 84 | 85 | virtual void readEvents() = 0; 86 | 87 | 88 | void addEvent(std::unique_ptr event); 89 | 90 | private: 91 | 92 | int64_t watchedDirCount; 93 | 94 | std::mutex eventsMtx; 95 | std::list> eventQueue; 96 | 97 | 98 | std::atomic runThread; 99 | std::thread watchThread; 100 | 101 | void watcherRun(); 102 | }; 103 | 104 | 105 | #endif //SPACEDISPLAY_SPACEWATCHER_H 106 | -------------------------------------------------------------------------------- /lib/include/utils.h: -------------------------------------------------------------------------------- 1 | 2 | #ifndef SPACEDISPLAY_UTILS_H 3 | #define SPACEDISPLAY_UTILS_H 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | namespace Utils { 11 | struct RectI { 12 | int x, y; 13 | int w, h; 14 | }; 15 | 16 | uint16_t strCrc16(const std::string &str); 17 | 18 | std::string formatSize(int64_t size); 19 | 20 | //todo we don't need anything from C++14, just make_unique, so use this simple version with C++11 21 | template 22 | std::unique_ptr make_unique(Args &&... args) { 23 | return std::unique_ptr(new T(std::forward(args)...)); 24 | } 25 | 26 | template 27 | std::unique_ptr make_unique_arr(size_t N) { 28 | return std::unique_ptr(new T[N]); 29 | } 30 | 31 | /** 32 | * Checks if given value is inside the array 33 | * @tparam T type of value 34 | * @param value 35 | * @param array 36 | * @return 37 | */ 38 | template 39 | bool in_array(const T &value, const std::vector &array) { 40 | return std::find(array.begin(), array.end(), value) != array.end(); 41 | } 42 | 43 | template 44 | T clip(const T &n, const T &lower, const T &upper) { 45 | return std::max(lower, std::min(n, upper)); 46 | } 47 | 48 | /** 49 | * Round fraction `num/denom` to the nearest int. 50 | * Equivalent to int(round(double(num)/double(denom))) 51 | * @tparam T 52 | * @param num 53 | * @param denom 54 | * @return round(num/denom) 55 | */ 56 | template 57 | T roundFrac(T num, T denom) { 58 | static_assert(std::is_integral::value, "Integral type is required"); 59 | return (num + denom / 2) / denom; 60 | } 61 | 62 | template 63 | std::string strFormat(const char *format, Args ... args) { 64 | size_t size = snprintf(nullptr, 0, format, args ...) + 1; // Extra space for '\0' 65 | std::unique_ptr buf(new char[size]); 66 | snprintf(buf.get(), size, format, args ...); 67 | return std::string(buf.get(), buf.get() + size - 1); // We don't want the '\0' inside 68 | } 69 | } 70 | 71 | #endif //SPACEDISPLAY_UTILS_H 72 | -------------------------------------------------------------------------------- /lib/private/LinuxFileIterator.cpp: -------------------------------------------------------------------------------- 1 | #include "platformutils.h" 2 | 3 | #include "LinuxFileIterator.h" 4 | 5 | #include 6 | 7 | LinuxFileIterator::LinuxFileIterator(std::string path) : 8 | valid(false), dir(false), size(0), path(std::move(path)) { 9 | dirp = opendir(this->path.c_str()); 10 | getNextFileData(); 11 | } 12 | 13 | LinuxFileIterator::~LinuxFileIterator() { 14 | if (dirp != nullptr) { 15 | closedir(dirp); 16 | } 17 | } 18 | 19 | std::unique_ptr FileIterator::create(const std::string &path) { 20 | return std::unique_ptr(new LinuxFileIterator(path)); 21 | } 22 | 23 | void LinuxFileIterator::operator++() { 24 | assertValid(); 25 | getNextFileData(); 26 | } 27 | 28 | bool LinuxFileIterator::isValid() const { 29 | return valid; 30 | } 31 | 32 | const std::string &LinuxFileIterator::getName() const { 33 | assertValid(); 34 | return name; 35 | } 36 | 37 | bool LinuxFileIterator::isDir() const { 38 | assertValid(); 39 | return dir; 40 | } 41 | 42 | int64_t LinuxFileIterator::getSize() const { 43 | assertValid(); 44 | return size; 45 | } 46 | 47 | void LinuxFileIterator::getNextFileData() { 48 | valid = false; 49 | if (dirp == nullptr) 50 | return; 51 | 52 | struct stat file_stat{}; 53 | struct dirent *dp; 54 | 55 | while ((dp = readdir(dirp)) != nullptr) { 56 | name = dp->d_name; 57 | if (name.empty() || name == "." || name == "..") 58 | continue; 59 | break; 60 | } 61 | 62 | if (dp == nullptr) 63 | return; 64 | 65 | // to get file info we need full path 66 | std::string child = path; 67 | child.push_back('/'); 68 | child.append(name); 69 | 70 | if (lstat(child.c_str(), &file_stat) == 0) { 71 | valid = true; 72 | dir = S_ISDIR(file_stat.st_mode); 73 | size = file_stat.st_size; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /lib/private/LinuxFileIterator.h: -------------------------------------------------------------------------------- 1 | #ifndef SPACEDISPLAY_LINUXFILEITERATOR_H 2 | #define SPACEDISPLAY_LINUXFILEITERATOR_H 3 | 4 | #include "FileIterator.h" 5 | 6 | #include 7 | 8 | class LinuxFileIterator : public FileIterator { 9 | private: 10 | explicit LinuxFileIterator(std::string path); 11 | 12 | public: 13 | ~LinuxFileIterator() override; 14 | 15 | void operator++() override; 16 | 17 | bool isValid() const override; 18 | 19 | const std::string &getName() const override; 20 | 21 | bool isDir() const override; 22 | 23 | int64_t getSize() const override; 24 | 25 | friend std::unique_ptr FileIterator::create(const std::string &path); 26 | 27 | private: 28 | bool valid; 29 | std::string name; 30 | bool dir; 31 | int64_t size; 32 | 33 | DIR *dirp; 34 | std::string path; 35 | 36 | /** 37 | * Gets file data for the next file returned by handle 38 | * If handle is new (just returned by opendir) then filedata for the first file will be read 39 | */ 40 | void getNextFileData(); 41 | }; 42 | 43 | #endif //SPACEDISPLAY_LINUXFILEITERATOR_H 44 | -------------------------------------------------------------------------------- /lib/private/LinuxFileManager.cpp: -------------------------------------------------------------------------------- 1 | #include "FileManager.h" 2 | #include "FileIterator.h" 3 | 4 | #include "utils.h" 5 | #include "filepath.h" 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include 13 | #include 14 | 15 | enum class FileManagerType { 16 | UNKNOWN = 0, 17 | DOLPHIN = 1, 18 | NAUTILUS = 2, 19 | NEMO = 3, 20 | }; 21 | 22 | /** 23 | * Parses desktop file at the specified path and 24 | * tries to detect which file manager it corresponds to 25 | * @param path 26 | * @return 27 | */ 28 | static FileManagerType parseFileManagerDesktopFile(const std::string &path); 29 | 30 | /** 31 | * Uses "xdg-mime query default inode/directory" to get default file manager 32 | * @return 33 | */ 34 | static FileManagerType getDefaultFileManager(); 35 | 36 | static void systemAsync(std::string cmd); 37 | 38 | static std::string exec(const char *cmd); 39 | 40 | void FileManager::openFolder(const std::string &path) { 41 | std::string folder_path = path; 42 | FilePath::normalize(folder_path, FilePath::SlashHandling::ADD); 43 | auto command = Utils::strFormat("xdg-open \"%s\"", folder_path.c_str()); 44 | systemAsync(command); 45 | } 46 | 47 | /** 48 | * on Linux we try to determine default file manager to use appropriate command 49 | * to select file. From most popular managers only thunar is not supported (it seems it doesn't have 50 | * command for selecting file) 51 | * 52 | * If file manager was not detected than we just open parent directory of this file (file itself is not selected) 53 | */ 54 | void FileManager::showFile(const std::string &path) { 55 | std::string file_path = path; 56 | FilePath::normalize(file_path, FilePath::SlashHandling::REMOVE); 57 | std::string command; 58 | switch (getDefaultFileManager()) { 59 | case FileManagerType::DOLPHIN: 60 | command = Utils::strFormat("dolphin --select \"%s\"", file_path.c_str()); 61 | break; 62 | case FileManagerType::NAUTILUS: 63 | command = Utils::strFormat("nautilus --select \"%s\"", file_path.c_str()); 64 | break; 65 | case FileManagerType::NEMO: 66 | command = Utils::strFormat("nemo \"%s\"", file_path.c_str()); 67 | break; 68 | case FileManagerType::UNKNOWN: 69 | default: 70 | std::string dir_path = file_path; 71 | auto last_slash = dir_path.find_last_of('/'); 72 | if (last_slash != std::string::npos) 73 | dir_path = dir_path.substr(0, last_slash); 74 | command = Utils::strFormat("xdg-open \"%s\"", dir_path.c_str()); 75 | break; 76 | } 77 | 78 | systemAsync(command); 79 | } 80 | 81 | FileManagerType parseFileManagerDesktopFile(const std::string &path) { 82 | std::ifstream infile(path); 83 | std::string line; 84 | while (std::getline(infile, line)) { 85 | if (line.rfind("Exec=", 0) != 0) 86 | continue; 87 | 88 | if (line.find("dolphin") != std::string::npos) { 89 | return FileManagerType::DOLPHIN; 90 | } else if (line.find("nautilus") != std::string::npos) { 91 | return FileManagerType::NAUTILUS; 92 | } else if (line.find("nemo") != std::string::npos) { 93 | return FileManagerType::NEMO; 94 | } 95 | } 96 | 97 | return FileManagerType::UNKNOWN; 98 | } 99 | 100 | FileManagerType getDefaultFileManager() { 101 | const char *homedir; 102 | 103 | if ((homedir = getenv("HOME")) == nullptr) { 104 | homedir = getpwuid(getuid())->pw_dir; 105 | } 106 | 107 | std::vector desktopLocations; 108 | desktopLocations.push_back(Utils::strFormat("%s/.local/share/applications", homedir)); 109 | desktopLocations.emplace_back("/usr/local/share/applications"); 110 | desktopLocations.emplace_back("/usr/share/applications"); 111 | 112 | auto fileManager = exec("xdg-mime query default inode/directory"); 113 | auto fileManagerEnd = fileManager.find_first_of('\n'); 114 | if (fileManagerEnd != std::string::npos) { 115 | fileManager = fileManager.substr(0, fileManagerEnd); 116 | } 117 | if (fileManager.empty()) { 118 | return FileManagerType::UNKNOWN; 119 | } 120 | std::cout << "Default file manager: " << fileManager << "\n"; 121 | 122 | for (auto &loc: desktopLocations) { 123 | for (auto it = FileIterator::create(loc); it->isValid(); ++(*it)) { 124 | if (it->getName() == fileManager) { 125 | auto path = Utils::strFormat("%s/%s", loc.c_str(), it->getName().c_str()); 126 | return parseFileManagerDesktopFile(path); 127 | } 128 | } 129 | } 130 | 131 | return FileManagerType::UNKNOWN; 132 | } 133 | 134 | void systemAsync(std::string cmd) { 135 | cmd = regex_replace(cmd, std::regex("[$]"), "\\$"); 136 | std::thread t1([cmd] { 137 | std::cout << "Executing: " << cmd << "\n"; 138 | system(cmd.c_str()); 139 | }); 140 | t1.detach(); 141 | } 142 | 143 | std::string exec(const char *cmd) { 144 | std::array buffer{}; 145 | std::string result; 146 | std::unique_ptr pipe(popen(cmd, "r"), pclose); 147 | if (!pipe) { 148 | throw std::runtime_error("popen() failed!"); 149 | } 150 | while (fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr) { 151 | result += buffer.data(); 152 | } 153 | return result; 154 | } 155 | -------------------------------------------------------------------------------- /lib/private/LinuxPlatformUtils.cpp: -------------------------------------------------------------------------------- 1 | #include "platformutils.h" 2 | #include "utils.h" 3 | #include "filepath.h" 4 | #include "FileIterator.h" 5 | 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | static void processMountPoints(const std::function &consumer); 14 | 15 | bool PlatformUtils::can_scan_dir(const std::string &path) { 16 | struct stat file_stat{}; 17 | if (lstat(path.c_str(), &file_stat) == 0) 18 | return S_ISDIR(file_stat.st_mode); 19 | else 20 | return false; 21 | } 22 | 23 | std::vector PlatformUtils::getAvailableMounts() { 24 | std::vector availableMounts; 25 | 26 | processMountPoints([&availableMounts](const std::string &mount, bool isExcluded) { 27 | if (!isExcluded) { 28 | availableMounts.push_back(mount); 29 | } 30 | }); 31 | 32 | return availableMounts; 33 | } 34 | 35 | std::vector PlatformUtils::getExcludedPaths() { 36 | std::vector excludedPaths; 37 | 38 | processMountPoints([&excludedPaths](const std::string &mount, bool isExcluded) { 39 | if (isExcluded) { 40 | excludedPaths.push_back(mount); 41 | } 42 | }); 43 | 44 | return excludedPaths; 45 | } 46 | 47 | bool PlatformUtils::get_mount_space(const std::string &path, int64_t &totalSpace, int64_t &availableSpace) { 48 | struct statvfs disk_stat{}; 49 | totalSpace = 0; 50 | availableSpace = 0; 51 | if (statvfs(path.c_str(), &disk_stat) == 0) { 52 | totalSpace = int64_t(disk_stat.f_frsize) * int64_t(disk_stat.f_blocks); 53 | availableSpace = int64_t(disk_stat.f_frsize) * int64_t(disk_stat.f_bavail); 54 | return true; 55 | } else 56 | return false; 57 | } 58 | 59 | bool PlatformUtils::deleteDir(const std::string &path) { 60 | bool deleted = true; 61 | try { 62 | FilePath fpath(path); 63 | 64 | for (auto it = FileIterator::create(path); it->isValid(); ++(*it)) { 65 | if (it->isDir()) { 66 | fpath.addDir(it->getName()); 67 | deleted &= deleteDir(fpath.getPath()); 68 | } else { 69 | fpath.addFile(it->getName()); 70 | deleted &= unlink(fpath.getPath().c_str()) == 0; 71 | } 72 | fpath.goUp(); 73 | } 74 | //after all children are removed, remove this directory 75 | if (deleted) 76 | deleted &= rmdir(fpath.getPath().c_str()) == 0; 77 | } catch (std::exception &) { 78 | return false; 79 | } 80 | 81 | return deleted; 82 | } 83 | 84 | void processMountPoints(const std::function &consumer) { 85 | //TODO add more partitions if supported 86 | const std::vector partitions = {"ext2", "ext3", "ext4", "vfat", "ntfs", "fuseblk"}; 87 | 88 | std::ifstream file("/proc/mounts"); 89 | std::string str; 90 | std::regex rgx(R"(^\S* (\S*) (\S*))"); 91 | while (std::getline(file, str)) { 92 | std::smatch matches; 93 | 94 | if (!std::regex_search(str, matches, rgx) || matches.size() != 3) { 95 | continue; 96 | } 97 | 98 | // access path and partition type 99 | std::string mount = matches[1]; 100 | std::string part = matches[2]; 101 | 102 | //include closing slash if it isn't there 103 | if (mount.back() != PlatformUtils::filePathSeparator) { 104 | mount.push_back(PlatformUtils::filePathSeparator); 105 | } 106 | consumer(mount, !Utils::in_array(part, partitions)); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /lib/private/LinuxSpaceWatcher.cpp: -------------------------------------------------------------------------------- 1 | #include "LinuxSpaceWatcher.h" 2 | #include "utils.h" 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | 10 | std::unique_ptr SpaceWatcher::create(const std::string &path) { 11 | auto *ptr = new LinuxSpaceWatcher(); 12 | if (!ptr->beginWatch(path)) { 13 | delete ptr; 14 | throw std::runtime_error("Can't start watching " + path); 15 | } 16 | return std::unique_ptr(ptr); 17 | } 18 | 19 | LinuxSpaceWatcher::LinuxSpaceWatcher() : inotifyFd(-1), watchBuffer(watchBufferSize, 0) { 20 | 21 | } 22 | 23 | LinuxSpaceWatcher::~LinuxSpaceWatcher() { 24 | if (inotifyFd != -1) { 25 | close(inotifyFd); 26 | std::lock_guard lock(inotifyWdsMtx); 27 | inotifyWds.clear(); 28 | } 29 | } 30 | 31 | bool LinuxSpaceWatcher::beginWatch(const std::string &path) { 32 | inotifyFd = inotify_init1(IN_NONBLOCK); 33 | if (inotifyFd != -1) 34 | return addDir(path) == SpaceWatcher::AddDirStatus::ADDED; 35 | return false; 36 | } 37 | 38 | int64_t LinuxSpaceWatcher::getDirCountLimit() const { 39 | int64_t limit = 0; 40 | 41 | std::ifstream file("/proc/sys/fs/inotify/max_user_watches"); 42 | file >> limit; 43 | 44 | return limit; 45 | } 46 | 47 | SpaceWatcher::AddDirStatus LinuxSpaceWatcher::addDir(const std::string &path) { 48 | int wd; 49 | 50 | //not using IN_DELETE_SELF and IN_MOVE_SELF since these events should be detected by parent directory 51 | const auto EVENTS = IN_MODIFY | IN_MOVE | IN_CREATE | IN_DELETE; 52 | 53 | wd = inotify_add_watch(inotifyFd, path.c_str(), EVENTS); 54 | //TODO we should detect when process runs out of available inotify watches 55 | // so client could notify user about it 56 | if (wd == -1) { 57 | switch (errno) { 58 | case ENOSPC: 59 | return AddDirStatus::DIR_LIMIT_REACHED; 60 | } 61 | return AddDirStatus::ACCESS_DENIED; //this is a default, although not very accurate 62 | } 63 | std::lock_guard lock(inotifyWdsMtx); 64 | auto it = inotifyWds.find(wd); 65 | if (it != inotifyWds.end()) { 66 | it->second = path; 67 | } else { 68 | inotifyWds[wd] = path; 69 | SpaceWatcher::addDir(path); 70 | } 71 | return AddDirStatus::ADDED; 72 | } 73 | 74 | void LinuxSpaceWatcher::rmDir(const std::string &path) { 75 | //TODO 76 | SpaceWatcher::rmDir(path); 77 | } 78 | 79 | void LinuxSpaceWatcher::processInotifyEvent(struct inotify_event *inotifyEvent) { 80 | if (inotifyEvent->mask & IN_IGNORED) { 81 | //watch was removed so remove it from our map 82 | std::lock_guard lock(inotifyWdsMtx); 83 | auto it = inotifyWds.find(inotifyEvent->wd); 84 | if (it != inotifyWds.end()) { 85 | SpaceWatcher::rmDir(it->second); 86 | inotifyWds.erase(it); 87 | } 88 | return; 89 | } 90 | 91 | if (inotifyEvent->len == 0) 92 | return; 93 | 94 | auto fileEvent = Utils::make_unique(); 95 | 96 | if (inotifyEvent->mask & IN_CREATE) 97 | fileEvent->action = FileAction::ADDED; 98 | else if (inotifyEvent->mask & IN_DELETE) 99 | fileEvent->action = FileAction::REMOVED; 100 | else if (inotifyEvent->mask & IN_MODIFY) 101 | fileEvent->action = FileAction::MODIFIED; 102 | else if (inotifyEvent->mask & IN_MOVED_FROM) 103 | fileEvent->action = FileAction::OLD_NAME; 104 | else if (inotifyEvent->mask & IN_MOVED_TO) 105 | fileEvent->action = FileAction::NEW_NAME; 106 | else 107 | return; 108 | 109 | { 110 | std::lock_guard lock(inotifyWdsMtx); 111 | 112 | auto it = inotifyWds.find(inotifyEvent->wd); 113 | if (it == inotifyWds.end()) 114 | return; //should not happen 115 | 116 | fileEvent->filepath = it->second + inotifyEvent->name; 117 | } 118 | 119 | if (fileEvent->filepath.back() == '/') 120 | fileEvent->filepath.pop_back(); 121 | 122 | auto lastSlash = fileEvent->filepath.find_last_of('/'); 123 | if (lastSlash != std::string::npos) 124 | fileEvent->parentpath = fileEvent->filepath.substr(0, lastSlash + 1); 125 | else 126 | fileEvent->parentpath = ""; 127 | 128 | addEvent(std::move(fileEvent)); 129 | } 130 | 131 | void LinuxSpaceWatcher::readEvents() { 132 | while (true) { 133 | auto numRead = read(inotifyFd, watchBuffer.data(), watchBufferSize); 134 | if (numRead <= 0) 135 | break; 136 | 137 | for (size_t i = 0; i < numRead;) { 138 | auto event = reinterpret_cast(&watchBuffer[i]); 139 | processInotifyEvent(event); 140 | 141 | i += sizeof(inotify_event) + event->len; 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /lib/private/LinuxSpaceWatcher.h: -------------------------------------------------------------------------------- 1 | #ifndef SPACEDISPLAY_LINUXSPACEWATCHER_H 2 | #define SPACEDISPLAY_LINUXSPACEWATCHER_H 3 | 4 | #include "spacewatcher.h" 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | struct inotify_event; 13 | 14 | class LinuxSpaceWatcher : public SpaceWatcher { 15 | public: 16 | ~LinuxSpaceWatcher() override; 17 | 18 | int64_t getDirCountLimit() const override; 19 | 20 | AddDirStatus addDir(const std::string &path) override; 21 | 22 | void rmDir(const std::string &path) override; 23 | 24 | friend std::unique_ptr SpaceWatcher::create(const std::string &path); 25 | protected: 26 | void readEvents() override; 27 | 28 | private: 29 | 30 | const int watchBufferSize = 1024 * 48; 31 | 32 | std::vector watchBuffer; 33 | 34 | int inotifyFd; 35 | std::unordered_map inotifyWds; 36 | std::mutex inotifyWdsMtx; 37 | 38 | LinuxSpaceWatcher(); 39 | 40 | bool beginWatch(const std::string &path); 41 | 42 | void processInotifyEvent(struct inotify_event *inotifyEvent); 43 | }; 44 | 45 | #endif //SPACEDISPLAY_LINUXSPACEWATCHER_H 46 | -------------------------------------------------------------------------------- /lib/private/WinFileIterator.cpp: -------------------------------------------------------------------------------- 1 | #include "platformutils.h" 2 | 3 | #include "WinFileIterator.h" 4 | 5 | #include 6 | 7 | WinFileIterator::WinFileIterator(const std::string &path) : 8 | valid(false), dir(false), size(0), dirHandle(INVALID_HANDLE_VALUE) { 9 | auto wname = PlatformUtils::str2wstr(path); 10 | //it's okay to have multiple slashes at the end 11 | wname.append(L"\\*"); 12 | 13 | WIN32_FIND_DATAW fileData; 14 | 15 | dirHandle = FindFirstFileW(wname.c_str(), &fileData); 16 | processFileData(true, &fileData); 17 | } 18 | 19 | WinFileIterator::~WinFileIterator() { 20 | if (dirHandle != INVALID_HANDLE_VALUE) { 21 | FindClose(dirHandle); 22 | } 23 | } 24 | 25 | std::unique_ptr FileIterator::create(const std::string &path) { 26 | return std::unique_ptr(new WinFileIterator(path)); 27 | } 28 | 29 | void WinFileIterator::operator++() { 30 | assertValid(); 31 | WIN32_FIND_DATAW fileData; 32 | 33 | processFileData(false, &fileData); 34 | } 35 | 36 | bool WinFileIterator::isValid() const { 37 | return valid; 38 | } 39 | 40 | const std::string &WinFileIterator::getName() const { 41 | assertValid(); 42 | return name; 43 | } 44 | 45 | bool WinFileIterator::isDir() const { 46 | assertValid(); 47 | return dir; 48 | } 49 | 50 | int64_t WinFileIterator::getSize() const { 51 | assertValid(); 52 | return size; 53 | } 54 | 55 | void WinFileIterator::processFileData(bool isFirst, WIN32_FIND_DATAW *fileData) { 56 | bool found = dirHandle != INVALID_HANDLE_VALUE; 57 | 58 | if (found && (isFirst || FindNextFileW(dirHandle, fileData) != 0)) { 59 | auto cname = PlatformUtils::wstr2str(fileData->cFileName); 60 | 61 | while (found && (cname.empty() || cname == "." || cname == "..")) { 62 | found = FindNextFileW(dirHandle, fileData) != 0; 63 | cname = PlatformUtils::wstr2str(fileData->cFileName); 64 | } 65 | 66 | if (found) { 67 | valid = true; 68 | name = std::move(cname); 69 | dir = (fileData->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0; 70 | //don't count mount points and symlinks as directories 71 | if ((fileData->dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0 && 72 | (fileData->dwReserved0 == IO_REPARSE_TAG_SYMLINK || 73 | fileData->dwReserved0 == IO_REPARSE_TAG_MOUNT_POINT)) { 74 | dir = false; 75 | } 76 | 77 | size = (int64_t(fileData->nFileSizeHigh) * (int64_t(MAXDWORD) + 1)) + 78 | int64_t(fileData->nFileSizeLow); 79 | } 80 | } else { 81 | valid = false; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /lib/private/WinFileIterator.h: -------------------------------------------------------------------------------- 1 | #ifndef SPACEDISPLAY_WINFILEITERATOR_H 2 | #define SPACEDISPLAY_WINFILEITERATOR_H 3 | 4 | #include "FileIterator.h" 5 | 6 | #include 7 | 8 | class WinFileIterator : public FileIterator { 9 | private: 10 | explicit WinFileIterator(const std::string &path); 11 | 12 | public: 13 | ~WinFileIterator() override; 14 | 15 | void operator++() override; 16 | 17 | bool isValid() const override; 18 | 19 | const std::string &getName() const override; 20 | 21 | bool isDir() const override; 22 | 23 | int64_t getSize() const override; 24 | 25 | friend std::unique_ptr FileIterator::create(const std::string &path); 26 | 27 | private: 28 | bool valid; 29 | std::string name; 30 | bool dir; 31 | int64_t size; 32 | 33 | HANDLE dirHandle; 34 | 35 | /** 36 | * Gets file data for the first or next file returned by handle 37 | */ 38 | void processFileData(bool isFirst, WIN32_FIND_DATAW *fileData); 39 | }; 40 | 41 | #endif //SPACEDISPLAY_WINFILEITERATOR_H 42 | -------------------------------------------------------------------------------- /lib/private/WinFileManager.cpp: -------------------------------------------------------------------------------- 1 | #include "FileManager.h" 2 | 3 | #include "utils.h" 4 | #include "filepath.h" 5 | #include "platformutils.h" 6 | 7 | #include 8 | #include 9 | 10 | void FileManager::openFolder(const std::string &path) { 11 | std::string folder_path = path; 12 | FilePath::normalize(folder_path, FilePath::SlashHandling::ADD); 13 | auto pars = Utils::strFormat("/n,\"%s\"", folder_path.c_str()); 14 | auto parsw = PlatformUtils::str2wstr(pars); 15 | 16 | std::wcout << L"Launch explorer with args: " << parsw << '\n'; 17 | 18 | ShellExecuteW(nullptr, L"open", L"explorer", parsw.c_str(), nullptr, SW_SHOWNORMAL); 19 | } 20 | 21 | void FileManager::showFile(const std::string &path) { 22 | std::string file_path = path; 23 | FilePath::normalize(file_path, FilePath::SlashHandling::REMOVE); 24 | auto pars = Utils::strFormat("/select,\"%s\"", file_path.c_str()); 25 | auto parsw = PlatformUtils::str2wstr(pars); 26 | 27 | std::wcout << L"Launch explorer with args: " << parsw << '\n'; 28 | 29 | ShellExecuteW(nullptr, L"open", L"explorer", parsw.c_str(), nullptr, SW_SHOWNORMAL); 30 | } 31 | 32 | -------------------------------------------------------------------------------- /lib/private/WinPlatformUtils.cpp: -------------------------------------------------------------------------------- 1 | #include "platformutils.h" 2 | #include "utils.h" 3 | 4 | #include 5 | 6 | #include 7 | 8 | bool PlatformUtils::can_scan_dir(const std::string &path) { 9 | auto wpath = PlatformUtils::str2wstr(path); 10 | bool result = false; 11 | 12 | auto handle = CreateFileW( 13 | wpath.c_str(), 14 | FILE_LIST_DIRECTORY, 15 | FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, 16 | nullptr, 17 | OPEN_EXISTING, 18 | FILE_FLAG_BACKUP_SEMANTICS, 19 | nullptr 20 | ); 21 | 22 | if (handle != INVALID_HANDLE_VALUE) { 23 | BY_HANDLE_FILE_INFORMATION lpFileInformation; 24 | if (GetFileInformationByHandle(handle, &lpFileInformation)) 25 | result = (lpFileInformation.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0; 26 | } else 27 | return false; 28 | 29 | CloseHandle(handle); 30 | return result; 31 | } 32 | 33 | std::vector PlatformUtils::getAvailableMounts() { 34 | std::vector availableMounts; 35 | 36 | auto drivesMask = GetLogicalDrives(); 37 | 38 | char driveName[4] = {'A', ':', '\\', '\0'}; 39 | 40 | for (char drive = 'A'; drive <= 'Z'; ++drive) { 41 | if (drivesMask & 0x1u) { 42 | driveName[0] = drive; 43 | auto type = GetDriveTypeA(driveName); 44 | 45 | if (type == DRIVE_FIXED || type == DRIVE_REMOVABLE) 46 | availableMounts.emplace_back(driveName); 47 | } 48 | drivesMask >>= 1u; 49 | } 50 | 51 | return availableMounts; 52 | } 53 | 54 | std::vector PlatformUtils::getExcludedPaths() { 55 | return {}; 56 | } 57 | 58 | bool PlatformUtils::get_mount_space(const std::string &path, int64_t &totalSpace, int64_t &availableSpace) { 59 | auto wpath = PlatformUtils::str2wstr(path); 60 | 61 | ULARGE_INTEGER totalBytes; 62 | ULARGE_INTEGER freeBytesForCaller; 63 | 64 | totalSpace = 0; 65 | availableSpace = 0; 66 | 67 | if (GetDiskFreeSpaceExW(wpath.c_str(), &freeBytesForCaller, &totalBytes, nullptr) != 0) { 68 | totalSpace = static_cast(totalBytes.QuadPart); 69 | availableSpace = static_cast(freeBytesForCaller.QuadPart); 70 | 71 | return true; 72 | } else 73 | return false; 74 | } 75 | 76 | std::wstring PlatformUtils::str2wstr(std::string const &str) { 77 | int len = MultiByteToWideChar(CP_UTF8, 0, str.c_str(), (int) str.size(), nullptr, 0); 78 | std::wstring ret(len, '\0'); 79 | MultiByteToWideChar(CP_UTF8, 0, str.c_str(), (int) str.size(), (LPWSTR) ret.data(), (int) ret.size()); 80 | return ret; 81 | } 82 | 83 | std::string PlatformUtils::wstr2str(std::wstring const &wstr) { 84 | int len = WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), (int) wstr.size(), nullptr, 0, nullptr, nullptr); 85 | std::string ret(len, '\0'); 86 | WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), (int) wstr.size(), (LPSTR) ret.data(), (int) ret.size(), nullptr, 87 | nullptr); 88 | return ret; 89 | } 90 | 91 | bool PlatformUtils::toLongPath(std::string &shortPath) { 92 | auto wpath = PlatformUtils::str2wstr(shortPath); 93 | auto requiredBufLen = GetLongPathNameW(wpath.c_str(), nullptr, 0); 94 | if (requiredBufLen > 0) { 95 | auto longName = Utils::make_unique_arr(requiredBufLen); 96 | auto copiedChars = GetLongPathNameW(wpath.c_str(), longName.get(), requiredBufLen); 97 | if (copiedChars < requiredBufLen && copiedChars > 0) { 98 | shortPath = PlatformUtils::wstr2str(longName.get()); 99 | return true; 100 | } 101 | } 102 | return false; 103 | } 104 | 105 | bool PlatformUtils::deleteDir(const std::string &path) { 106 | std::wstring wpath = str2wstr(path); 107 | 108 | // string in SHFILEOPSTRUCTW must be double null terminated so do it manually 109 | std::vector buffer(wpath.size() + 2, L'\0'); 110 | memcpy(buffer.data(), wpath.c_str(), wpath.size() * sizeof(wchar_t)); 111 | 112 | SHFILEOPSTRUCTW file_op = { 113 | nullptr, 114 | FO_DELETE, 115 | buffer.data(), 116 | nullptr, 117 | FOF_NOCONFIRMATION | 118 | FOF_NOERRORUI | 119 | FOF_SILENT, 120 | false, 121 | nullptr, 122 | nullptr}; 123 | return SHFileOperationW(&file_op) == 0; // returns 0 on success, non zero on failure. 124 | } 125 | -------------------------------------------------------------------------------- /lib/private/WinSpaceWatcher.cpp: -------------------------------------------------------------------------------- 1 | #include "WinSpaceWatcher.h" 2 | 3 | #include "platformutils.h" 4 | #include "utils.h" 5 | 6 | #include 7 | 8 | std::unique_ptr SpaceWatcher::create(const std::string &path) { 9 | auto *ptr = new WinSpaceWatcher(); 10 | if (!ptr->beginWatch(path)) { 11 | delete ptr; 12 | throw std::runtime_error("Can't start watching " + path); 13 | } 14 | return std::unique_ptr(ptr); 15 | } 16 | 17 | WinSpaceWatcher::WinSpaceWatcher() : watchedDir(INVALID_HANDLE_VALUE) { 18 | watchBuffer = std::unique_ptr(new DWORD[watchBufferSize]); 19 | } 20 | 21 | WinSpaceWatcher::~WinSpaceWatcher() { 22 | if (watchedDir != INVALID_HANDLE_VALUE) { 23 | CloseHandle(watchedDir); 24 | watchedPath.clear(); 25 | watchedDir = INVALID_HANDLE_VALUE; 26 | } 27 | } 28 | 29 | bool WinSpaceWatcher::beginWatch(const std::string &path) { 30 | auto wname = PlatformUtils::str2wstr(path); 31 | watchedDir = CreateFileW( 32 | wname.c_str(), 33 | FILE_LIST_DIRECTORY, //needed for reading changes 34 | FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, 35 | nullptr, 36 | OPEN_EXISTING, 37 | FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, //for async use 38 | nullptr 39 | ); 40 | 41 | if (watchedDir != INVALID_HANDLE_VALUE) { 42 | watchedPath = path; 43 | if (watchedPath.back() != PlatformUtils::filePathSeparator) 44 | watchedPath.push_back(PlatformUtils::filePathSeparator); 45 | return true; 46 | } 47 | return false; 48 | } 49 | 50 | void WinSpaceWatcher::readEvents() { 51 | if (watchedDir == INVALID_HANDLE_VALUE) 52 | return; 53 | 54 | DWORD bytesReturned = 0; 55 | 56 | auto success = ReadDirectoryChangesW( 57 | watchedDir, 58 | watchBuffer.get(), //DWORD aligned buffer 59 | watchBufferSize * sizeof(DWORD), //size of buffer in bytes 60 | true, //watching all files recursively 61 | FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME | FILE_NOTIFY_CHANGE_SIZE, 62 | &bytesReturned, 63 | nullptr, 64 | nullptr 65 | ); 66 | if (success == 0 || bytesReturned == 0) 67 | return; 68 | 69 | //calculate required number of dwords to hold returned bytes 70 | auto dwords = bytesReturned / sizeof(DWORD) + 1; 71 | 72 | // buffer is filled with objects of structure FILE_NOTIFY_INFORMATION 73 | // first DWORD holds offset to the next record (in bytes!) 74 | // second DWORD holds Action 75 | // third DWORD holds filename length 76 | // then wchar buffer of filename length (without null terminating char) 77 | 78 | // minimum size is 4, so do nothing until we receive it 79 | if (dwords > 3) { 80 | size_t currentDword = 0; 81 | 82 | while (true) { 83 | auto notifyInfo = reinterpret_cast(&watchBuffer[currentDword]); 84 | 85 | auto fileEvent = Utils::make_unique(); 86 | switch (notifyInfo->Action) { 87 | case FILE_ACTION_ADDED: 88 | fileEvent->action = FileAction::ADDED; 89 | break; 90 | case FILE_ACTION_REMOVED: 91 | fileEvent->action = FileAction::REMOVED; 92 | break; 93 | case FILE_ACTION_MODIFIED: 94 | fileEvent->action = FileAction::MODIFIED; 95 | break; 96 | case FILE_ACTION_RENAMED_OLD_NAME: 97 | fileEvent->action = FileAction::OLD_NAME; 98 | break; 99 | case FILE_ACTION_RENAMED_NEW_NAME: 100 | fileEvent->action = FileAction::NEW_NAME; 101 | break; 102 | default: 103 | continue;//should not happen 104 | } 105 | 106 | auto nameLen = notifyInfo->FileNameLength / sizeof(wchar_t); 107 | //nameLen doesn't include null-terminating byte 108 | auto name = Utils::make_unique_arr(nameLen + 1); 109 | 110 | memcpy(name.get(), notifyInfo->FileName, notifyInfo->FileNameLength); 111 | name[nameLen] = L'\0'; 112 | fileEvent->filepath = watchedPath + PlatformUtils::wstr2str(name.get()); 113 | 114 | // returned filename might (or might not) be a short path. we should restore long path 115 | 116 | if (fileEvent->action != FileAction::MODIFIED && fileEvent->action != FileAction::REMOVED) { 117 | PlatformUtils::toLongPath(fileEvent->filepath); 118 | } 119 | 120 | if (fileEvent->filepath.back() == '\\') 121 | fileEvent->filepath.pop_back(); 122 | 123 | auto lastSlash = fileEvent->filepath.find_last_of('\\'); 124 | if (lastSlash != std::string::npos) 125 | fileEvent->parentpath = fileEvent->filepath.substr(0, lastSlash + 1); 126 | else 127 | fileEvent->parentpath = ""; 128 | 129 | PlatformUtils::toLongPath(fileEvent->parentpath); 130 | 131 | addEvent(std::move(fileEvent)); 132 | 133 | //offset to next this should be dword aligned 134 | auto next = notifyInfo->NextEntryOffset / sizeof(DWORD); 135 | currentDword += next; 136 | if (next == 0) 137 | break; 138 | } 139 | } 140 | 141 | } 142 | -------------------------------------------------------------------------------- /lib/private/WinSpaceWatcher.h: -------------------------------------------------------------------------------- 1 | #ifndef SPACEDISPLAY_WINSPACEWATCHER_H 2 | #define SPACEDISPLAY_WINSPACEWATCHER_H 3 | 4 | #include "spacewatcher.h" 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | 13 | class WinSpaceWatcher : public SpaceWatcher { 14 | public: 15 | ~WinSpaceWatcher() override; 16 | 17 | int64_t getDirCountLimit() const override { return -1; } 18 | 19 | friend std::unique_ptr SpaceWatcher::create(const std::string &path); 20 | protected: 21 | void readEvents() override; 22 | 23 | private: 24 | WinSpaceWatcher(); 25 | 26 | const int watchBufferSize = 1024 * 48; 27 | 28 | std::atomic watchedDir; 29 | 30 | std::string watchedPath; 31 | 32 | std::unique_ptr watchBuffer; 33 | 34 | bool beginWatch(const std::string &path); 35 | }; 36 | 37 | #endif //SPACEDISPLAY_WINSPACEWATCHER_H 38 | -------------------------------------------------------------------------------- /lib/src/fileentry.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | 5 | #include "fileentry.h" 6 | #include "utils.h" 7 | 8 | extern "C" { 9 | #include 10 | } 11 | 12 | FileEntry::FileEntry(const std::string &name_, bool isDir_, int64_t size_) : 13 | bIsDir(isDir_), pendingDelete(false), parent(nullptr), 14 | nameCrc(0), pathCrc(0), size(size_) { 15 | auto nameLen = name_.length(); 16 | if (nameLen == 0) 17 | throw std::invalid_argument("Can't create FileEntry with empty name"); 18 | 19 | auto chars = Utils::make_unique_arr(nameLen + 1); 20 | memcpy(chars.get(), name_.c_str(), (nameLen + 1) * sizeof(char)); 21 | nameCrc = crc16(chars.get(), (uint16_t) nameLen); 22 | pathCrc = nameCrc; 23 | name = std::move(chars); 24 | } 25 | 26 | void FileEntry::addChild(std::unique_ptr child) { 27 | if (!child) 28 | return; 29 | 30 | auto childSize = child->size; 31 | //this will only insert child without changing our size or any callbacks to parent 32 | _addChild(std::move(child)); 33 | size += childSize; 34 | 35 | if (parent) 36 | parent->onChildSizeChanged(this, childSize); 37 | } 38 | 39 | void FileEntry::removePendingDelete(std::vector> &deletedChildren) { 40 | int64_t changedSize = 0; 41 | auto it = children.begin(); 42 | while (it != children.end()) { 43 | if (!it->firstEntry) //this shouldn't happen, but better check 44 | it = children.erase(it); 45 | 46 | auto child = it->firstEntry.get(); 47 | while (child->nextEntry) { 48 | if (child->nextEntry->pendingDelete) { 49 | auto temp = std::move(child->nextEntry); 50 | child->nextEntry = std::move(temp->nextEntry); 51 | changedSize += temp->size; 52 | deletedChildren.push_back(std::move(temp)); 53 | continue; 54 | } 55 | child = child->nextEntry.get(); 56 | } 57 | //now all children in this bin (except first) are processed 58 | //now check if first child should also be removed and if it is, check if we have anything left 59 | if (it->firstEntry->pendingDelete) { 60 | auto &ptrFirst = const_cast &>(it->firstEntry); 61 | auto temp = std::move(ptrFirst); 62 | if (temp->nextEntry) 63 | ptrFirst = std::move(temp->nextEntry); 64 | changedSize += temp->size; 65 | deletedChildren.push_back(std::move(temp)); 66 | } 67 | //if no entries left, delete bin 68 | if (!it->firstEntry) 69 | it = children.erase(it); 70 | else 71 | ++it; 72 | } 73 | if (changedSize > 0) { 74 | size -= changedSize; 75 | if (parent) 76 | parent->onChildSizeChanged(this, -changedSize); 77 | } 78 | } 79 | 80 | void FileEntry::_addChild(std::unique_ptr child) { 81 | if (!child) 82 | return; 83 | 84 | const auto childSize = child->size; 85 | child->parent = this; 86 | child->updatePathCrc(pathCrc); 87 | // first try to find existing bin for appropriate size 88 | auto it = children.find(EntryBin(childSize)); 89 | if (it != children.end()) { 90 | // if it exists, just add child to the front 91 | // cost cast is required because iterator returns const qualified bin 92 | // pointers don't affect sorting of children so this is safe 93 | auto &ptrFirst = const_cast &>(it->firstEntry); 94 | child->nextEntry = std::move(ptrFirst); 95 | ptrFirst = std::move(child); 96 | } else { 97 | // there is no bin with such size, so create new one 98 | children.insert(EntryBin(childSize, std::move(child))); 99 | } 100 | } 101 | 102 | void FileEntry::setSize(int64_t newSize) { 103 | auto sizeChange = newSize - size; 104 | size = newSize; 105 | // if size changed, tell about it to parent 106 | if (parent && sizeChange != 0) 107 | parent->onChildSizeChanged(this, sizeChange); 108 | } 109 | 110 | int64_t FileEntry::getSize() const { 111 | return size; 112 | } 113 | 114 | uint16_t FileEntry::getNameCrc() const { 115 | return nameCrc; 116 | } 117 | 118 | uint16_t FileEntry::getPathCrc() const { 119 | return pathCrc; 120 | } 121 | 122 | void FileEntry::updatePathCrc(uint16_t parentPathCrc) { 123 | pathCrc = parentPathCrc ^ nameCrc; 124 | } 125 | 126 | const char *FileEntry::getName() const { 127 | return name.get(); 128 | } 129 | 130 | const FileEntry *FileEntry::getParent() const { 131 | return parent; 132 | } 133 | 134 | bool FileEntry::forEach(const std::function &func) const { 135 | if (children.empty()) 136 | return false; 137 | 138 | for (auto &bin : children) { 139 | bool stop = false; 140 | auto child = bin.firstEntry.get(); 141 | while (child != nullptr && !stop) { 142 | stop = !func(*child); 143 | child = child->nextEntry.get(); 144 | } 145 | 146 | if (stop) 147 | break; 148 | } 149 | 150 | return true; 151 | } 152 | 153 | void FileEntry::markChildrenPendingDelete(int &files, int &dirs) { 154 | files = 0; 155 | dirs = 0; 156 | 157 | for (auto &bin : children) { 158 | auto child = bin.firstEntry.get(); 159 | while (child) { 160 | if (child->bIsDir) 161 | ++dirs; 162 | else 163 | ++files; 164 | child->pendingDelete = true; 165 | child = child->nextEntry.get(); 166 | } 167 | } 168 | } 169 | 170 | void FileEntry::unmarkPendingDelete() { 171 | pendingDelete = false; 172 | } 173 | 174 | bool FileEntry::isDir() const { 175 | return bIsDir; 176 | } 177 | 178 | bool FileEntry::isRoot() const { 179 | return parent == nullptr; 180 | } 181 | 182 | void FileEntry::onChildSizeChanged(FileEntry *child, int64_t sizeChange) { 183 | 184 | if (!child || sizeChange == 0) 185 | return; 186 | 187 | //size of child changed so we should use its previous size 188 | auto it = children.find(EntryBin(child->size - sizeChange)); 189 | 190 | if (it == children.end()) { 191 | //should not happen 192 | std::cerr << "Can't find child in parents children!\n"; 193 | return; 194 | } 195 | 196 | auto first = it->firstEntry.get(); 197 | 198 | std::unique_ptr childPtr; 199 | 200 | if (first == child) { 201 | auto &ptrFirst = const_cast &>(it->firstEntry); 202 | childPtr = std::move(ptrFirst); 203 | if (childPtr->nextEntry) 204 | ptrFirst = std::move(childPtr->nextEntry); 205 | else { 206 | //this was the only child in bin so we should delete bin 207 | children.erase(it); 208 | } 209 | } else { 210 | //find child in entry chain 211 | while (first != nullptr && first->nextEntry.get() != child) 212 | first = first->nextEntry.get(); 213 | 214 | if (first != nullptr) { 215 | childPtr = std::move(first->nextEntry); 216 | first->nextEntry = std::move(childPtr->nextEntry); 217 | } 218 | } 219 | 220 | if (!childPtr) { 221 | //should not happen 222 | std::cerr << "Can't find child in parents children!\n"; 223 | return; 224 | } 225 | 226 | // now just add child back 227 | _addChild(std::move(childPtr)); 228 | size += sizeChange; 229 | 230 | if (parent) 231 | parent->onChildSizeChanged(this, sizeChange); 232 | } 233 | -------------------------------------------------------------------------------- /lib/src/filepath.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | #include "filepath.h" 5 | #include "utils.h" 6 | #include "platformutils.h" 7 | 8 | FilePath::FilePath(const std::string &root_, uint16_t crc) { 9 | if (root_.empty()) 10 | throw std::invalid_argument("Can't set empty root for path"); 11 | std::string root = root_; 12 | 13 | // replaces all incorrect slashes if any and adds slash at the end if it wasn't there 14 | normalize(root, SlashHandling::ADD); 15 | 16 | parts.clear(); 17 | pathCrcs.clear(); 18 | parts.push_back(root); 19 | if (crc == 0) 20 | crc = Utils::strCrc16(root); 21 | pathCrcs.push_back(crc); 22 | } 23 | 24 | FilePath::FilePath(const std::string &path, const std::string &root) { 25 | if (path.empty() || root.empty()) 26 | throw std::invalid_argument("Can't create path from empty root or path"); 27 | 28 | auto newPath = path; 29 | auto newRoot = root; 30 | bool isDir = path.back() == PlatformUtils::filePathSeparator || 31 | path.back() == PlatformUtils::invertedFilePathSeparator; 32 | normalize(newPath, isDir ? SlashHandling::ADD : SlashHandling::REMOVE); 33 | normalize(newRoot, SlashHandling::ADD); 34 | 35 | if (newPath.rfind(newRoot, 0) != 0) 36 | throw std::invalid_argument("Provided path doesn't begin with provided root"); 37 | 38 | parts.push_back(newRoot); 39 | pathCrcs.push_back(Utils::strCrc16(newRoot)); 40 | 41 | newPath.erase(0, newRoot.length()); 42 | 43 | //split remaining string by slashes 44 | size_t pos; 45 | while ((pos = newPath.find(PlatformUtils::filePathSeparator)) != std::string::npos) { 46 | std::string dirName = newPath.substr(0, pos + 1); //keep slash 47 | parts.push_back(dirName); 48 | dirName.pop_back();// slash should not count in calculation of crc 49 | pathCrcs.push_back(pathCrcs.back() ^ Utils::strCrc16(dirName)); 50 | newPath.erase(0, pos + 1); 51 | } 52 | if (!newPath.empty()) { 53 | parts.push_back(newPath); 54 | pathCrcs.push_back(pathCrcs.back() ^ Utils::strCrc16(newPath)); 55 | } 56 | } 57 | 58 | std::string FilePath::getPath(bool addDirSlash) const { 59 | std::string path; 60 | //each directory name already has slash at the end so we just concatenate them all 61 | for (const auto &part : parts) 62 | path.append(part); 63 | //if we don't need slash at the end for directories - remove it 64 | if (!addDirSlash && path.back() == PlatformUtils::filePathSeparator) 65 | path.pop_back(); 66 | 67 | return path; 68 | } 69 | 70 | std::string FilePath::getName() const { 71 | if (parts.size() == 1 || !isDir()) //root and file are returned as is 72 | return parts.back(); 73 | else { 74 | std::string str = parts.back(); 75 | str.pop_back();//remove last slash 76 | return str; 77 | } 78 | } 79 | 80 | std::string FilePath::getRoot() const { 81 | return parts[0]; 82 | } 83 | 84 | const std::vector &FilePath::getParts() const { 85 | return parts; 86 | } 87 | 88 | uint16_t FilePath::getPathCrc() const { 89 | return pathCrcs.back(); 90 | } 91 | 92 | void FilePath::addDir(const std::string &name, uint16_t crc) { 93 | if (name.empty()) 94 | throw std::invalid_argument("Can't add dir with empty name"); 95 | if (!isDir()) 96 | throw std::invalid_argument("Can't add dir to file path"); 97 | parts.push_back(name); 98 | // every part that is a directory should have slash at the end 99 | if (parts.back().back() != PlatformUtils::filePathSeparator) 100 | parts.back().push_back(PlatformUtils::filePathSeparator); 101 | 102 | if (crc == 0) 103 | crc = Utils::strCrc16(name); 104 | pathCrcs.push_back(pathCrcs.back() ^ crc); 105 | } 106 | 107 | void FilePath::addFile(const std::string &name, uint16_t crc) { 108 | if (name.empty()) 109 | throw std::invalid_argument("Can't add file with empty name"); 110 | if (!isDir()) 111 | throw std::invalid_argument("Can't add file to file path"); 112 | parts.push_back(name); 113 | if (crc == 0) 114 | crc = Utils::strCrc16(name); 115 | pathCrcs.push_back(pathCrcs.back() ^ crc); 116 | } 117 | 118 | bool FilePath::isDir() const { 119 | return parts.back().back() == PlatformUtils::filePathSeparator; 120 | } 121 | 122 | bool FilePath::canGoUp() const { 123 | return parts.size() > 1; 124 | } 125 | 126 | bool FilePath::goUp() { 127 | if (canGoUp()) { 128 | parts.pop_back(); 129 | pathCrcs.pop_back(); 130 | return true; 131 | } else 132 | return false; 133 | } 134 | 135 | FilePath::CompareResult FilePath::compareTo(const FilePath &path) { 136 | if (parts.size() < path.parts.size()) { 137 | //this path can be only parent to provided path if crc is the same 138 | if (path.pathCrcs[parts.size() - 1] != pathCrcs.back()) 139 | return CompareResult::DIFFERENT; 140 | 141 | for (int i = 0; i < parts.size(); ++i) 142 | if (parts[i] != path.parts[i]) 143 | return CompareResult::DIFFERENT; 144 | 145 | return CompareResult::PARENT; 146 | } else { 147 | if (pathCrcs[path.parts.size() - 1] != path.pathCrcs.back()) 148 | return CompareResult::DIFFERENT; 149 | 150 | for (int i = 0; i < path.parts.size(); ++i) 151 | if (parts[i] != path.parts[i]) 152 | return CompareResult::DIFFERENT; 153 | 154 | if (parts.size() == path.parts.size()) 155 | return CompareResult::EQUAL; 156 | 157 | return CompareResult::CHILD; 158 | } 159 | } 160 | 161 | bool FilePath::makeRelativeTo(const FilePath &parentPath) { 162 | auto state = compareTo(parentPath); 163 | if (state != CompareResult::CHILD) //this path should be child to parentPath 164 | return false; 165 | 166 | //erase first entries from provided path, so it becomes relative to this path 167 | parts.erase(parts.begin(), parts.begin() + parentPath.parts.size()); 168 | pathCrcs.erase(pathCrcs.begin(), pathCrcs.begin() + parentPath.parts.size()); 169 | 170 | return true; 171 | } 172 | 173 | void FilePath::normalize(std::string &path, SlashHandling slashHandling) { 174 | // make all slashes correct for this platform 175 | std::replace(path.begin(), path.end(), PlatformUtils::invertedFilePathSeparator, PlatformUtils::filePathSeparator); 176 | 177 | if (slashHandling == SlashHandling::REMOVE) { 178 | while (path.size() > 1 && path.back() == PlatformUtils::filePathSeparator) { 179 | path.pop_back(); 180 | } 181 | } else if (slashHandling == SlashHandling::ADD) { 182 | while (path.size() > 1 && 183 | path.back() == PlatformUtils::filePathSeparator && 184 | path[path.size() - 2] == PlatformUtils::filePathSeparator) { 185 | path.pop_back(); 186 | } 187 | if (path.back() != PlatformUtils::filePathSeparator) { 188 | path.push_back(PlatformUtils::filePathSeparator); 189 | } 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /lib/src/logger.cpp: -------------------------------------------------------------------------------- 1 | #include "logger.h" 2 | 3 | #include "utils.h" 4 | #include 5 | 6 | 7 | Logger::Logger() : hasNewLog(false) { 8 | 9 | } 10 | 11 | void Logger::log(const std::string &msg, const std::string &tag) { 12 | std::lock_guard lock(logMtx); 13 | 14 | history.emplace_back(Utils::strFormat("[%s] %s", tag.c_str(), msg.c_str())); 15 | std::cout << history.back() << std::endl; 16 | hasNewLog = true; 17 | } 18 | 19 | std::vector Logger::getHistory() { 20 | std::lock_guard lock(logMtx); 21 | hasNewLog = false; 22 | return history; 23 | } 24 | 25 | void Logger::clear() { 26 | std::lock_guard lock(logMtx); 27 | history.clear(); 28 | hasNewLog = false; 29 | } 30 | 31 | bool Logger::hasNew() { 32 | //TODO should add option to remember multiple clients that can 33 | // have different states of `hasNew` property 34 | return hasNewLog; 35 | } 36 | -------------------------------------------------------------------------------- /lib/src/spacewatcher.cpp: -------------------------------------------------------------------------------- 1 | #include "spacewatcher.h" 2 | 3 | #include 4 | 5 | SpaceWatcher::~SpaceWatcher() { 6 | runThread = false; 7 | watchThread.join(); 8 | } 9 | 10 | int64_t SpaceWatcher::getWatchedDirCount() const { 11 | return getDirCountLimit() < 0 ? 1 : watchedDirCount; 12 | } 13 | 14 | SpaceWatcher::AddDirStatus SpaceWatcher::addDir(const std::string &path) { 15 | ++watchedDirCount; 16 | return AddDirStatus::ADDED; 17 | } 18 | 19 | void SpaceWatcher::rmDir(const std::string &path) { 20 | if (watchedDirCount > 0) 21 | --watchedDirCount; 22 | } 23 | 24 | std::unique_ptr SpaceWatcher::popEvent() { 25 | std::lock_guard lock(eventsMtx); 26 | if (!eventQueue.empty()) { 27 | auto event = std::move(eventQueue.front()); 28 | eventQueue.pop_front(); 29 | return event; 30 | } 31 | return nullptr; 32 | } 33 | 34 | SpaceWatcher::SpaceWatcher() : runThread(true), watchedDirCount(0) { 35 | //Start thread after everything is initialized 36 | watchThread = std::thread(&SpaceWatcher::watcherRun, this); 37 | } 38 | 39 | void SpaceWatcher::addEvent(std::unique_ptr event) { 40 | if (!event) 41 | return; 42 | 43 | std::lock_guard lock(eventsMtx); 44 | //TODO check if we already have the same events in queue 45 | eventQueue.push_back(std::move(event)); 46 | } 47 | 48 | void SpaceWatcher::watcherRun() { 49 | std::cout << "Start watcher thread\n"; 50 | while (runThread) { 51 | readEvents(); 52 | std::this_thread::sleep_for(std::chrono::milliseconds(20)); 53 | } 54 | std::cout << "Stop watcher thread\n"; 55 | } 56 | -------------------------------------------------------------------------------- /lib/src/utils.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include "utils.h" 7 | #include "platformutils.h" 8 | 9 | extern "C" { 10 | #include 11 | } 12 | 13 | uint16_t Utils::strCrc16(const std::string &str) { 14 | return crc16((char *) str.c_str(), (uint16_t) str.length()); 15 | } 16 | 17 | std::string Utils::formatSize(int64_t size) { 18 | const float KB = 1024.f; 19 | const float MB = KB * 1024.f; 20 | const float GB = MB * 1024.f; 21 | 22 | auto sz = (float) size; 23 | 24 | std::string sizeStr; 25 | 26 | if (sz > GB) { 27 | sz /= GB; 28 | sizeStr = strFormat("%0.1f GiB", sz); 29 | } else if (sz > MB) { 30 | sz /= MB; 31 | sizeStr = strFormat("%0.1f MiB", sz); 32 | } else if (sz > KB) { 33 | sz /= KB; 34 | sizeStr = strFormat("%0.1f KiB", sz); 35 | } else { 36 | sizeStr = strFormat("%0.1f B", sz); 37 | } 38 | 39 | return sizeStr; 40 | } 41 | -------------------------------------------------------------------------------- /res/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | #generate files for resource compilation 2 | find_package( PythonInterp 3.0 REQUIRED ) 3 | execute_process(COMMAND ${PYTHON_EXECUTABLE} 4 | ${PROJECT_SOURCE_DIR}/deps/resource-builder/resource-builder.py #path to python script that builds resources 5 | ${CMAKE_CURRENT_SOURCE_DIR} #work directory where resources are stored 6 | WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) 7 | 8 | # link to generated resources 9 | add_subdirectory(build) 10 | 11 | # On Windows add .rc file with app icon to resources library 12 | # SpacedisplayRes is the name of auto generated library (declared in resources.json) 13 | if( WIN32 ) 14 | target_sources(SpacedisplayRes INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/win-res.rc) 15 | endif() 16 | 17 | if( NOT WIN32 ) 18 | install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/spacedisplay.desktop 19 | DESTINATION share/applications/) 20 | install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/icons/svg/appicon.svg 21 | DESTINATION share/icons/hicolor/scalable/apps 22 | RENAME spacedisplay.svg) 23 | endif() 24 | -------------------------------------------------------------------------------- /res/icons/appicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funbiscuit/spacedisplay/963e1ce22abfec28af2529f25ebaeda0e82e3701/res/icons/appicon.ico -------------------------------------------------------------------------------- /res/icons/svg/appicon-bw.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /res/icons/svg/appicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /res/icons/svg/arrow-back.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /res/icons/svg/arrow-forward.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /res/icons/svg/folder-navigate-up.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /res/icons/svg/folder-refresh.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /res/icons/svg/home.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /res/icons/svg/new-scan.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /res/icons/svg/notify-new.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 8 | 9 | -------------------------------------------------------------------------------- /res/icons/svg/notify.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /res/icons/svg/pause.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /res/icons/svg/refresh.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /res/icons/svg/smooth-mode.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /res/icons/svg/space-free.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /res/icons/svg/space-unknown.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /res/icons/svg/zoom-in.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /res/icons/svg/zoom-out.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /res/resources.json: -------------------------------------------------------------------------------- 1 | { 2 | "resources": [ 3 | "./icons/svg/*.svg" 4 | ], 5 | "output" : "build", 6 | "project_name" : "SpacedisplayRes" 7 | } 8 | -------------------------------------------------------------------------------- /res/spacedisplay.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Encoding=UTF-8 3 | Type=Application 4 | Name=Space Display 5 | Comment=Simple tool for analyzing used disk space 6 | Exec=spacedisplay 7 | Icon=spacedisplay 8 | Categories=Utility;System;FileTools; 9 | -------------------------------------------------------------------------------- /res/win-res.rc: -------------------------------------------------------------------------------- 1 | 1 VERSIONINFO 2 | FILEVERSION 1,0,0,0 3 | PRODUCTVERSION 1,0,0,0 4 | BEGIN 5 | BLOCK "StringFileInfo" 6 | BEGIN 7 | BLOCK "040904E4" 8 | BEGIN 9 | VALUE "CompanyName", "funbiscuit" 10 | VALUE "FileVersion", "1.0" 11 | VALUE "ProductName", "Space Display" 12 | VALUE "ProductVersion", "1.0" 13 | END 14 | END 15 | BLOCK "VarFileInfo" 16 | BEGIN 17 | VALUE "Translation", 0x409, 1252 18 | END 19 | END 20 | id ICON "icons/appicon.ico" 21 | -------------------------------------------------------------------------------- /tests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_executable(spacedisplay_test DirHelper.cpp) 2 | 3 | target_include_directories(spacedisplay_test PRIVATE 4 | ${CMAKE_CURRENT_SOURCE_DIR} 5 | ) 6 | 7 | target_sources(spacedisplay_test PRIVATE 8 | ${CMAKE_CURRENT_SOURCE_DIR}/FileIteratorTest.cpp 9 | ${CMAKE_CURRENT_SOURCE_DIR}/FileEntryTest.cpp 10 | ${CMAKE_CURRENT_SOURCE_DIR}/FileDBTest.cpp 11 | ${CMAKE_CURRENT_SOURCE_DIR}/FilePathTest.cpp 12 | ${CMAKE_CURRENT_SOURCE_DIR}/ScannerTest.cpp 13 | ${CMAKE_CURRENT_SOURCE_DIR}/SpaceWatcherTest.cpp 14 | ${CMAKE_CURRENT_SOURCE_DIR}/UtilsTest.cpp 15 | ${CMAKE_CURRENT_SOURCE_DIR}/LoggerTest.cpp 16 | ${CMAKE_CURRENT_SOURCE_DIR}/PriorityCacheTest.cpp 17 | ) 18 | 19 | target_link_libraries(spacedisplay_test PRIVATE spacedisplay_lib) 20 | target_link_libraries(spacedisplay_test PRIVATE Catch2WithMain) 21 | -------------------------------------------------------------------------------- /tests/DirHelper.cpp: -------------------------------------------------------------------------------- 1 | #include "DirHelper.h" 2 | #include "platformutils.h" 3 | #include "utils.h" 4 | #include "filepath.h" 5 | 6 | #include 7 | 8 | //TODO rewrite 9 | #if _WIN32 10 | 11 | #include 12 | 13 | #define mkdir(dir, mode) _mkdir(dir) 14 | #define rmdir(dir) _rmdir(dir) 15 | #else 16 | #include 17 | #include 18 | #endif 19 | 20 | 21 | DirHelper::DirHelper(const std::string &path) { 22 | // delete dir if it already exist 23 | PlatformUtils::deleteDir(path); 24 | if (mkdir(path.c_str(), 0755) == 0) 25 | root = Utils::make_unique(path); 26 | } 27 | 28 | DirHelper::~DirHelper() { 29 | if (root) 30 | PlatformUtils::deleteDir(root->getRoot()); 31 | } 32 | 33 | bool DirHelper::createDir(const std::string &path) { 34 | if (!root) 35 | return false; 36 | 37 | auto newPath = *root; 38 | newPath.addDir(path); 39 | 40 | return mkdir(newPath.getPath().c_str(), 0755) == 0; 41 | } 42 | 43 | bool DirHelper::createFile(const std::string &path) { 44 | if (!root) 45 | return false; 46 | 47 | auto newPath = *root; 48 | newPath.addFile(path); 49 | 50 | auto f = fopen(newPath.getPath().c_str(), "w+"); 51 | if (f) { 52 | fclose(f); 53 | return true; 54 | } 55 | return false; 56 | } 57 | 58 | bool DirHelper::deleteDir(const std::string &path) { 59 | if (!root) 60 | return false; 61 | 62 | auto newPath = *root; 63 | newPath.addDir(path); 64 | 65 | return PlatformUtils::deleteDir(newPath.getPath()); 66 | } 67 | 68 | bool DirHelper::rename(const std::string &oldPath, const std::string &newPath) { 69 | if (!root) 70 | return false; 71 | 72 | auto path = *root; 73 | path.addFile(oldPath); //this way it works for both dirs and files 74 | auto oldStr = path.getPath(false); 75 | path.goUp(); 76 | path.addFile(newPath); 77 | 78 | return ::rename(oldStr.c_str(), path.getPath(false).c_str()) == 0; 79 | } 80 | -------------------------------------------------------------------------------- /tests/DirHelper.h: -------------------------------------------------------------------------------- 1 | #ifndef SPACEDISPLAY_DIRHELPER_H 2 | #define SPACEDISPLAY_DIRHELPER_H 3 | 4 | #include 5 | #include 6 | 7 | class FilePath; 8 | 9 | class DirHelper { 10 | public: 11 | /** 12 | * Creates directory at specified path. All other dirs/files 13 | * will be created in that directory 14 | * @param path 15 | */ 16 | explicit DirHelper(const std::string &path); 17 | 18 | ~DirHelper(); 19 | 20 | bool createDir(const std::string &path); 21 | 22 | bool createFile(const std::string &path); 23 | 24 | bool deleteDir(const std::string &path); 25 | 26 | bool rename(const std::string &oldPath, const std::string &newPath); 27 | 28 | private: 29 | std::unique_ptr root; 30 | }; 31 | 32 | #endif //SPACEDISPLAY_DIRHELPER_H 33 | -------------------------------------------------------------------------------- /tests/FileIteratorTest.cpp: -------------------------------------------------------------------------------- 1 | #include "filepath.h" 2 | #include "FileIterator.h" 3 | #include "DirHelper.h" 4 | 5 | #include 6 | 7 | struct File { 8 | std::string name; 9 | bool isDir; 10 | int64_t size; 11 | 12 | explicit File(const FileIterator &it) : 13 | File(it.getName(), it.isDir(), it.getSize()) {} 14 | 15 | explicit File(std::string name, bool isDir = false, int64_t size = 0) : name(std::move(name)), isDir(isDir), 16 | size(size) {} 17 | 18 | bool operator==(const File &other) const { 19 | return isDir == other.isDir && name == other.name; 20 | }; 21 | }; 22 | 23 | std::ostream &operator<<(std::ostream &os, File const &value) { 24 | if (value.isDir) 25 | os << "{ *dir* "; 26 | else 27 | os << "{ *file* "; 28 | os << value.name << " }"; 29 | return os; 30 | } 31 | 32 | #include 33 | #include 34 | 35 | TEST_CASE("FileIterator tests", "[file]") 36 | { 37 | DirHelper dh("TestDir"); 38 | SECTION("Iterator of non existing dir") 39 | { 40 | dh.deleteDir("dir"); 41 | auto it = FileIterator::create("dir"); 42 | REQUIRE_FALSE(it->isValid()); 43 | REQUIRE_THROWS_AS(it->getName(), std::out_of_range); 44 | REQUIRE_THROWS_AS(it->isDir(), std::out_of_range); 45 | REQUIRE_THROWS_AS(it->getSize(), std::out_of_range); 46 | REQUIRE_THROWS_AS(it->operator++(), std::out_of_range); 47 | } 48 | 49 | SECTION("Iterator of existing dir") 50 | { 51 | std::vector expectedFiles{ 52 | File("dir", true), 53 | File("file.txt"), 54 | File("file2.txt"), 55 | File("5.txt"), 56 | File("dir2", true), 57 | }; 58 | 59 | for (auto &f : expectedFiles) { 60 | if (f.isDir) 61 | dh.createDir(f.name); 62 | else 63 | dh.createFile(f.name); 64 | } 65 | dh.createFile("dir/3.txt"); 66 | dh.createFile("dir/4.txt"); 67 | 68 | auto it = FileIterator::create("TestDir"); 69 | REQUIRE(it->isValid()); 70 | 71 | std::vector files; 72 | //order is not specified, so need to collect all and then check 73 | while (it->isValid()) { 74 | files.emplace_back(*it); 75 | ++(*it); 76 | } 77 | 78 | REQUIRE_THAT(files, Catch::Matchers::UnorderedEquals(expectedFiles)); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /tests/LoggerTest.cpp: -------------------------------------------------------------------------------- 1 | #include "logger.h" 2 | 3 | #include 4 | 5 | TEST_CASE("Logger tests", "[logger]") 6 | { 7 | Logger logger; 8 | 9 | auto history = logger.getHistory(); 10 | 11 | REQUIRE(history.empty()); 12 | REQUIRE_FALSE(logger.hasNew()); 13 | 14 | SECTION("Add logs with default tag") 15 | { 16 | logger.log("test entry"); 17 | logger.log("new entry"); 18 | REQUIRE(logger.hasNew()); 19 | history = logger.getHistory(); 20 | REQUIRE(history.size() == 2); 21 | REQUIRE_FALSE(logger.hasNew()); 22 | 23 | history[0] = "[LOG] test entry"; 24 | history[1] = "[LOG] new entry"; 25 | } 26 | 27 | SECTION("Add logs with custom tag") 28 | { 29 | logger.log("test entry", "SCAN"); 30 | logger.log("new entry", "WATCH"); 31 | REQUIRE(logger.hasNew()); 32 | history = logger.getHistory(); 33 | REQUIRE(history.size() == 2); 34 | REQUIRE_FALSE(logger.hasNew()); 35 | 36 | history[0] = "[SCAN] test entry"; 37 | history[1] = "[WATCH] new entry"; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/PriorityCacheTest.cpp: -------------------------------------------------------------------------------- 1 | #include "PriorityCache.h" 2 | 3 | #include 4 | 5 | TEST_CASE("Priority cache tests with no supplier", "[priority-cache]") 6 | { 7 | PriorityCache cache; 8 | 9 | SECTION("Get throws when accessing non cached values") 10 | { 11 | REQUIRE_THROWS_AS(cache.get(1), std::runtime_error); 12 | } 13 | 14 | SECTION("Get does not throw when accessing cached values") 15 | { 16 | cache.put(1, 5); 17 | REQUIRE_NOTHROW(cache.get(1)); 18 | REQUIRE(cache.get(1) == 5); 19 | } 20 | 21 | SECTION("Is cached works for cached and not cached keys") { 22 | REQUIRE_FALSE(cache.isCached(1)); 23 | cache.put(1, 1); 24 | REQUIRE(cache.isCached(1)); 25 | } 26 | } 27 | 28 | TEST_CASE("Priority cache tests", "[priority-cache]") 29 | { 30 | size_t callCount = 0; 31 | auto supplier = [&callCount](int key) -> int { 32 | ++callCount; 33 | return key * key; 34 | }; 35 | 36 | PriorityCache cache(supplier); 37 | 38 | SECTION("Get returns correct results") 39 | { 40 | for (int i = -10; i < 10; ++i) { 41 | REQUIRE(cache.get(i) == i * i); 42 | } 43 | } 44 | 45 | SECTION("Get calls supplier only once for each unique key") 46 | { 47 | for (int i = 0; i < 10; ++i) { 48 | cache.get(1); 49 | } 50 | REQUIRE(callCount == 1); 51 | for (int i = 0; i < 10; ++i) { 52 | cache.get(i); 53 | } 54 | REQUIRE(callCount == 10); 55 | } 56 | 57 | SECTION("Trim removes oldest values from cache") 58 | { 59 | for (int i = 0; i < 10; ++i) { 60 | cache.get(i); 61 | } 62 | cache.trim(5); 63 | for (int i = 0; i < 10; ++i) { 64 | if (i < 5) { 65 | REQUIRE_FALSE(cache.isCached(i)); 66 | } else { 67 | REQUIRE(cache.isCached(i)); 68 | } 69 | } 70 | } 71 | 72 | SECTION("Access time of key is checked when trimming") 73 | { 74 | for (int i = 0; i < 10; ++i) { 75 | cache.get(i); 76 | } 77 | cache.get(0); 78 | cache.trim(5); 79 | for (int i = 0; i < 10; ++i) { 80 | if (i < 6 && i > 0) { 81 | REQUIRE_FALSE(cache.isCached(i)); 82 | } else { 83 | REQUIRE(cache.isCached(i)); 84 | } 85 | } 86 | } 87 | 88 | SECTION("Accessing key multiple times doesn't make it newer") 89 | { 90 | // even if key was accessed a lot of times, it will be dropped after 91 | // trim if there are newer keys 92 | for (int i = 0; i < 100; ++i) { 93 | cache.get(0); 94 | } 95 | for (int i = 1; i < 6; ++i) { 96 | cache.get(i); 97 | } 98 | cache.trim(5); 99 | REQUIRE_FALSE(cache.isCached(0)); 100 | for (int i = 1; i < 6; ++i) { 101 | REQUIRE(cache.isCached(i)); 102 | } 103 | } 104 | 105 | SECTION("With max size set, values are automatically removed") { 106 | int maxSize = 5; 107 | cache.setMaxSize(maxSize); 108 | for (int i = 0; i < maxSize; ++i) { 109 | cache.get(i); 110 | } 111 | for (int i = 0; i < maxSize; ++i) { 112 | REQUIRE(cache.isCached(i)); 113 | } 114 | for (int i = maxSize; i < 2*maxSize; ++i) { 115 | cache.get(i); 116 | 117 | for (int j = 0; j <= i; ++j) { 118 | INFO("Check key " << j << " when added key " << i); 119 | if (i - j >= maxSize) { 120 | REQUIRE_FALSE(cache.isCached(j)); 121 | } else { 122 | REQUIRE(cache.isCached(j)); 123 | } 124 | } 125 | } 126 | } 127 | 128 | SECTION("Set max size trims if needed") 129 | { 130 | int maxSize = 5; 131 | for (int i = 0; i < 2*maxSize; ++i) { 132 | cache.get(i); 133 | } 134 | cache.setMaxSize(maxSize); 135 | 136 | for (int i = 0; i < 2*maxSize; ++i) { 137 | if (i < maxSize) { 138 | REQUIRE_FALSE(cache.isCached(i)); 139 | } else { 140 | REQUIRE(cache.isCached(i)); 141 | } 142 | } 143 | } 144 | 145 | SECTION("Trim doesn't change max size") 146 | { 147 | int maxSize = 5; 148 | cache.setMaxSize(maxSize); 149 | for (int i = 0; i < 2*maxSize; ++i) { 150 | cache.get(i); 151 | } 152 | cache.trim(3); 153 | for (int i = 0; i < 2*maxSize; ++i) { 154 | cache.get(i); 155 | } 156 | for (int i = 0; i < 2*maxSize; ++i) { 157 | if (i < maxSize) { 158 | REQUIRE_FALSE(cache.isCached(i)); 159 | } else { 160 | REQUIRE(cache.isCached(i)); 161 | } 162 | } 163 | } 164 | 165 | SECTION("Invalidate removes all cached values") 166 | { 167 | int maxSize = 5; 168 | for (int i = 0; i < maxSize; ++i) { 169 | cache.get(i); 170 | } 171 | cache.invalidate(); 172 | for (int i = 0; i < maxSize; ++i) { 173 | REQUIRE_FALSE(cache.isCached(i)); 174 | } 175 | } 176 | } 177 | 178 | 179 | TEST_CASE("Priority cache with unique pointers", "[priority-cache]") 180 | { 181 | auto supplier = [](int key) -> std::unique_ptr { 182 | return std::unique_ptr(new int(key * key)); 183 | }; 184 | 185 | PriorityCache> cache(supplier); 186 | 187 | SECTION("Get returns correct results") 188 | { 189 | for (int i = -10; i < 10; ++i) { 190 | REQUIRE((*cache.get(i)) == i * i); 191 | } 192 | } 193 | 194 | SECTION("Trim removes oldest values from cache") 195 | { 196 | for (int i = 0; i < 10; ++i) { 197 | cache.get(i); 198 | } 199 | cache.get(0); 200 | cache.trim(5); 201 | for (int i = 0; i < 10; ++i) { 202 | if (i < 6 && i > 0) { 203 | REQUIRE_FALSE(cache.isCached(i)); 204 | } else { 205 | REQUIRE((*cache.get(i)) == i * i); 206 | } 207 | } 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /tests/ScannerTest.cpp: -------------------------------------------------------------------------------- 1 | #include "spacescanner.h" 2 | #include "filepath.h" 3 | #include "filedb.h" 4 | #include "utils.h" 5 | #include "platformutils.h" 6 | #include "DirHelper.h" 7 | 8 | #include 9 | 10 | #include 11 | 12 | TEST_CASE("Scanner tests", "[scanner]") 13 | { 14 | SECTION("Scan custom path") 15 | { 16 | DirHelper dh("TestDir"); 17 | dh.createDir("test"); 18 | dh.createFile("test/test.txt"); 19 | dh.createFile("test/test2.txt"); 20 | dh.createDir("test3"); 21 | dh.createFile("test3/test.txt"); 22 | dh.createFile("test3/test2.txt"); 23 | dh.createFile("test3/test3.txt"); 24 | dh.createFile("test3/test4.txt"); 25 | dh.createDir("test3/test5"); 26 | dh.createFile("test3/test5/test1.txt"); 27 | dh.createFile("test3/test5/test2.txt"); 28 | dh.createFile("test3/test5/test3.txt"); 29 | 30 | std::unique_ptr scanner; 31 | REQUIRE_THROWS_AS(SpaceScanner("TestDir2"), std::runtime_error); 32 | REQUIRE_NOTHROW(scanner = Utils::make_unique("TestDir")); 33 | 34 | REQUIRE(scanner->canPause()); 35 | REQUIRE_FALSE(scanner->canResume()); 36 | REQUIRE_FALSE(scanner->isProgressKnown()); 37 | REQUIRE(scanner->hasChanges()); 38 | 39 | //wait for scanner to complete 40 | while (scanner->getScanProgress() < 100) 41 | std::this_thread::sleep_for(std::chrono::milliseconds(1)); 42 | 43 | REQUIRE_FALSE(scanner->canPause()); 44 | REQUIRE_FALSE(scanner->canResume()); 45 | REQUIRE(scanner->hasChanges()); 46 | 47 | REQUIRE(scanner->getDirCount() == 4); 48 | REQUIRE(scanner->getFileCount() == 9); 49 | scanner->rescanPath(FilePath("TestDir")); 50 | REQUIRE(scanner->canPause()); 51 | REQUIRE_FALSE(scanner->canResume()); 52 | 53 | 54 | //wait for scanner to complete 55 | while (scanner->getScanProgress() < 100) 56 | std::this_thread::sleep_for(std::chrono::milliseconds(1)); 57 | 58 | REQUIRE(scanner->getDirCount() == 4); 59 | REQUIRE(scanner->getFileCount() == 9); 60 | 61 | dh.createDir("test3/test2"); 62 | dh.createFile("test3/test2/test1.txt"); 63 | dh.createFile("test3/test2/test2.txt"); 64 | dh.createFile("test3/test2/test3.txt"); 65 | dh.createDir("test3/test1"); 66 | dh.createFile("test3/test1/test2.txt"); 67 | dh.createFile("test3/test1/test3.txt"); 68 | //wait for scanner to detect changes 69 | std::this_thread::sleep_for(std::chrono::milliseconds(50)); 70 | 71 | REQUIRE(scanner->getDirCount() == 6); 72 | REQUIRE(scanner->getFileCount() == 14); 73 | 74 | dh.createDir("test3/test10"); 75 | dh.createFile("test3/test10/test2.txt"); 76 | dh.createFile("test3/test10/test3.txt"); 77 | dh.createDir("test3/test10/test2"); 78 | dh.createFile("test3/test10/test2/test1.txt"); 79 | dh.createFile("test3/test10/test2/test2.txt"); 80 | FilePath path("TestDir"); 81 | path.addDir("test3"); 82 | 83 | //wait for scanner to finish 84 | std::this_thread::sleep_for(std::chrono::milliseconds(60)); 85 | REQUIRE_FALSE(scanner->canPause()); 86 | REQUIRE_FALSE(scanner->canResume()); 87 | REQUIRE(scanner->getScanProgress() == 100); 88 | 89 | REQUIRE(scanner->getDirCount() == 8); 90 | REQUIRE(scanner->getFileCount() == 18); 91 | 92 | int64_t watchedNow, limit; 93 | bool exceeded = scanner->getWatcherLimits(watchedNow, limit); 94 | REQUIRE_FALSE(exceeded); 95 | if (limit >= 0) 96 | REQUIRE(watchedNow == scanner->getDirCount()); 97 | else 98 | REQUIRE(watchedNow == 1); 99 | 100 | dh.deleteDir("test3/test10"); 101 | std::this_thread::sleep_for(std::chrono::milliseconds(60)); 102 | scanner->getWatcherLimits(watchedNow, limit); 103 | if (limit >= 0) 104 | REQUIRE(watchedNow == scanner->getDirCount()); 105 | else 106 | REQUIRE(watchedNow == 1); 107 | } 108 | 109 | SECTION("Scan root") 110 | { 111 | auto roots = PlatformUtils::getAvailableMounts(); 112 | REQUIRE_FALSE(roots.empty()); 113 | std::unique_ptr scanner; 114 | for (auto &root : roots) { 115 | try { 116 | scanner = Utils::make_unique(root); 117 | break; 118 | } catch (std::runtime_error &e) { 119 | std::cout << "Can't scan path '" + root + "'!\n"; 120 | } 121 | } 122 | if (!scanner) { 123 | std::cout << "Root dir is not available, can't test root scanning!\n"; 124 | return; 125 | } 126 | 127 | REQUIRE(scanner->canPause()); 128 | REQUIRE_FALSE(scanner->canResume()); 129 | REQUIRE(scanner->isProgressKnown()); 130 | REQUIRE(scanner->hasChanges()); 131 | 132 | //wait for scanner to start scanning 133 | std::this_thread::sleep_for(std::chrono::milliseconds(50)); 134 | 135 | SECTION("Pause and stop running scan") 136 | { 137 | REQUIRE(scanner->canPause()); 138 | REQUIRE_FALSE(scanner->canResume()); 139 | REQUIRE(scanner->pauseScan()); 140 | REQUIRE_FALSE(scanner->canPause()); 141 | REQUIRE(scanner->canResume()); 142 | 143 | //wait for scanner to actually pause 144 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 145 | REQUIRE_FALSE(scanner->canPause()); 146 | REQUIRE(scanner->canResume()); 147 | auto curScanPath = scanner->getCurrentScanPath(); 148 | REQUIRE(curScanPath != nullptr); 149 | curScanPath = scanner->getCurrentScanPath(); 150 | REQUIRE(curScanPath != nullptr); 151 | 152 | REQUIRE(scanner->hasChanges()); 153 | auto &db = scanner->getFileDB(); 154 | auto root = scanner->getRootPath(); 155 | auto processed = db.processEntry(root, [](const FileEntry &entry) {}); 156 | REQUIRE(processed); 157 | REQUIRE_FALSE(scanner->hasChanges()); 158 | 159 | REQUIRE(scanner->getDirCount() > 0); 160 | REQUIRE(scanner->getFileCount() > 0); 161 | 162 | REQUIRE(scanner->resumeScan()); 163 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 164 | 165 | REQUIRE(scanner->canPause()); 166 | REQUIRE_FALSE(scanner->canResume()); 167 | scanner->stopScan(); 168 | REQUIRE_FALSE(scanner->canPause()); 169 | REQUIRE_FALSE(scanner->canResume()); 170 | REQUIRE(scanner->getScanProgress() == 100); 171 | 172 | int64_t used, available, total; 173 | scanner->getSpace(used, available, total); 174 | REQUIRE(used > 0); 175 | REQUIRE(available > 0); 176 | REQUIRE(total > 0); 177 | REQUIRE((total - available) >= used); 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /tests/SpaceWatcherTest.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "filepath.h" 5 | #include "spacewatcher.h" 6 | #include "platformutils.h" 7 | #include "DirHelper.h" 8 | 9 | #include 10 | 11 | TEST_CASE("SpaceWatcher basic events", "[watcher]") 12 | { 13 | std::unique_ptr watcher; 14 | 15 | SECTION("Can't watch not existing path") { 16 | REQUIRE_THROWS_AS(SpaceWatcher::create("not existing dir"), std::runtime_error); 17 | } 18 | 19 | //wait for watcher thread to initialize 20 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 21 | 22 | FilePath root("TestDir"); 23 | DirHelper dh(root.getPath()); 24 | 25 | SECTION("Create directory or file") 26 | { 27 | REQUIRE_NOTHROW(watcher = SpaceWatcher::create(root.getPath())); 28 | std::this_thread::sleep_for(std::chrono::milliseconds(40)); 29 | 30 | root.addDir("test"); 31 | dh.createDir("test"); 32 | watcher->addDir(root.getPath()); 33 | 34 | //wait for watcher thread to detect events 35 | std::this_thread::sleep_for(std::chrono::milliseconds(40)); 36 | auto ev = watcher->popEvent(); 37 | REQUIRE(ev != nullptr); 38 | 39 | if (ev->filepath.back() != PlatformUtils::filePathSeparator) 40 | ev->filepath.push_back(PlatformUtils::filePathSeparator); 41 | REQUIRE(root.getPath() == ev->filepath); 42 | root.goUp(); 43 | REQUIRE(root.getPath() == ev->parentpath); 44 | REQUIRE(ev->action == SpaceWatcher::FileAction::ADDED); 45 | REQUIRE(watcher->popEvent() == nullptr); 46 | 47 | dh.createFile("test/test.txt"); 48 | //wait for watcher thread to detect events 49 | std::this_thread::sleep_for(std::chrono::milliseconds(40)); 50 | 51 | ev = watcher->popEvent(); 52 | REQUIRE(ev != nullptr); 53 | root.addDir("test"); 54 | root.addFile("test.txt"); 55 | REQUIRE(root.getPath() == ev->filepath); 56 | root.goUp(); 57 | REQUIRE(root.getPath() == ev->parentpath); 58 | REQUIRE(ev->action == SpaceWatcher::FileAction::ADDED); 59 | REQUIRE(watcher->popEvent() == nullptr); 60 | } 61 | 62 | SECTION("Modify directory or file") 63 | { 64 | //create exising dir structure 65 | dh.createDir("test"); 66 | dh.createFile("test/test.txt"); 67 | 68 | REQUIRE_NOTHROW(watcher = SpaceWatcher::create(root.getPath())); 69 | root.addDir("test"); 70 | watcher->addDir(root.getPath()); 71 | std::this_thread::sleep_for(std::chrono::milliseconds(40)); 72 | 73 | SECTION("Delete") 74 | { 75 | dh.deleteDir("test"); 76 | 77 | //wait for watcher thread to detect events 78 | std::this_thread::sleep_for(std::chrono::milliseconds(40)); 79 | auto ev = watcher->popEvent(); 80 | REQUIRE(ev != nullptr); 81 | 82 | root.addFile("test.txt"); 83 | 84 | REQUIRE(root.getPath() == ev->filepath); 85 | root.goUp(); 86 | REQUIRE(root.getPath() == ev->parentpath); 87 | REQUIRE(ev->action == SpaceWatcher::FileAction::REMOVED); 88 | 89 | ev = watcher->popEvent(); 90 | 91 | REQUIRE(ev != nullptr); 92 | if (ev->filepath.back() != PlatformUtils::filePathSeparator) 93 | ev->filepath.push_back(PlatformUtils::filePathSeparator); 94 | 95 | REQUIRE(root.getPath() == ev->filepath); 96 | root.goUp(); 97 | REQUIRE(root.getPath() == ev->parentpath); 98 | REQUIRE(ev->action == SpaceWatcher::FileAction::REMOVED); 99 | } 100 | 101 | SECTION("Rename") 102 | { 103 | dh.rename("test/test.txt", "test/renamed_test.txt"); 104 | 105 | //wait for watcher thread to detect events 106 | std::this_thread::sleep_for(std::chrono::milliseconds(40)); 107 | auto ev = watcher->popEvent(); 108 | REQUIRE(ev != nullptr); 109 | 110 | root.addFile("test.txt"); 111 | 112 | REQUIRE(root.getPath() == ev->filepath); 113 | root.goUp(); 114 | REQUIRE(root.getPath() == ev->parentpath); 115 | REQUIRE(ev->action == SpaceWatcher::FileAction::OLD_NAME); 116 | 117 | ev = watcher->popEvent(); 118 | 119 | REQUIRE(ev != nullptr); 120 | 121 | root.addFile("renamed_test.txt"); 122 | REQUIRE(root.getPath() == ev->filepath); 123 | root.goUp(); 124 | REQUIRE(root.getPath() == ev->parentpath); 125 | REQUIRE(ev->action == SpaceWatcher::FileAction::NEW_NAME); 126 | } 127 | 128 | SECTION("Modify") 129 | { 130 | root.addFile("test.txt"); 131 | std::ofstream fs(root.getPath()); 132 | fs << "test\n"; 133 | fs.close(); 134 | 135 | //wait for watcher thread to detect events 136 | std::this_thread::sleep_for(std::chrono::milliseconds(40)); 137 | auto ev = watcher->popEvent(); 138 | REQUIRE(ev != nullptr); 139 | 140 | REQUIRE(root.getPath() == ev->filepath); 141 | root.goUp(); 142 | REQUIRE(root.getPath() == ev->parentpath); 143 | REQUIRE(ev->action == SpaceWatcher::FileAction::MODIFIED); 144 | } 145 | REQUIRE(watcher->popEvent() == nullptr); 146 | } 147 | } 148 | 149 | -------------------------------------------------------------------------------- /tests/UtilsTest.cpp: -------------------------------------------------------------------------------- 1 | #include "utils.h" 2 | 3 | #include 4 | 5 | TEST_CASE("Utility tests", "[utils]") 6 | { 7 | std::vector testArr = {1, 2, 40, 23, 22, 22}; 8 | 9 | REQUIRE(Utils::in_array(40, testArr)); 10 | REQUIRE(Utils::in_array(22, testArr)); 11 | REQUIRE_FALSE(Utils::in_array(30, testArr)); 12 | 13 | REQUIRE(Utils::clip(10, 10, 20) == 10); 14 | REQUIRE(Utils::clip(20, 10, 20) == 20); 15 | REQUIRE(Utils::clip(15, 10, 20) == 15); 16 | REQUIRE(Utils::clip(5, 10, 20) == 10); 17 | REQUIRE(Utils::clip(25, 10, 20) == 20); 18 | 19 | int64_t size = 54; 20 | REQUIRE(Utils::formatSize(size) == "54.0 B"); 21 | size = 13; 22 | REQUIRE(Utils::formatSize(size * 1024) == "13.0 KiB"); 23 | size = 25; 24 | REQUIRE(Utils::formatSize(size * 1024 * 1024) == "25.0 MiB"); 25 | size = 90; 26 | REQUIRE(Utils::formatSize(size * 1024 * 1024 * 1024) == "90.0 GiB"); 27 | 28 | REQUIRE(Utils::roundFrac(10, 2) == 5); 29 | REQUIRE(Utils::roundFrac(11, 2) == 6); 30 | REQUIRE(Utils::roundFrac(10, 3) == 3); 31 | REQUIRE(Utils::roundFrac(11, 3) == 4); 32 | REQUIRE(Utils::roundFrac(50, 7) == 7); 33 | REQUIRE(Utils::roundFrac(51, 7) == 7); 34 | REQUIRE(Utils::roundFrac(52, 7) == 7); 35 | REQUIRE(Utils::roundFrac(53, 7) == 8); 36 | REQUIRE(Utils::roundFrac(54, 7) == 8); 37 | REQUIRE(Utils::roundFrac(55, 7) == 8); 38 | 39 | } 40 | --------------------------------------------------------------------------------