├── .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 |
--------------------------------------------------------------------------------