├── .gitignore ├── Makefile ├── README.md ├── duck.c ├── duck.h ├── dui.h ├── duil.c ├── duiw.c ├── imgs └── duck.gif └── main.c /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | duck.exe -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ifneq ($(OS), Windows_NT) 2 | SRC = main.c duck.c duil.c 3 | FLAGS = -lncurses -lm 4 | INSTALL_DIR = /usr/local/bin 5 | else 6 | SRC = main.c duck.c duiw.c 7 | endif 8 | 9 | all: $(SRC) 10 | gcc -o duck $^ $(FLAGS) 11 | 12 | install: all 13 | @echo "Installing duck to $(INSTALL_DIR)" 14 | @install -m 0755 duck $(INSTALL_DIR) 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ____ _ _ ____ _ __ 2 | | _ \| | | |/ ___| |/ / 3 | | | | | | | | | | ' / 4 | | |_| | |_| | |___| . \ 5 | |____/ \___/ \____|_|\_\ 6 | 7 | duck - Disk usage analysis tool & file explorer. 8 | 9 | ## About 10 | 11 | DUCK is a cross-platform interactive terminal-based disk analysis tool and file explorer designed for efficiency and ease of use. It provides a visually intuitive way to analyze disk space usage and navigate directories directly from the terminal. 12 | 13 |
14 | 15 | 16 | 17 | ## Installation 18 | 19 | **To install duck on Windows**: 20 | 21 | Download the .exe from the release section or build it using ```make``` 22 | 23 | **To install duck on Linux**: 24 | 25 | ```bash 26 | make && sudo make install 27 | ``` 28 | 29 | This will compile the program and install it to `/usr/local/bin`. 30 | 31 | ## Usage 32 | 33 | ``` 34 | duck [options] 35 | 36 | Options: 37 | -b, --benchmark Benchmark execution 38 | -c, --count Show item count 39 | -e, --exclude [extensions] Exclude files with specified extensions 40 | -i, --include [extensions] Include only files with specified extensions 41 | -h, --help Print this info 42 | -hf, --hidden Ignore hidden files. 43 | -s, --sort Sort by (size is default) 44 | methods: 45 | ``` 46 | 47 | ## Performance 48 | Running duck on a **100GB** directory with **900k files and 100k directories** took around **5sec (with a warm cache)** / **20sec (with a cold cache)** and used ~300mb of memory. 49 | 50 | Run on Windows 11 24H2 machine with Ryzen 9 7940HS and the drive is located on an SSD. Performance may very according to the machine duck is run on. 51 | 52 | ### Warm cache 53 | ``` 54 | Files: 916529 Directories: 109097 55 | Discovery time: 5.025 sec 56 | Total time: 5.025 sec 57 | 58 | 111.18 GB ~/ 59 | 45.11 GB 40.58% [██████ ] Qt/ 60 | 28.00 GB 25.19% [████ ] Xilinx/ 61 | 13.16 GB 11.84% [██ ] Microsoft Visual Studio/ 62 | 4.65 GB 4.18% [█ ] CLion/ 63 | 4.56 GB 4.10% [█ ] JetBrains Rider 2024.1.1/ 64 | 3.91 GB 3.51% [█ ] intelFPGA/ 65 | 3.47 GB 3.12% [ ] IntelliJ IDEA 2024.1.1/ 66 | 2.71 GB 2.43% [ ] PyCharm Professional/ 67 | 2.58 GB 2.32% [ ] Android/ 68 | 1.93 GB 1.73% [ ] DataGrip/ 69 | 499.33 MB 0.44% [ ] Arduino/ 70 | 418.72 MB 0.37% [ ] Microsoft VS Code/ 71 | 148.13 MB 0.13% [ ] Logisim Evolution/ 72 | 33.35 MB 0.03% [ ] ollydbg/ 73 | 10.10 MB 0.01% [ ] AIMSpice/ 74 | 4.48 MB 0.00% [ ] npp/ 75 | 4.00 MB 0.00% [ ] masm_minimal/ 76 | 2.39 MB 0.00% [ ] nasm-2.16.01/ 77 | ``` 78 | 79 | ### Cold cache 80 | ``` 81 | Files: 916529 Directories: 109097 82 | Discovery time: 21.198 sec 83 | Total time: 21.198 sec 84 | 85 | 111.18 GB ~/ 86 | 45.11 GB 40.58% [██████ ] Qt/ 87 | 28.00 GB 25.19% [████ ] Xilinx/ 88 | 13.16 GB 11.84% [██ ] Microsoft Visual Studio/ 89 | 4.65 GB 4.18% [█ ] CLion/ 90 | 4.56 GB 4.10% [█ ] JetBrains Rider 2024.1.1/ 91 | 3.91 GB 3.51% [█ ] intelFPGA/ 92 | 3.47 GB 3.12% [ ] IntelliJ IDEA 2024.1.1/ 93 | 2.71 GB 2.43% [ ] PyCharm Professional/ 94 | 2.58 GB 2.32% [ ] Android/ 95 | 1.93 GB 1.73% [ ] DataGrip/ 96 | 499.33 MB 0.44% [ ] Arduino/ 97 | 418.72 MB 0.37% [ ] Microsoft VS Code/ 98 | 148.13 MB 0.13% [ ] Logisim Evolution/ 99 | 33.35 MB 0.03% [ ] ollydbg/ 100 | 10.10 MB 0.01% [ ] AIMSpice/ 101 | 4.48 MB 0.00% [ ] npp/ 102 | 4.00 MB 0.00% [ ] masm_minimal/ 103 | 2.39 MB 0.00% [ ] nasm-2.16.01/ 104 | ``` 105 | 106 | ## Development 107 | 108 | External dependencies are kept to a minimum. The windows version uses only the win32 api with no additional dependencies, while the linux version uses [ncurses](https://invisible-island.net/ncurses/) to easily manage the terminal. 109 | 110 | To build duck use the provided makefile. 111 | 112 | The project is modular (only the terminal ui is platform specifc), `duiw.c` contains the UI implementation for win32 platforms and `duil.c` contains the UI implementation for unix based platforms. 113 | -------------------------------------------------------------------------------- /duck.c: -------------------------------------------------------------------------------- 1 | #include "duck.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #ifdef __unix__ 9 | #ifndef _DEFAULT_SOURCE 10 | #define _DEFAULT_SOURCE 11 | #endif 12 | 13 | #include 14 | 15 | #include 16 | #include 17 | #endif 18 | 19 | benchmark bench = {0, 0, 0, 0}; 20 | 21 | dirtree *dirtree_alloc(const char *name, enum filetype type, dirtree *parent) 22 | { 23 | dirtree *node = calloc(1, sizeof(dirtree)); 24 | 25 | strcpy(node->desc.name, name); 26 | node->desc.length = strlen(name); 27 | node->desc.type = type; 28 | node->desc.hidden = 0; 29 | node->selected_file = 0; 30 | 31 | node->nfiles = 0; 32 | node->size = 0; 33 | 34 | node->capacity = 20; 35 | node->files = NULL; 36 | node->parent = parent; 37 | 38 | return node; 39 | } 40 | 41 | void dirtree_insert(dirtree *root, dirtree* node) 42 | { 43 | if(root->files == NULL) 44 | root->files = calloc(root->capacity, sizeof(dirtree*)); 45 | 46 | if(root->nfiles >= root->capacity) 47 | { 48 | root->capacity *= 2; 49 | root->files = realloc(root->files, root->capacity * sizeof(dirtree*)); 50 | } 51 | 52 | root->files[root->nfiles] = node; 53 | root->size += node->size; 54 | 55 | root->nfiles++; 56 | } 57 | 58 | void dirtree_sort(dirtree *root, int (*comparator)(const void*, const void*)) 59 | { 60 | if(root == NULL) 61 | return; 62 | 63 | qsort(root->files, root->nfiles, sizeof(dirtree*), comparator); 64 | 65 | for(int i = 0; i < root->nfiles; i++) 66 | dirtree_sort(root->files[i], comparator); 67 | } 68 | 69 | 70 | int dirtree_down(dirtree **root) 71 | { 72 | if((*root)->files[(*root)->selected_file]->desc.type == DDIRECTORY) 73 | { 74 | *root = (*root)->files[(*root)->selected_file]; 75 | return 1; 76 | } 77 | 78 | return 0; 79 | } 80 | 81 | int dirtree_up(dirtree **root) 82 | { 83 | if((*root)->parent != NULL) 84 | { 85 | *root = (*root)->parent; 86 | return 1; 87 | } 88 | 89 | return 0; 90 | } 91 | 92 | 93 | int dirtree_select_next_file(dirtree *root) 94 | { 95 | if(root->selected_file < root->nfiles - 1) 96 | { 97 | root->selected_file++; 98 | return 1; 99 | } 100 | 101 | return 0; 102 | } 103 | 104 | int dirtree_select_prev_file(dirtree *root) 105 | { 106 | if(root->selected_file > 0) 107 | { 108 | root->selected_file--; 109 | return 1; 110 | } 111 | 112 | return 0; 113 | } 114 | 115 | 116 | int dirtree_comp_size(const void *a, const void *b) 117 | { 118 | dirtree *aa = *((dirtree**)a); 119 | dirtree *bb = *((dirtree**)b); 120 | 121 | // Return like this instead of bb->size - aa->size 122 | // because the comparator must return an int 123 | // but dirtree.size is size_t aka unsigned long long 124 | if(bb->size > aa->size) 125 | return 1; 126 | else if(bb->size < aa->size) 127 | return -1; 128 | else 129 | return 0; 130 | } 131 | 132 | int dirtree_comp_alpha(const void *a, const void *b) 133 | { 134 | dirtree *aa = *((dirtree**)a); 135 | dirtree *bb = *((dirtree**)b); 136 | 137 | return strcmp(aa->desc.name, bb->desc.name); 138 | } 139 | 140 | int dirtree_comp_items(const void *a, const void *b) 141 | { 142 | dirtree *aa = *((dirtree**)a); 143 | dirtree *bb = *((dirtree**)b); 144 | 145 | return bb->nfiles - aa->nfiles; 146 | } 147 | 148 | void size(double size, char *out) 149 | { 150 | int m = 0; 151 | while(size > 1000) 152 | { 153 | size /= 1024.0; 154 | m++; 155 | } 156 | 157 | if(m == 0) 158 | sprintf(out, "%3.2lf B", size); 159 | else 160 | sprintf(out, "%3.2lf %cB", size, magnitudes[m - 1]); 161 | } 162 | 163 | int dirtree_getpath(dirtree *tree, char *path) 164 | { 165 | if(tree == NULL) 166 | return 0; 167 | 168 | int l = dirtree_getpath(tree->parent, path); 169 | 170 | if(tree->parent == NULL) 171 | strcpy(path, tree->desc.name); 172 | else 173 | strcat(path, tree->desc.name); 174 | strcat(path, "/"); 175 | 176 | return l + tree->desc.length + 1; 177 | } 178 | 179 | /** 180 | * Applies filter options on the given file. 181 | * @return 0 if the file should be skipped. 182 | */ 183 | int filter(dirtree *node, duckoptions options) 184 | { 185 | if(options.hide && node->desc.hidden) 186 | return 0; 187 | 188 | if(options.nexts) 189 | { 190 | if(options.include && node->desc.type == DDIRECTORY) 191 | return node->nfiles > 0; 192 | 193 | if(node->desc.type == DDIRECTORY && node->nfiles == 0) 194 | return 0; 195 | 196 | int ext_in_list = 0; 197 | const char *ext = strrchr(node->desc.name, '.'); 198 | 199 | if(ext != NULL) 200 | { 201 | for(int i = 0; i < options.nexts; i++) 202 | { 203 | if(strcmp(ext + 1, options.extenstions[i]) == 0) 204 | { 205 | ext_in_list = 1; 206 | break; 207 | } 208 | } 209 | } 210 | 211 | if(options.include && !ext_in_list) 212 | return 0; 213 | 214 | if(options.exclude && ext_in_list) 215 | return 0; 216 | } 217 | 218 | return 1; 219 | } 220 | 221 | #ifdef __unix__ 222 | /** 223 | * Walk the given directory and builds the resulted tree. 224 | * Returns the size of the directory. 225 | * UNIX IMPLEMENTATION 226 | */ 227 | size_t dir_walk(const char *dir, dirtree *tree, duckoptions options) 228 | { 229 | char path[MAX_PATH]; 230 | 231 | // Return if the current path is invalid 232 | DIR *d = opendir(dir); 233 | if(d == NULL) 234 | return 0; 235 | 236 | struct dirent *dent; 237 | struct stat fstat; 238 | 239 | while((dent = readdir(d)) != NULL) 240 | { 241 | // Skip if the current director is . or .. 242 | if(strcmp(dent->d_name, ".") == 0 || strcmp(dent->d_name, "..") == 0) 243 | continue; 244 | 245 | int isDirectory = dent->d_type == 4; //DT_DIR 246 | 247 | snprintf(path, MAX_PATH, "%s/%s", dir, dent->d_name); 248 | 249 | stat(path, &fstat); 250 | 251 | dirtree *n = dirtree_alloc(dent->d_name, isDirectory ? DDIRECTORY : DFILE, tree); 252 | n->size = fstat.st_size; 253 | 254 | if(isDirectory) 255 | n->size = dir_walk(path, n, options); 256 | 257 | if(!filter(n, options)) 258 | continue; 259 | 260 | dirtree_insert(tree, n); 261 | 262 | BENCHMARK_INC_FILES(bench, isDirectory); 263 | } 264 | 265 | closedir(d); 266 | 267 | return tree->size; 268 | } 269 | #else 270 | /** 271 | * Walk the given directory and builds the resulted tree. 272 | * Returns the size of the directory. 273 | * WIN32 IMPLEMENTATION 274 | */ 275 | size_t dir_walk(const char *dir, dirtree *tree, duckoptions options) 276 | { 277 | WIN32_FIND_DATA find_data; 278 | HANDLE hFind = NULL; 279 | 280 | char path[MAX_PATH]; 281 | sprintf(path, "%s\\*", dir); 282 | 283 | if((hFind = FindFirstFile(path, &find_data)) == INVALID_HANDLE_VALUE) 284 | return 0; 285 | 286 | do 287 | { 288 | // Skip if the current director is . or .. 289 | if(strcmp(find_data.cFileName, ".") == 0 || strcmp(find_data.cFileName, "..") == 0) 290 | continue; 291 | 292 | int isDirectory = find_data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY; 293 | 294 | dirtree *n = dirtree_alloc(find_data.cFileName, isDirectory ? DDIRECTORY : DFILE, tree); 295 | n->size = find_data.nFileSizeLow; 296 | n->desc.hidden = find_data.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN; 297 | 298 | snprintf(path, MAX_PATH, "%s\\%s", dir, find_data.cFileName); 299 | 300 | // If the file is a directory recurse on it 301 | if(isDirectory) 302 | n->size = dir_walk(path, n, options); 303 | 304 | // if(!filter(find_data.cFileName, isDirectory, find_data.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN, options)) 305 | if(!filter(n, options)) 306 | continue; 307 | 308 | dirtree_insert(tree, n); 309 | 310 | BENCHMARK_INC_FILES(bench, isDirectory); 311 | 312 | } while (FindNextFile(hFind, &find_data)); 313 | 314 | FindClose(hFind); 315 | 316 | return tree->size; 317 | } 318 | #endif 319 | 320 | void build_dirtree(dirtree *tree, const char *path, duckoptions options) 321 | { 322 | clock_t begin, end; 323 | 324 | begin = clock(); 325 | 326 | dir_walk(path, tree, options); 327 | 328 | end = clock(); 329 | bench.dtime = (double)(end - begin) / CLOCKS_PER_SEC; 330 | 331 | switch(options.sort) 332 | { 333 | case DSIZE: 334 | dirtree_sort(tree, &dirtree_comp_size); 335 | break; 336 | 337 | case DALPHABETIC: 338 | dirtree_sort(tree, &dirtree_comp_alpha); 339 | break; 340 | 341 | case DITEMS: 342 | dirtree_sort(tree, &dirtree_comp_items); 343 | break; 344 | } 345 | 346 | end = clock(); 347 | bench.ttime = (double)(end - begin) / CLOCKS_PER_SEC; 348 | } -------------------------------------------------------------------------------- /duck.h: -------------------------------------------------------------------------------- 1 | #ifndef DUCK_H 2 | #define DUCK_H 3 | 4 | #include 5 | 6 | #ifdef __unix__ 7 | #include 8 | #include 9 | 10 | #define MAX_PATH PATH_MAX 11 | #else 12 | #include 13 | #endif 14 | 15 | enum filetype 16 | { 17 | DFILE, 18 | DDIRECTORY 19 | }; 20 | 21 | static const char *magnitudes = "KMGT"; 22 | 23 | 24 | typedef struct duckoptions 25 | { 26 | int benchmark; 27 | int hide; 28 | 29 | enum sortmethod 30 | { 31 | DSIZE, 32 | DALPHABETIC, 33 | DITEMS 34 | } sort; 35 | 36 | int include; 37 | int exclude; 38 | int nexts; 39 | char extenstions[20][20]; 40 | 41 | } duckoptions; 42 | 43 | 44 | /** 45 | * Benchmark result data 46 | */ 47 | typedef struct benchmark 48 | { 49 | double ttime; // total time 50 | double dtime; // discovery time 51 | int files; 52 | int directories; 53 | } benchmark; 54 | 55 | #define BENCHMARK_LINES 4 56 | #define BENCHMARK_INC_FILES(bench, dir) if(dir) \ 57 | bench.directories++; \ 58 | else \ 59 | bench.files++; 60 | 61 | extern benchmark bench; 62 | 63 | 64 | typedef struct dirtree_desc 65 | { 66 | enum filetype type; 67 | int hidden; 68 | char name[MAX_PATH]; 69 | uint8_t length; 70 | } dirtree_desc; 71 | 72 | /** 73 | * Node of the directory tree 74 | */ 75 | typedef struct dirtree 76 | { 77 | struct dirtree_desc desc; 78 | 79 | /** 80 | * Selected (highlighted) file. 81 | * When going down a level this will be the opened directory 82 | */ 83 | uint32_t selected_file; 84 | 85 | /** 86 | * Size of the current file / directory in bytes; 87 | */ 88 | size_t size; 89 | 90 | /** 91 | * Number of files / directories contained 92 | */ 93 | uint32_t nfiles; 94 | 95 | uint32_t capacity; 96 | struct dirtree **files; 97 | 98 | struct dirtree *parent; 99 | } dirtree; 100 | 101 | dirtree* dirtree_alloc(const char* name, enum filetype type, dirtree *parent); 102 | 103 | void dirtree_insert(dirtree *root, dirtree* node); 104 | void dirtree_sort(dirtree *root, int (*comparator)(const void*, const void*)); 105 | 106 | /** 107 | * Go down one level. Open the selected directory 108 | */ 109 | int dirtree_down(dirtree **root); 110 | 111 | /** 112 | * Go up one level. Navigate back. 113 | */ 114 | int dirtree_up(dirtree **root); 115 | 116 | int dirtree_select_next_file(dirtree *root); 117 | int dirtree_select_prev_file(dirtree *root); 118 | 119 | /** 120 | * Returns the path from root for the given file (dritree node). 121 | * @returns The length of path 122 | */ 123 | int dirtree_getpath(dirtree *tree, char *path); 124 | 125 | /** 126 | * Transforms the given size in bytes to kb/mb/gb 127 | * to respect the following format %3.2d 128 | */ 129 | void size(double size, char* out); 130 | 131 | /** 132 | * Recursively traverse all the files and directory starting at path 133 | * and build the output directory tree 134 | */ 135 | void build_dirtree(dirtree *tree, const char *path, duckoptions options); 136 | 137 | #endif -------------------------------------------------------------------------------- /dui.h: -------------------------------------------------------------------------------- 1 | #ifndef DUI_H 2 | #define DUI_H 3 | 4 | #include "duck.h" 5 | 6 | #include 7 | #include 8 | 9 | #ifdef __unix__ 10 | #include 11 | 12 | #define DUCK_DOWN KEY_DOWN 13 | #define DUCK_UP KEY_UP 14 | #define DUCK_QUIT 'q' 15 | #define DUCK_ENTER 10 16 | #define DUCK_BACK KEY_BACKSPACE 17 | #else 18 | #include 19 | 20 | #define DUCK_DOWN 80 21 | #define DUCK_UP 72 22 | #define DUCK_QUIT 'q' 23 | #define DUCK_ENTER 13 24 | #define DUCK_BACK 8 25 | #endif 26 | 27 | #define CLEAR_ALL 0x01 28 | #define CLEAR_ATTRIBUTES 0x02 29 | #define CLEAR_CURSOR_OFFSET 0x04 30 | #define CLEAR_END 0x08 31 | 32 | static const char headerMsg[] = " duck ~ Use arrow keys to navigate, backspace - go to parent directory, enter - go to child directory"; 33 | 34 | static const char loadChars[16] = {219, 219, 219, 219, 219, 219, 219, 219, 219, 219, 219, 219, 219, 219, 219, 0}; 35 | 36 | typedef struct duioptions 37 | { 38 | /** 39 | * Fullscreen mode 40 | * (enabled) Clears the terminal before printing 41 | * (disabled) Doesn't clear the terminal, the output is printed normally 42 | */ 43 | int fullscreen; 44 | 45 | /** 46 | * Y coord to start drawing UI from 47 | */ 48 | int y; 49 | 50 | /** 51 | * Show item count 52 | */ 53 | int count; 54 | } duioptions; 55 | 56 | void dui_init(duioptions options); 57 | void dui_print(dirtree *tree); 58 | 59 | void dui_header(); 60 | 61 | static inline void dui_benchmark_print(benchmark bench) 62 | { 63 | #ifdef __unix__ 64 | printw("Files: %d Directories: %d\n", bench.files, bench.directories); 65 | printw("Discovery time: %.3lf sec\n", bench.dtime); 66 | printw("Total time: %.3lf sec\n", bench.dtime); 67 | #else 68 | COORD coord = {0, 1}; 69 | SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), coord); 70 | printf("Files: %d Directories: %d\n", bench.files, bench.directories); 71 | printf("Discovery time: %.3lf sec\n", bench.dtime); 72 | printf("Total time: %.3lf sec\n", bench.dtime); 73 | #endif 74 | } 75 | 76 | static inline void dui_tree_root_to_string(dirtree *root, char *str, duioptions options) 77 | { 78 | static char sz[10]; 79 | char path[300] = ""; 80 | dirtree_getpath(root, path); 81 | 82 | size((double)root->size, sz); 83 | 84 | if(options.count) 85 | sprintf(str, "%9s items: %6d %s", sz, root->nfiles, path); 86 | else 87 | sprintf(str, "%9s %s", sz, path); 88 | } 89 | 90 | static inline void dui_tree_child_to_string(dirtree *node, dirtree *parent, char *str, duioptions options) 91 | { 92 | static char sz[10]; 93 | 94 | // Space percentage for the current file 95 | double percent = (node->size * 1.0) / parent->size; 96 | 97 | size((double)node->size, sz); 98 | 99 | if(options.count) 100 | sprintf(str, "%9s %5.2lf%% items: %6d [%-15.*s] %s%c", sz, percent * 100, node->nfiles, (int)round(percent * 15), loadChars, node->desc.name, node->desc.type == DDIRECTORY ? '/' : ' '); 101 | else 102 | sprintf(str, "%9s %5.2lf%% [%-15.*s] %s%c", sz, percent * 100, (int)round(percent * 15), loadChars, node->desc.name, node->desc.type == DDIRECTORY ? '/' : ' '); 103 | } 104 | 105 | /** 106 | * Clears the part of the console where output was printed. (The whole console for 107 | * fullscreen mode and only the used part when in non fullscreen mode) 108 | * 109 | * CLEAR_ALL - clears the whole screen by flooding with spaces ' ' and removing color attributes 110 | * 111 | * CLEAR_ATTRIBUTES - clears only the color attributes 112 | * 113 | * @param mode CLEAR_ALL or CLEAR_ATTRIBUTES 114 | */ 115 | void dui_clear(int mode); 116 | 117 | void dui_end(); 118 | 119 | /** 120 | * Moves the cursor down. Returns the clear method that needs to be used 121 | */ 122 | int dui_scroll_down(); 123 | 124 | /** 125 | * Moves the cursor up. Returns the clear method that needs to be used 126 | */ 127 | int dui_scroll_up(); 128 | 129 | #endif -------------------------------------------------------------------------------- /duil.c: -------------------------------------------------------------------------------- 1 | /** 2 | * DUCK Terminal UI implementation for unix based system 3 | */ 4 | 5 | #include "dui.h" 6 | 7 | #ifdef __unix__ 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | static int scrollOffset = 0; 20 | static int cursor; 21 | static int curfetch; 22 | 23 | static duioptions options; 24 | 25 | /** 26 | * Sets the cursor position if the cursor fetch flag is activated. 27 | */ 28 | void cursor_update(int pos) 29 | { 30 | if(!curfetch) 31 | return; 32 | 33 | cursor = MIN(pos + options.y, LINES); 34 | scrollOffset = MAX(0, pos + options.y - LINES); 35 | 36 | curfetch = 0; 37 | } 38 | 39 | void dui_init(duioptions doptions) 40 | { 41 | options = doptions; 42 | cursor = 1 + options.y; 43 | 44 | initscr(); 45 | 46 | if(has_colors()) 47 | { 48 | use_default_colors(); 49 | start_color(); 50 | init_pair(1, -1, COLOR_GREEN); 51 | init_pair(2, COLOR_WHITE, COLOR_BLUE); 52 | } 53 | 54 | noecho(); 55 | keypad(stdscr, TRUE); 56 | curs_set(0); 57 | scrollok(stdscr, TRUE); 58 | } 59 | 60 | void dui_header() 61 | { 62 | attron(COLOR_PAIR(2)); 63 | printw("%s", headerMsg); 64 | attroff(COLOR_PAIR(2)); 65 | } 66 | 67 | void dui_print(dirtree *tree) 68 | { 69 | if(tree == NULL) 70 | return; 71 | 72 | /** 73 | * Sets the cursor position to be on the same line with 74 | * the highlighted file when navigating up or down the directory tree 75 | */ 76 | cursor_update(tree->selected_file + 1); 77 | 78 | char out[310] = ""; 79 | char sz[10]; 80 | 81 | dui_tree_root_to_string(tree, out, options); 82 | mvprintw(options.y, 0, "%s", out); 83 | 84 | for(int i = 0 + scrollOffset; i < tree->nfiles; i++) 85 | { 86 | dirtree *d = tree->files[i]; 87 | 88 | // Size percentage for the current file 89 | double percent = (d->size * 1.0) / tree->size; 90 | 91 | size((double)d->size, sz); 92 | 93 | // Change console text color to highlight this file if selected 94 | if(i == tree->selected_file) 95 | attron(COLOR_PAIR(1)); 96 | 97 | dui_tree_child_to_string(d, tree, out, options); 98 | mvprintw(options.y + i - scrollOffset + 1, 0, "%s", out); 99 | 100 | // Disable color 101 | attroff(COLOR_PAIR(1)); 102 | } 103 | } 104 | 105 | void dui_clear(int mode) 106 | { 107 | if(mode & CLEAR_ALL) 108 | { 109 | move(options.y, 0); 110 | clrtobot(); 111 | } 112 | 113 | if(mode & CLEAR_CURSOR_OFFSET) 114 | { 115 | cursor = options.y + 1; 116 | scrollOffset = 0; 117 | 118 | // Enable the cursor fetch flag so it can be set on the next call of dui_print 119 | curfetch = 1; 120 | } 121 | } 122 | 123 | void dui_end() 124 | { 125 | curs_set(1); 126 | endwin(); 127 | } 128 | 129 | int dui_scroll_down() 130 | { 131 | if(cursor < LINES - 1) 132 | { 133 | // When the cursor is on the last line scroll 134 | // by adding an offset when printing 135 | // Also we need to clear the whole console when doing this to avoid 136 | // artifacts, as lines can now be of different length 137 | cursor++; 138 | return CLEAR_ATTRIBUTES; 139 | } 140 | else 141 | { 142 | scrollOffset++; 143 | return CLEAR_ALL; 144 | } 145 | } 146 | 147 | int dui_scroll_up() 148 | { 149 | if(cursor + scrollOffset > LINES - 1) 150 | { 151 | // When the cursor is on the last line scroll 152 | // by adding an offset when printing 153 | // Also we need to clear the whole console when doing this to avoid 154 | // artifacts, as lines can now be of different length 155 | scrollOffset--; 156 | return CLEAR_ALL; 157 | } 158 | else 159 | { 160 | cursor--; 161 | return CLEAR_ATTRIBUTES; 162 | } 163 | } 164 | 165 | #endif -------------------------------------------------------------------------------- /duiw.c: -------------------------------------------------------------------------------- 1 | /** 2 | * DUCK Terminal UI implementation for win32 system 3 | */ 4 | 5 | #include "dui.h" 6 | 7 | #ifndef __unix__ 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #include 15 | 16 | static int scrollOffset = 0; 17 | static int cursor; 18 | static int curfetch; 19 | 20 | static duioptions options; 21 | 22 | /** 23 | * Attributes used to set console text color. 24 | * Size set to MAX_PATH (260) - the maximum size of a file/directory name is 260 characters on WIN32 systems 25 | */ 26 | static WORD colorAttributes[MAX_PATH]; 27 | 28 | static HANDLE hConsole; 29 | static CONSOLE_SCREEN_BUFFER_INFO csbi; 30 | 31 | /** 32 | * Sets the cursor position if the cursor fetch flag is activated. 33 | */ 34 | void cursor_update(int pos) 35 | { 36 | if(!curfetch) 37 | return; 38 | 39 | cursor = min(pos + options.y, csbi.dwMaximumWindowSize.Y); 40 | scrollOffset = max(0, pos + options.y - csbi.dwMaximumWindowSize.Y); 41 | 42 | curfetch = 0; 43 | } 44 | 45 | void dui_init(duioptions doptions) 46 | { 47 | options = doptions; 48 | 49 | system("cls"); 50 | 51 | // Initialize the color attributes 52 | for(int i = 0; i < MAX_PATH; i++) 53 | colorAttributes[i] = BACKGROUND_GREEN | FOREGROUND_RED | FOREGROUND_BLUE | FOREGROUND_GREEN;; 54 | 55 | hConsole = GetStdHandle(STD_OUTPUT_HANDLE); 56 | GetConsoleScreenBufferInfo(hConsole, &csbi); 57 | 58 | cursor = csbi.dwCursorPosition.Y + options.y + 1; 59 | 60 | // Disable the cursor 61 | CONSOLE_CURSOR_INFO cci; 62 | GetConsoleCursorInfo(hConsole, &cci); 63 | 64 | cci.bVisible = 0; 65 | SetConsoleCursorInfo(hConsole, &cci); 66 | } 67 | 68 | void dui_header() 69 | { 70 | COORD coord = {0, 0}; 71 | DWORD bytes; 72 | 73 | WORD *background = (WORD*)malloc(csbi.dwMaximumWindowSize.X * sizeof(WORD)); 74 | for(int i = 0; i < csbi.dwMaximumWindowSize.X; i++) 75 | background[i] = BACKGROUND_BLUE | FOREGROUND_RED | FOREGROUND_BLUE | FOREGROUND_GREEN; 76 | 77 | WriteConsoleOutputAttribute(hConsole, background, csbi.dwMaximumWindowSize.X, coord, &bytes); 78 | 79 | WriteConsoleOutputCharacter(hConsole, headerMsg, strlen(headerMsg), coord, &bytes); 80 | } 81 | 82 | void dui_print(dirtree *tree) 83 | { 84 | if(tree == NULL) 85 | return; 86 | 87 | /** 88 | * Sets the cursor position to be on the same line with 89 | * the highlighted file when navigating up or down the directory tree 90 | */ 91 | cursor_update(tree->selected_file + 1); 92 | 93 | DWORD bytes; 94 | COORD coord = {0, options.y}; 95 | 96 | char out[300] = ""; 97 | 98 | // Print the size and current director path on the first line 99 | dui_tree_root_to_string(tree, out, options); 100 | 101 | int len = strlen(out); 102 | WriteConsoleOutputCharacterA(hConsole, out, len, coord, &bytes); 103 | 104 | for(int i = 0 + scrollOffset; i < tree->nfiles; i++) 105 | { 106 | dirtree *d = tree->files[i]; 107 | 108 | coord.Y = options.y + (short)i - scrollOffset + 1; 109 | 110 | dui_tree_child_to_string(d, tree, out, options); 111 | 112 | len = strlen(out); 113 | 114 | // Change console text color to highlight this file if selected 115 | if(i == tree->selected_file) 116 | WriteConsoleOutputAttribute(hConsole, colorAttributes, len, coord, &bytes); 117 | 118 | WriteConsoleOutputCharacterA(hConsole, out, len, coord, &bytes); 119 | } 120 | } 121 | 122 | void dui_clear(int mode) 123 | { 124 | COORD coord = {0, options.y}; 125 | DWORD bytes; 126 | 127 | if(mode & CLEAR_ALL) 128 | { 129 | FillConsoleOutputCharacter(hConsole, TEXT(' '), csbi.dwSize.X * csbi.dwSize.Y, coord, &bytes); 130 | FillConsoleOutputAttribute(hConsole, csbi.wAttributes, csbi.dwSize.X * csbi.dwSize.Y, coord, &bytes); 131 | } 132 | 133 | if(mode & CLEAR_END) 134 | { 135 | coord.Y = 0; 136 | FillConsoleOutputCharacter(hConsole, TEXT(' '), csbi.dwSize.X * csbi.dwSize.Y, coord, &bytes); 137 | FillConsoleOutputAttribute(hConsole, csbi.wAttributes, csbi.dwSize.X * csbi.dwSize.Y, coord, &bytes); 138 | } 139 | 140 | if (mode & CLEAR_CURSOR_OFFSET) 141 | { 142 | cursor = csbi.dwCursorPosition.Y + options.y + 1; 143 | scrollOffset = 0; 144 | 145 | // Enable the cursor fetch flag so it can be set on the next call of dui_print 146 | curfetch = 1; 147 | } 148 | 149 | if(mode & CLEAR_ATTRIBUTES) 150 | { 151 | FillConsoleOutputAttribute(hConsole, csbi.wAttributes, csbi.dwSize.X * csbi.dwSize.Y, coord, &bytes); 152 | } 153 | } 154 | 155 | void dui_end() 156 | { 157 | dui_clear(CLEAR_ALL); 158 | CONSOLE_CURSOR_INFO cci; 159 | GetConsoleCursorInfo(hConsole, &cci); 160 | 161 | cci.bVisible = 1; 162 | SetConsoleCursorInfo(hConsole, &cci); 163 | 164 | COORD coord = {0, 0}; 165 | SetConsoleCursorPosition(hConsole, coord); 166 | } 167 | 168 | int dui_scroll_down() 169 | { 170 | if(cursor < csbi.dwMaximumWindowSize.Y - 1) 171 | { 172 | cursor++; 173 | return CLEAR_ATTRIBUTES; 174 | } 175 | else 176 | { 177 | // When the cursor is on the last line scroll 178 | // by adding an offset when printing 179 | // Also we need to clear the whole console when doing this to avoid 180 | // artifacts, as lines can now be of different length 181 | scrollOffset++; 182 | return CLEAR_ALL; 183 | } 184 | } 185 | 186 | int dui_scroll_up() 187 | { 188 | if(cursor + scrollOffset > csbi.dwMaximumWindowSize.Y - 1) 189 | { 190 | // When the cursor is on the last line scroll 191 | // by adding an offset when printing 192 | // Also we need to clear the whole console when doing this to avoid 193 | // artifacts, as lines can now be of different length 194 | scrollOffset--; 195 | return CLEAR_ALL; 196 | } 197 | else if(cursor > 0) 198 | { 199 | cursor--; 200 | return CLEAR_ATTRIBUTES; 201 | } 202 | } 203 | 204 | #endif -------------------------------------------------------------------------------- /imgs/duck.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darusc/duck/9b6b4e97279ad34ae498fb619ba9dbb65ec29a5d/imgs/duck.gif -------------------------------------------------------------------------------- /main.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #ifdef __unix__ 6 | #include 7 | #else 8 | #include 9 | #endif 10 | 11 | #include "duck.h" 12 | #include "dui.h" 13 | 14 | #define ARG(i, s) (strcmp(argv[i], s) == 0) 15 | 16 | dirtree* root = NULL; 17 | 18 | int main(int argc, char **argv) 19 | { 20 | if(argc < 2 || ARG(1, "-h") || ARG(1, "--help") || argv[1][0] == '-') 21 | { 22 | printf("duck v1.0 - Disk usage analysis tool\nhttps://github.com/darusc/duck\n\n"); 23 | printf("Usage:\n duck [options]\n\n"); 24 | printf("Options:\n"); 25 | printf(" -b, --benchmark Benchmark execution\n"); 26 | printf(" -c, --count Show item count\n"); 27 | printf(" -e, --exclude [extensions] Exclude files with specified extensions\n"); 28 | printf(" -i, --include [extensions] Include only files with specified extensions\n"); 29 | printf(" -h, --help Print this info\n"); 30 | #ifndef __unix__ 31 | printf(" -hf, --hidden Ignore hidden files.\n"); 32 | #endif 33 | printf(" -s, --sort Sort by (size is default)\n"); 34 | printf(" methods: \n"); 35 | printf("\n\n"); 36 | 37 | return 0; 38 | } 39 | 40 | duioptions uioptions = {1, 2, 0}; 41 | duckoptions doptions = {0 ,0, DSIZE, 0, 0, 0}; 42 | 43 | for(int i = 2; i < argc; i++) 44 | { 45 | if(ARG(i, "-b") || ARG(i, "--benchmark")) 46 | { 47 | doptions.benchmark = 1; 48 | uioptions.y = BENCHMARK_LINES; 49 | } 50 | else if(ARG(i, "-hf") || ARG(i, "--hidden")) 51 | { 52 | doptions.hide = 1; 53 | } 54 | else if(ARG(i, "-s") || ARG(i, "--sort")) 55 | { 56 | if(ARG(i + 1, "size")) 57 | doptions.sort = DSIZE; 58 | else if(ARG(i + 1, "alphabetic")) 59 | doptions.sort = DALPHABETIC; 60 | else if(ARG(i + 1, "items")) 61 | doptions.sort = DITEMS; 62 | } 63 | else if(ARG(i, "-i") || ARG(i, "--include")) 64 | { 65 | doptions.include = 1; 66 | } 67 | else if(ARG(i, "-e") || ARG(i, "--exclude")) 68 | { 69 | doptions.exclude = 1; 70 | } 71 | else if(ARG(i, "-c") || ARG(i, "--count")) 72 | { 73 | uioptions.count = 1; 74 | } 75 | 76 | if(doptions.exclude || doptions.include) 77 | { 78 | i++; 79 | while(i < argc && argv[i][0] != '-') 80 | strcpy(doptions.extenstions[doptions.nexts++], argv[i++]); 81 | i--; 82 | } 83 | } 84 | 85 | root = dirtree_alloc("~", DDIRECTORY, NULL); 86 | build_dirtree(root, argv[1], doptions); 87 | 88 | dui_init(uioptions); 89 | 90 | dui_header(); 91 | 92 | if(doptions.benchmark) 93 | { 94 | dui_benchmark_print(bench); 95 | } 96 | 97 | int clrmode = 0; 98 | dui_print(root); 99 | while(1) 100 | { 101 | int key = getch(); 102 | 103 | switch(key) 104 | { 105 | case DUCK_QUIT: 106 | dui_clear(CLEAR_END); 107 | dui_end(); 108 | return 0; 109 | 110 | case DUCK_DOWN: 111 | case 'j': // Vim-style down 112 | if(dirtree_select_next_file(root)) 113 | { 114 | clrmode = dui_scroll_down(); 115 | } 116 | break; 117 | 118 | case DUCK_UP: 119 | case 'k': // Vim-style up 120 | if(dirtree_select_prev_file(root)) 121 | { 122 | clrmode = dui_scroll_up(); 123 | } 124 | break; 125 | 126 | case 'l': // Vim-style child 127 | case DUCK_ENTER: 128 | clrmode = dirtree_down(&root) * (CLEAR_ALL | CLEAR_CURSOR_OFFSET); 129 | break; 130 | 131 | case 'h': // Vim-style parent 132 | case DUCK_BACK: 133 | clrmode = dirtree_up(&root) * (CLEAR_ALL | CLEAR_CURSOR_OFFSET); 134 | break; 135 | } 136 | 137 | if(clrmode) 138 | { 139 | #ifdef __unix__ 140 | dui_clear(clrmode); 141 | dui_print(root); 142 | refresh(); 143 | #else 144 | dui_clear(clrmode); 145 | dui_print(root); 146 | #endif 147 | 148 | clrmode = 0; 149 | } 150 | } 151 | 152 | return 0; 153 | } 154 | --------------------------------------------------------------------------------