Skip to content

Commit 8e4e5c4

Browse files
committed
ui: vm preview
Terminal must support kitty graphics protocol. New config parameters: [preview] enabled = 0|1 scale = 0|1 png_path = /path/to/vm.png
1 parent d6b9e35 commit 8e4e5c4

File tree

10 files changed

+165
-2
lines changed

10 files changed

+165
-2
lines changed

CHANGES

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
unreleased
22
------------------------
3+
- Feature: VM preview
4+
Terminal must support kitty graphics protocol
5+
New config parameters:
6+
[preview]
7+
enabled = 0|1
8+
scale = 0|1
9+
png_path = /path/to/vm.png
10+
- Feature: configurable refresh timeout for properties window
11+
Parameter `refresh_timeout` in `main` section
312
- Feature: image format type support: qcow2 and raw
413
- Bugfix: fix import on some locales
514

nemu.cfg.sample

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@ db = /home/user/.nemu.db
2525
# if not set VTE's default cursor style will be used
2626
# cursor_style = 1
2727

28+
# Properties refresh timeout (ms)
29+
# refresh_timeout = 500
30+
31+
[preview]
32+
# enabled = 0
33+
# scale = 0
34+
# png_path = /tmp/nemu.png
35+
2836
[viewer]
2937
# default protocol (1 - spice, 0 - vnc)
3038
spice_default = 1

src/nm_cfg_file.c

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,17 +31,21 @@ static const char NM_DEFAULT_SPICE[] = "/usr/bin/remote-viewer";
3131
static const char NM_DEFAULT_QEMUDIR[] = "/usr/bin";
3232
#endif /* NM_WITH_QEMU */
3333

34+
static const int NM_DEFAULT_REFRESH = 500;
35+
3436
static const char NM_DEFAULT_VNCARG[] = ":%p";
3537
static const char NM_DEFAULT_SPICEARG[] = "--title %t spice://127.0.0.1:%p";
3638

3739
static const char NM_DEFAULT_TARGET[] = "x86_64,i386";
3840

3941
static const char NM_DEFAULT_PID[] = "/tmp/nemu.pid";
42+
static const char NM_DEFAULT_PNG[] = "/tmp/nemu.png";
4043

4144
static const char NM_INI_S_MAIN[] = "main";
4245
static const char NM_INI_S_VIEW[] = "viewer";
4346
static const char NM_INI_S_QEMU[] = "qemu";
4447
static const char NM_INI_S_DMON[] = "nemu-monitor";
48+
static const char NM_INI_S_PREV[] = "preview";
4549

4650
static const char NM_INI_P_VM[] = "vmdir";
4751
static const char NM_INI_P_DB[] = "db";
@@ -64,6 +68,7 @@ static const char NM_INI_P_AUTO[] = "autostart";
6468
static const char NM_INI_P_SLP[] = "sleep";
6569
static const char NM_INI_P_GL_SEP[] = "glyph_separator";
6670
static const char NM_INI_P_GL_CHECK[] = "glyph_checkbox";
71+
static const char NM_INI_P_REFRESH[] = "refresh_timeout";
6772
#if defined (NM_WITH_REMOTE)
6873
static const char NM_INI_P_API_SRV[] = "remote_control";
6974
static const char NM_INI_P_API_IFACE[] = "remote_interface";
@@ -77,6 +82,9 @@ static const char NM_INI_P_API_HASH[] = "remote_hash";
7782
static const char NM_INI_P_DYES[] = "dbus_enabled";
7883
static const char NM_INI_P_DTMT[] = "dbus_timeout";
7984
#endif
85+
static const char NM_INI_P_PREV_FLAG[] = "enabled";
86+
static const char NM_INI_P_PREV_SCALE[] = "scale";
87+
static const char NM_INI_P_PREV_PATH[] = "png_path";
8088

8189
static const char * const CURSOR_STYLE_STR[] = {
8290
"Default",
@@ -355,6 +363,34 @@ void nm_cfg_init(bool bypass_cfg)
355363
cfg.glyphs.checkbox = !!nm_str_stoui(&tmp_buf, 10);
356364
}
357365

366+
nm_str_trunc(&tmp_buf, 0);
367+
if (nm_get_opt_param(ini, NM_INI_S_MAIN, NM_INI_P_REFRESH,
368+
&tmp_buf) == NM_OK) {
369+
cfg.refresh_timeout = nm_str_stoul(&tmp_buf, 10);
370+
} else {
371+
cfg.refresh_timeout = NM_DEFAULT_REFRESH;
372+
}
373+
374+
/* VM preview */
375+
nm_str_trunc(&tmp_buf, 0);
376+
if (nm_get_opt_param(ini, NM_INI_S_PREV, NM_INI_P_PREV_FLAG,
377+
&tmp_buf) == NM_OK) {
378+
cfg.preview.enabled = !!nm_str_stoui(&tmp_buf, 10);
379+
} else {
380+
cfg.preview.enabled = 0;
381+
}
382+
nm_str_trunc(&tmp_buf, 0);
383+
if (nm_get_opt_param(ini, NM_INI_S_PREV, NM_INI_P_PREV_SCALE,
384+
&tmp_buf) == NM_OK) {
385+
cfg.preview.scale = !!nm_str_stoui(&tmp_buf, 10);
386+
} else {
387+
cfg.preview.scale = 0;
388+
}
389+
if (nm_get_opt_param(ini, NM_INI_S_PREV, NM_INI_P_PREV_PATH,
390+
&cfg.preview.path) != NM_OK) {
391+
nm_str_alloc_text(&cfg.preview.path, NM_DEFAULT_PNG);
392+
}
393+
358394
#if defined (NM_WITH_REMOTE)
359395
nm_str_trunc(&tmp_buf, 0);
360396
cfg.api_server = 0;
@@ -428,6 +464,7 @@ void nm_cfg_free(void)
428464
nm_str_free(&cfg.pid);
429465
nm_str_free(&cfg.daemon_pid);
430466
nm_str_free(&cfg.qemu_bin_path);
467+
nm_str_free(&cfg.preview.path);
431468
nm_vect_free(&cfg.qemu_targets, NULL);
432469
#if defined (NM_WITH_REMOTE)
433470
nm_str_free(&cfg.api_cert_path);
@@ -605,6 +642,11 @@ static void nm_generate_cfg(const char *home, const nm_str_t *cfg_path)
605642
"# see https://terminalguide.namepad.de/seq/csi_sq_t_space/"
606643
"\n# if not set VTE's default cursor style will be used\n"
607644
"# cursor_style = 1\n\n");
645+
fprintf(cfg_file, "# Properties refresh timeout (ms)\n"
646+
"# refresh_timeout = 500\n\n");
647+
fprintf(cfg_file, "[preview]\n");
648+
fprintf(cfg_file, "# enabled = 0\n# scale = 0\n"
649+
"# png_path = /tmp/nemu.png\n\n");
608650
fprintf(cfg_file, "[viewer]\n");
609651
fprintf(cfg_file, "# default protocol (1 - spice, 0 - vnc)"
610652
"\nspice_default = 1\n\n");

src/nm_cfg_file.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ typedef struct {
2626
uint32_t checkbox:1;
2727
} nm_glyph_t;
2828

29+
typedef struct {
30+
nm_str_t path;
31+
uint32_t enabled:1;
32+
uint32_t scale:1;
33+
} nm_preview_t;
34+
2935
typedef struct {
3036
nm_str_t vm_dir;
3137
nm_str_t db_path;
@@ -43,8 +49,10 @@ typedef struct {
4349
nm_rgb_t hl_color;
4450
nm_rgb_t err_color;
4551
nm_glyph_t glyphs;
52+
nm_preview_t preview;
4653
nm_str_t debug_path;
4754
uint64_t daemon_sleep;
55+
uint64_t refresh_timeout;
4856
uint32_t cursor_style;
4957
#if defined (NM_WITH_DBUS)
5058
uint32_t dbus_enabled:1;

src/nm_main_loop.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,10 @@ void nm_start_main_loop(void)
188188
* Otherwise text will be flicker in tty.
189189
*/
190190
if (ch != ERR) {
191+
if (cfg->preview.enabled) {
192+
printf("\x1b_Ga=d,d=i,i=20509,q=2;\x1b\x5c");
193+
fflush(stdout);
194+
}
191195
clear_action = 1;
192196
}
193197

src/nm_qmp_control.c

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,10 @@ static const char NM_QMP_DEV_NET_ADD[] =
8484
static const char NM_QMP_DEV_DEL[] =
8585
"{'execute':'device_del','arguments':{'id':'dev-%s'}}";
8686

87+
static const char NM_QMP_TAKE_SCREENSHOT[] =
88+
"{'execute':'screendump','arguments':{'filename':'%s',"
89+
"'format':'png'}}";
90+
8791
#if defined NM_OS_LINUX
8892
static const char NM_QMP_NET_TAP_FD_ADD[] =
8993
"{'execute':'netdev_add','arguments':{'type':'tap',"
@@ -152,6 +156,16 @@ void nm_qmp_vm_resume(const nm_str_t *name)
152156
nm_qmp_vm_exec(name, NM_QMP_CMD_VM_CONT, &tv);
153157
}
154158

159+
void nm_qmp_take_screenshot(const nm_str_t *name, const nm_str_t *path)
160+
{
161+
nm_str_t qmp_query = NM_INIT_STR;
162+
struct timeval tv = { .tv_sec = 0, .tv_usec = 1000000 }; /* 1s */
163+
164+
nm_str_format(&qmp_query, NM_QMP_TAKE_SCREENSHOT, path->data);
165+
nm_qmp_vm_exec(name, qmp_query.data, &tv);
166+
nm_str_free(&qmp_query);
167+
}
168+
155169
int nm_qmp_savevm(const nm_str_t *name, const nm_str_t *snap)
156170
{
157171
nm_vect_t drives = NM_INIT_VECT;

src/nm_qmp_control.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ void nm_qmp_vm_stop(const nm_str_t *name);
1010
void nm_qmp_vm_reset(const nm_str_t *name);
1111
void nm_qmp_vm_pause(const nm_str_t *name);
1212
void nm_qmp_vm_resume(const nm_str_t *name);
13+
void nm_qmp_take_screenshot(const nm_str_t *name, const nm_str_t *path);
1314
int nm_qmp_savevm(const nm_str_t *name, const nm_str_t *snap);
1415
int nm_qmp_loadvm(const nm_str_t *name, const nm_str_t *snap);
1516
int nm_qmp_delvm(const nm_str_t *name, const nm_str_t *snap);

src/nm_utils.c

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,4 +587,44 @@ nm_get_drive_size(const nm_str_t *path, off_t *virtual_size, off_t *actual_size)
587587
nm_vect_free(&cmdv, NULL);
588588
json_object_put(js);
589589
}
590+
591+
static const char
592+
b64_chars[] =
593+
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
594+
595+
char *nm_64_encode(const uint8_t *src, size_t src_len)
596+
{
597+
size_t res_len;
598+
char *res;
599+
600+
res_len = 4 * ((src_len + 2) / 3);
601+
res = nm_calloc(1, res_len + 1);
602+
if (!res) {
603+
return NULL;
604+
}
605+
606+
for (size_t i = 0, j = 0; i < src_len; i += 3, j += 4) {
607+
size_t a = i < src_len ? src[i] : 0;
608+
size_t b = i + 1 < src_len ? src[i + 1] : 0;
609+
size_t c = i + 2 < src_len ? src[i + 2] : 0;
610+
size_t abc = (a << 0x10) + (b << 0x8) + c;
611+
612+
res[j] = b64_chars[(abc >> 18) & 0x3F];
613+
res[j + 1] = b64_chars[(abc >> 12) & 0x3F];
614+
615+
if (i + 1 < src_len) {
616+
res[j + 2] = b64_chars[(abc >> 6) & 0x3F];
617+
} else {
618+
res[j + 2] = '=';
619+
}
620+
621+
if (i + 2 < src_len) {
622+
res[j + 3] = b64_chars[abc & 0x3F];
623+
} else {
624+
res[j + 3] = '=';
625+
}
626+
}
627+
628+
return res;
629+
}
590630
/* vim:set ts=4 sw=4: */

src/nm_utils.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,5 +66,8 @@ void nm_get_time(nm_str_t *res, const char *fmt);
6666
void nm_gen_rand_str(nm_str_t *res, size_t len);
6767
void nm_gen_uid(nm_str_t *res);
6868

69+
/* Caller must free the return value. */
70+
char *nm_64_encode(const uint8_t *src, size_t src_len);
71+
6972
#endif /* NM_UTILS_H_ */
7073
/* vim:set ts=4 sw=4: */

src/nm_window.c

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include <nm_cfg_file.h>
99
#include <nm_usb_plug.h>
1010
#include <nm_stat_usage.h>
11+
#include <nm_qmp_control.h>
1112

1213
static float nm_window_scale = 0.7;
1314

@@ -140,7 +141,7 @@ void nm_init_side(void)
140141
nm_init_window__(side_window, _("VM list"));
141142
}
142143

143-
wtimeout(side_window, 500);
144+
wtimeout(side_window, nm_cfg_get()->refresh_timeout);
144145
}
145146

146147
void nm_init_side_lan(void)
@@ -629,7 +630,7 @@ nm_print_vm_info(const nm_str_t *name, const nm_vmctl_data_t *vm, int status)
629630

630631
}
631632

632-
/* print PID */
633+
/* print runtime statistics and screen preview if enabled */
633634
{
634635
int fd;
635636
nm_str_t pid_path = NM_INIT_STR;
@@ -662,12 +663,45 @@ nm_print_vm_info(const nm_str_t *name, const nm_vmctl_data_t *vm, int status)
662663
#else
663664
(void) pid_num;
664665
#endif
666+
if (nm_cfg_get()->preview.enabled) {
667+
char *b64;
668+
nm_file_map_t png = NM_INIT_FILE;
669+
size_t side_cols, NM_UNUSED side_rows;
670+
671+
nm_qmp_take_screenshot(name, &nm_cfg_get()->preview.path);
672+
png.name = &nm_cfg_get()->preview.path;
673+
nm_map_file(&png);
674+
b64 = nm_64_encode(png.mp, png.size);
675+
676+
getmaxyx(side_window, side_rows, side_cols);
677+
678+
if (nm_cfg_get()->preview.scale) {
679+
nm_str_format(&buf,
680+
"\x1b_Gi=20509,q=2,a=T,C=1,c=%zu,r=%zu,f=100;%s"
681+
"\x1b\x5c",
682+
cols - 4, rows - y - 2, b64);
683+
} else {
684+
nm_str_format(&buf,
685+
"\x1b_Gi=20509,q=2,a=T,C=1,r=%zu,f=100;%s\x1b\x5c",
686+
rows - y - 3, b64);
687+
}
688+
mvcur(42, 42, y + 2, side_cols + 2);
689+
printf("%s", buf.data);
690+
fflush(stdout);
691+
692+
nm_unmap_file(&png);
693+
free(b64);
694+
}
665695
} else { /* clear PID file info and cpu usage data */
666696
if (y < (rows - 2)) {
667697
mvwhline(action_window, y, 1, ' ', cols - 4);
668698
mvwhline(action_window, y + 1, 1, ' ', cols - 4);
669699
}
670700
NM_STAT_CLEAN();
701+
if (nm_cfg_get()->preview.enabled) {
702+
printf("\x1b_Ga=d,d=i,i=20509,q=2;\x1b\x5c");
703+
fflush(stdout);
704+
}
671705
}
672706

673707
nm_str_free(&pid_path);

0 commit comments

Comments
 (0)