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