1 /* vifm
2 * Copyright (C) 2018 xaizek.
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
17 */
18
19 #include "media_menu.h"
20
21 #include <stddef.h> /* NULL */
22 #include <stdlib.h> /* free() */
23 #include <string.h> /* strdup() */
24 #include <wchar.h> /* wcscmp() */
25
26 #include "../cfg/config.h"
27 #include "../compat/fs_limits.h"
28 #include "../compat/reallocarray.h"
29 #include "../modes/dialogs/msg_dialog.h"
30 #include "../ui/ui.h"
31 #include "../utils/fs.h"
32 #include "../utils/path.h"
33 #include "../utils/str.h"
34 #include "../utils/string_array.h"
35 #include "../utils/utils.h"
36 #include "../running.h"
37 #include "menus.h"
38
39 /* Example state of the menu:
40 *
41 * Items Data Action
42 *
43 * /dev/sdc mount /dev/sda3
44 * `-- (not mounted) m/dev/sda3 mount /dev/sda3
45 *
46 * /dev/sdd {text}
47 * |-- / u/ unmount /
48 * `-- /root/tmp u/root/tmp unmount /root/tmp
49 *
50 * /dev/sde unmount /media
51 * `-- /media u/media unmount /media
52 *
53 * Additional keys:
54 * - r -- reload list
55 * - [ -- previous device
56 * - ] -- next device
57 * - m -- mount/unmount
58 *
59 * Behaviour on Enter:
60 * - navigate inside mount-point or,
61 * - mount device if there are no mount-points or,
62 * - do nothing and keep menu open
63 */
64
65 /* Description of a single device. */
66 typedef struct
67 {
68 char *device; /* Device name in the system. */
69 char *text; /* Arbitrary text next to device name (can be NULL). */
70 char **paths; /* List of paths at which the device is mounted. */
71 int path_count; /* Number of elements in the paths array. */
72 int has_info; /* Set when text is set via info=. */
73 }
74 media_info_t;
75
76 static int execute_media_cb(struct view_t *view, menu_data_t *m);
77 static KHandlerResponse media_khandler(struct view_t *view, menu_data_t *m,
78 const wchar_t keys[]);
79 static int reload_list(menu_data_t *m);
80 static void output_handler(const char line[], void *arg);
81 static void free_info_array(void);
82 static void position_cursor(menu_data_t *m);
83 static const char * get_selected_data(menu_data_t *m);
84 static void path_get_decors(const char path[], FileType type,
85 const char **prefix, const char **suffix);
86 static int mediaprg_mount(const char data[], menu_data_t *m);
87
88 /* List of media devices. */
89 static media_info_t *infos;
90 /* Number of elements in infos array. */
91 static int info_count;
92
93 int
show_media_menu(struct view_t * view)94 show_media_menu(struct view_t *view)
95 {
96 static menu_data_t m;
97 menus_init_data(&m, view, strdup("Media"), strdup("No media found"));
98 m.execute_handler = &execute_media_cb;
99 m.key_handler = &media_khandler;
100
101 if(reload_list(&m) != 0)
102 {
103 return 0;
104 }
105
106 position_cursor(&m);
107
108 return menus_enter(m.state, view);
109 }
110
111 /* Callback that is called when menu item is selected. Return non-zero to stay
112 * in menu mode and zero otherwise. */
113 static int
execute_media_cb(struct view_t * view,menu_data_t * m)114 execute_media_cb(struct view_t *view, menu_data_t *m)
115 {
116 const char *data = get_selected_data(m);
117 if(data != NULL)
118 {
119 if(*data == 'u')
120 {
121 const char *path = data + 1;
122 menus_goto_dir(view, path);
123 return 0;
124 }
125 else if(*data == 'm')
126 {
127 (void)mediaprg_mount(data, m);
128 }
129 }
130 return 1;
131 }
132
133 /* Menu-specific shortcut handler. Returns code that specifies both taken
134 * actions and what should be done next. */
135 static KHandlerResponse
media_khandler(struct view_t * view,menu_data_t * m,const wchar_t keys[])136 media_khandler(struct view_t *view, menu_data_t *m, const wchar_t keys[])
137 {
138 if(wcscmp(keys, L"r") == 0)
139 {
140 reload_list(m);
141 return KHR_REFRESH_WINDOW;
142 }
143 else if(wcscmp(keys, L"[") == 0)
144 {
145 int i = m->pos;
146 while(i-- > 0)
147 {
148 if(m->data[i] == NULL && m->data[i + 1] != NULL)
149 {
150 menus_set_pos(m->state, i);
151 menus_partial_redraw(m->state);
152 break;
153 }
154 }
155 return KHR_REFRESH_WINDOW;
156 }
157 else if(wcscmp(keys, L"]") == 0)
158 {
159 int i = m->pos;
160 while(++i < m->len - 1)
161 {
162 if(m->data[i] == NULL && m->data[i + 1] != NULL)
163 {
164 menus_set_pos(m->state, i);
165 menus_partial_redraw(m->state);
166 break;
167 }
168 }
169 return KHR_REFRESH_WINDOW;
170 }
171 else if(wcscmp(keys, L"m") == 0)
172 {
173 const char *data = get_selected_data(m);
174 (void)mediaprg_mount(data, m);
175 return KHR_REFRESH_WINDOW;
176 }
177 return KHR_UNHANDLED;
178 }
179
180 /* (Re)loads list of media devices into the menu. Returns zero on success,
181 * otherwise non-zero is returned (empty menu is not considered an error). */
182 static int
reload_list(menu_data_t * m)183 reload_list(menu_data_t *m)
184 {
185 free_string_array(m->data, m->len);
186 free_string_array(m->items, m->len);
187
188 m->data = NULL;
189 m->items = NULL;
190 m->len = 0;
191
192 if(cfg.media_prg[0] == '\0')
193 {
194 show_error_msg("Listing media devices",
195 "'mediaprg' option must not be empty");
196 return 1;
197 }
198
199 char *cmd = format_str("%s list", cfg.media_prg);
200 if(process_cmd_output("Listing media", cmd, 0, 0, &output_handler, &m) != 0)
201 {
202 free(cmd);
203 show_error_msgf("Listing media devices", "Unable to run: %s", cmd);
204 return 1;
205 }
206 free(cmd);
207
208 int i;
209 const int menu_rows = (menu_win != NULL ? (getmaxy(menu_win) + 1) - 3 : 100);
210 int max_rows_with_blanks = info_count /* Device lines. */
211 + (info_count - 1) /* Blank lines. */;
212 for(i = 0; i < info_count; ++i)
213 {
214 max_rows_with_blanks += infos[i].path_count > 0
215 ? infos[i].path_count /* Mount-points. */
216 : 1; /* "Not mounted" line. */
217 }
218
219 for(i = 0; i < info_count; ++i)
220 {
221 const char *prefix, *suffix;
222 media_info_t *info = &infos[i];
223
224 put_into_string_array(&m->data, m->len, NULL);
225
226 path_get_decors(info->device, FT_BLOCK_DEV, &prefix, &suffix);
227 m->len = put_into_string_array(&m->items, m->len,
228 format_str("%s%s%s %s", prefix, info->device, suffix,
229 info->text ? info->text : ""));
230
231 if(info->path_count == 0)
232 {
233 put_into_string_array(&m->data, m->len, format_str("m%s", info->device));
234 m->len = add_to_string_array(&m->items, m->len, "`-- (not mounted)");
235 }
236 else
237 {
238 int j;
239 for(j = 0; j < info->path_count; ++j)
240 {
241 put_into_string_array(&m->data, m->len,
242 format_str("u%s", info->paths[j]));
243
244 path_get_decors(info->paths[j], FT_DIR, &prefix, &suffix);
245 const char *path = (is_root_dir(info->paths[j]) && suffix[0] == '/')
246 ? ""
247 : info->paths[j];
248 m->len = put_into_string_array(&m->items, m->len,
249 format_str("%c-- %s%s%s", j + 1 == info->path_count ? '`' : '|',
250 prefix, path, suffix));
251 }
252 }
253
254 if(i != info_count - 1 && max_rows_with_blanks <= menu_rows)
255 {
256 put_into_string_array(&m->data, m->len, NULL);
257 m->len = add_to_string_array(&m->items, m->len, "");
258 }
259 }
260
261 free_info_array();
262
263 return 0;
264 }
265
266 /* Implements process_cmd_output() callback that builds array of devices. */
267 static void
output_handler(const char line[],void * arg)268 output_handler(const char line[], void *arg)
269 {
270 if(skip_prefix(&line, "device="))
271 {
272 media_info_t *new_infos = reallocarray(infos, ++info_count, sizeof(*infos));
273 if(new_infos != NULL)
274 {
275 infos = new_infos;
276 media_info_t *info = &infos[info_count - 1];
277 info->device = strdup(line);
278 info->text = NULL;
279 info->paths = NULL;
280 info->path_count = 0;
281 info->has_info = 0;
282 }
283 }
284 else if(info_count > 0)
285 {
286 media_info_t *info = &infos[info_count - 1];
287 if(skip_prefix(&line, "info="))
288 {
289 replace_string(&info->text, line);
290 info->has_info = 1;
291 }
292 else if(skip_prefix(&line, "label=") && !info->has_info)
293 {
294 put_string(&info->text, format_str("[%s]", line));
295 }
296 else if(skip_prefix(&line, "mount-point="))
297 {
298 info->path_count = add_to_string_array(&info->paths, info->path_count,
299 line);
300 }
301 }
302 }
303
304 /* Releases global data that collects information about media. */
305 static void
free_info_array(void)306 free_info_array(void)
307 {
308 int i;
309 for(i = 0; i < info_count; ++i)
310 {
311 media_info_t *info = &infos[i];
312 free(info->device);
313 free(info->text);
314 free_string_array(info->paths, info->path_count);
315 }
316 free(infos);
317 infos = NULL;
318 info_count = 0;
319 }
320
321 /* Puts cursor position to a mount point that corresponds to current path if
322 * possible. */
323 static void
position_cursor(menu_data_t * m)324 position_cursor(menu_data_t *m)
325 {
326 int i;
327 for(i = 0; i < m->len; ++i)
328 {
329 if(m->data[i] != NULL && *m->data[i] == 'u' &&
330 is_in_subtree(m->cwd, m->data[i] + 1, 1))
331 {
332 m->pos = i;
333 break;
334 }
335 }
336 }
337
338 /* Retrieves decorations for path. See ui_get_decors(). */
339 static void
path_get_decors(const char path[],FileType type,const char ** prefix,const char ** suffix)340 path_get_decors(const char path[], FileType type, const char **prefix,
341 const char **suffix)
342 {
343 dir_entry_t entry;
344
345 entry.name = get_last_path_component(path);
346 /* Avoid memory allocation. */
347 if(entry.name != path)
348 {
349 entry.name[-1] = '\0';
350 entry.origin = entry.name;
351 }
352 else
353 {
354 entry.origin = (type == FT_BLOCK_DEV ? "/dev" : "/");
355 }
356 entry.type = type;
357 entry.name_dec_num = -1;
358
359 ui_get_decors(&entry, prefix, suffix);
360
361 /* Restore original path. */
362 if(entry.name != path)
363 {
364 entry.name[-1] = '/';
365 }
366 }
367
368 /* Retrieves appropriate data for selected item in a human-friendly way. */
369 static const char *
get_selected_data(menu_data_t * m)370 get_selected_data(menu_data_t *m)
371 {
372 const char *data = m->data[m->pos];
373 if(data == NULL)
374 {
375 char *next_data = (m->pos + 1 < m->len ? m->data[m->pos + 1] : NULL);
376 char *next_next_data = (m->pos + 2 < m->len ? m->data[m->pos + 2] : NULL);
377 if(next_data != NULL && next_next_data == NULL)
378 {
379 data = next_data;
380 }
381 }
382 return data;
383 }
384
385 /* Executes "mount" or "unmount" command on data. Returns non-zero if program
386 * executed successfully and zero otherwise. */
387 static int
mediaprg_mount(const char data[],menu_data_t * m)388 mediaprg_mount(const char data[], menu_data_t *m)
389 {
390 if(data == NULL || (*data != 'm' && *data != 'u') || cfg.media_prg[0] == '\0')
391 {
392 return 1;
393 }
394
395 int error = 0;
396 int mount = (*data == 'm');
397 const char *path = data + 1;
398
399 if(!mount)
400 {
401 /* Try to get out of mount point before trying to unmount it. */
402 char cwd[PATH_MAX + 1];
403 if(get_cwd(cwd, sizeof(cwd)) == cwd && is_in_subtree(cwd, path, 1))
404 {
405 char out_of_mount_path[PATH_MAX + 1];
406 snprintf(out_of_mount_path, sizeof(out_of_mount_path), "%s/..", path);
407 (void)vifm_chdir(out_of_mount_path);
408 }
409 }
410
411 const char *action = (mount ? "mount" : "unmount");
412 const char *description = (mount ? "Mounting" : "Unmounting");
413
414 char *escaped_path = shell_like_escape(path, 0);
415 char *cmd = format_str("%s %s %s", cfg.media_prg, action, escaped_path);
416 if(rn_shell(cmd, PAUSE_NEVER, 0, SHELL_BY_APP) == 0)
417 {
418 reload_list(m);
419 }
420 else
421 {
422 error = 1;
423 show_error_msgf("Media operation error", "%s has failed", description);
424 }
425 free(escaped_path);
426 free(cmd);
427
428 menus_partial_redraw(m->state);
429 return error;
430 }
431
432 /* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
433 /* vim: set cinoptions+=t0 : */
434