mmsg: add man page and usage message

This commit is contained in:
DreamMaoMao 2026-05-31 22:44:12 +08:00
parent 6849e5b4db
commit a11cf12f28
3 changed files with 200 additions and 21 deletions

View file

@ -1,4 +1,4 @@
project('mango', ['c', 'cpp'], project('mango', ['c'],
version : '0.14.0', version : '0.14.0',
) )
@ -24,10 +24,6 @@ if sysconfdir.startswith(prefix) and not is_nixos
endif endif
endif endif
# 打印调试信息,确认 sysconfdir 的值
# message('prefix: ' + prefix)
# message('sysconfdir: ' + sysconfdir)
cc = meson.get_compiler('c') cc = meson.get_compiler('c')
libm = cc.find_library('m') libm = cc.find_library('m')
xcb = dependency('xcb', required : get_option('xwayland')) xcb = dependency('xcb', required : get_option('xwayland'))
@ -42,12 +38,11 @@ libscenefx_dep = dependency('scenefx-0.4',version: '>=0.4.1')
pixman_dep = dependency('pixman-1') pixman_dep = dependency('pixman-1')
cjson_dep = dependency('libcjson') cjson_dep = dependency('libcjson')
# version info
# 获取版本信息
git = find_program('git', required : false) git = find_program('git', required : false)
is_git_repo = false is_git_repo = false
# 检查当前目录是否是 Git 仓库 # check if current directory is a git repo
if git.found() if git.found()
git_status = run_command(git, 'rev-parse', '--is-inside-work-tree', check : false) git_status = run_command(git, 'rev-parse', '--is-inside-work-tree', check : false)
if git_status.returncode() == 0 and git_status.stdout().strip() == 'true' if git_status.returncode() == 0 and git_status.stdout().strip() == 'true'
@ -56,18 +51,18 @@ if git.found()
endif endif
if is_git_repo if is_git_repo
# 如果是 Git 目录,获取 Commit Hash 和最新的 tag # if current directory is a git repo, get commit hash and latest tag
commit_hash = run_command(git, 'rev-parse', '--short', 'HEAD', check : false).stdout().strip() commit_hash = run_command(git, 'rev-parse', '--short', 'HEAD', check : false).stdout().strip()
latest_tag = meson.project_version() latest_tag = meson.project_version()
version_with_hash = '@0@(@1@)'.format(latest_tag, commit_hash) version_with_hash = '@0@(@1@)'.format(latest_tag, commit_hash)
else else
# 如果不是 Git 目录,使用项目版本号和 "release" 字符串 # if not a git repo, use project version and "release" string
commit_hash = 'release' commit_hash = 'release'
latest_tag = meson.project_version() latest_tag = meson.project_version()
version_with_hash = '@0@(@1@)'.format(latest_tag, commit_hash) version_with_hash = '@0@(@1@)'.format(latest_tag, commit_hash)
endif endif
# 定义编译参数 # define compilation args
c_args = [ c_args = [
'-g', '-g',
'-Wno-unused-function', '-Wno-unused-function',
@ -77,7 +72,7 @@ c_args = [
'-DSYSCONFDIR="@0@"'.format('/etc'), '-DSYSCONFDIR="@0@"'.format('/etc'),
] ]
# 仅在 debug 选项启用时添加调试参数 # add debug args only when debug option is enabled
if get_option('asan') if get_option('asan')
c_args += [ c_args += [
'-fsanitize=address', '-fsanitize=address',
@ -90,7 +85,7 @@ if xcb.found() and xlibs.found()
c_args += '-DXWAYLAND' c_args += '-DXWAYLAND'
endif endif
# 链接参数(根据 debug 状态添加 ASAN # define link args
link_args = [] link_args = []
if get_option('asan') if get_option('asan')
link_args += '-fsanitize=address' link_args += '-fsanitize=address'
@ -135,7 +130,7 @@ wayland_scanner_private_code = generator(
arguments: ['private-code', '@INPUT@', '@OUTPUT@'] arguments: ['private-code', '@INPUT@', '@OUTPUT@']
) )
# 在 mmsg 目标中使用生成器 # use generator in mmsg target
executable('mmsg', executable('mmsg',
'mmsg/mmsg.c', 'mmsg/mmsg.c',
wayland_scanner_private_code.process(dwl_ipc_protocol), wayland_scanner_private_code.process(dwl_ipc_protocol),
@ -150,8 +145,10 @@ executable('mmsg',
], ],
) )
mandir = get_option('mandir')
desktop_install_dir = join_paths(prefix, 'share/wayland-sessions') desktop_install_dir = join_paths(prefix, 'share/wayland-sessions')
portal_install_dir = join_paths(prefix, 'share/xdg-desktop-portal') portal_install_dir = join_paths(prefix, 'share/xdg-desktop-portal')
install_data('assets/mango.desktop', install_dir : desktop_install_dir) install_data('assets/mango.desktop', install_dir : desktop_install_dir)
install_data('assets/mango-portals.conf', install_dir : portal_install_dir) install_data('assets/mango-portals.conf', install_dir : portal_install_dir)
install_data('assets/config.conf', install_dir : join_paths(sysconfdir, 'mango')) install_data('assets/config.conf', install_dir : join_paths(sysconfdir, 'mango'))
install_data('mmsg/mmsg.1', install_dir : join_paths(mandir, 'man1'))

122
mmsg/mmsg.1 Normal file
View file

@ -0,0 +1,122 @@
.TH "mmsg" "1" "2026-05-31" "mmsg 0.14.0" "mmsg manual"
.SH NAME
mmsg \- send commands to the mango Wayland compositor and receive JSON responses
.SH SYNOPSIS
\fBmmsg\fR <command> [\fIargs\fR...]
.br
\fBmmsg\fR \-\-help | \-h | help
.SH DESCRIPTION
\fBmmsg\fR connects to a running \fBmango\fR(1) compositor via the UNIX domain socket
specified by the \fIMANGO_INSTANCE_SIGNATURE\fR environment variable. It sends
the given command and prints the JSON reply to standard output. In \fBwatch\fR
mode it keeps the connection open and prints a stream of updates as they
occur.
.PP
If \fB\-\-help\fR, \fB\-h\fR or \fBhelp\fR is given, a complete usage summary is printed and
the program exits with success.
.SH COMMANDS
.SS "One\-shot queries (get)"
All \fBget\fR commands print a single JSON object and then close the connection.
.TP
\fBget version\fR
Return compositor version.
.TP
\fBget keymode\fR
Return current keymode.
.TP
\fBget keyboardlayout\fR
Return current keyboard layout.
.TP
\fBget last_open_surface\fR [\fImonitor\fR]
Return the last open surface (application) for the given monitor.
If \fImonitor\fR is omitted the focused monitor is used.
.TP
\fBget monitor\fR <name>
Return details of the named monitor.
.TP
\fBget focusing\-client\fR
Return details of the currently focused client.
.TP
\fBget client\fR <id>
Return details of the client with the given numeric ID.
.TP
\fBget tag\fR <monitor> <index>
Return details of a specific tag (1\-based index) on the named monitor.
.TP
\fBget all\-clients\fR
List all clients.
.TP
\fBget all\-monitors\fR
List all monitors.
.TP
\fBget all\-tags\fR
List tags for all monitors.
.TP
\fBget tags\fR <monitor>
List tags for a specific monitor.
.TP
\fBdispatch\fR <func>[,arg...] [client,<id>]
Invoke an internal compositor function.
The function name and its arguments are separated by commas.
Optionally add \fBclient,<id>\fR (before or after the function) to target a
specific client.
.br
Examples:
.RS
.IP \(bu 2
\fBmmsg dispatch togglefloating\fR
.IP \(bu 2
\fBmmsg dispatch movewin,10,100\fR
.IP \(bu 2
\fBmmsg dispatch movewin,10,100 client,4\fR
.RE
.SS "Persistent streams (watch)"
\fBWatch\fR commands keep the connection open and continuously output JSON
updates. The initial state is sent immediately, followed by updates whenever
relevant changes occur.
.TP
\fBwatch monitor\fR <name>
Stream updates for the named monitor.
.TP
\fBwatch focusing\-client\fR
Stream updates for the focused client.
.TP
\fBwatch client\fR <id>
Stream updates for the client with the given ID.
.TP
\fBwatch tags\fR <monitor>
Stream tag updates for the named monitor.
.TP
\fBwatch all\-monitors\fR
Stream updates for all monitors.
.TP
\fBwatch all\-tags\fR
Stream updates for all tags.
.TP
\fBwatch all\-clients\fR
Stream updates for all clients.
.TP
\fBwatch keymode\fR
Stream keymode changes.
.TP
\fBwatch keyboardlayout\fR
Stream keyboard layout changes.
.TP
\fBwatch last_open_surface\fR [\fImonitor\fR]
Stream last open surface changes for a specific monitor (or the focused one).
.SH ENVIRONMENT
.TP
\fIMANGO_INSTANCE_SIGNATURE\fR
Path to the compositor's IPC socket. Set automatically by \fBmango\fR(1).
If unset, \fBmmsg\fR prints an error and exits.
.SH EXIT STATUS
.TP
\fB0\fR
Success (or help message printed).
.TP
\fBEXIT_FAILURE\fR
An error occurred (connection refused, send/receive error, etc.).
.SH SEE ALSO
\fBmango\fR(1)
.SH BUGS
Report issues at the project's issue tracker.

View file

@ -7,7 +7,73 @@
#include <sys/un.h> #include <sys/un.h>
#include <unistd.h> #include <unistd.h>
static void usage(void) {
printf("Usage: mmsg <command> [args...]\n\n");
printf("One-shot queries (get):\n");
printf(
" get version Show compositor version\n");
printf(" get keymode Show current keymode\n");
printf(" get keyboardlayout Show current keyboard "
"layout\n");
printf(" get last_open_surface [monitor] Show last open surface "
"(default focused monitor)\n");
printf(" get monitor <name> Show monitor details\n");
printf(" get focusing-client Show focused client "
"details\n");
printf(" get client <id> Show client details by "
"ID\n");
printf(" get tag <monitor> <index> Show tag details "
"(1based index)\n");
printf(" get all-clients List all clients\n");
printf(" get all-monitors List all monitors\n");
printf(" get all-tags List all tags (all "
"monitors)\n");
printf(
" get tags <monitor> List tags for a monitor\n");
printf(" dispatch <func>[,arg...] [client,<id>] Call a compositor "
"function\n");
printf(" <func> and arguments are separated by commas.\n");
printf(" Add 'client,<id>' at the beginning or end to target a "
"specific client.\n");
printf(" Examples:\n");
printf(" dispatch togglefloating\n");
printf(" dispatch movewin,10,100\n");
printf(" dispatch movewin,10,100 client,4\n");
printf("Persistent streams (watch):\n");
printf(
" watch monitor <name> Stream monitor changes\n");
printf(" watch focusing-client Stream focused client "
"changes\n");
printf(
" watch client <id> Stream client changes\n");
printf(" watch tags <monitor> Stream tag changes for "
"a monitor\n");
printf(" watch all-monitors Stream all monitors "
"changes\n");
printf(
" watch all-tags Stream all tags changes\n");
printf(" watch all-clients Stream all clients "
"changes\n");
printf(
" watch keymode Stream keymode changes\n");
printf(" watch keyboardlayout Stream keyboard layout "
"changes\n");
printf(" watch last_open_surface [monitor] Stream last open "
"surface changes\n\n");
printf("Environment:\n");
printf(" MANGO_INSTANCE_SIGNATURE IPC socket path (set by the "
"compositor)\n\n");
printf("Run 'mmsg --help', '-h' or 'help' to see this message.\n");
}
int main(int argc, char *argv[]) { int main(int argc, char *argv[]) {
if (argc >= 2 &&
(strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-h") == 0 ||
strcmp(argv[1], "help") == 0)) {
usage();
return EXIT_SUCCESS;
}
if (argc < 2) { if (argc < 2) {
fprintf(stderr, "Usage: mmsg <command> [args...]\n"); fprintf(stderr, "Usage: mmsg <command> [args...]\n");
fprintf(stderr, " get <type> ... one-shot request\n"); fprintf(stderr, " get <type> ... one-shot request\n");
@ -37,7 +103,6 @@ int main(int argc, char *argv[]) {
return EXIT_FAILURE; return EXIT_FAILURE;
} }
// 拼接命令,缓冲区大小 4096 以容纳较长参数
char cmd[4096] = {0}; char cmd[4096] = {0};
int offset = 0; int offset = 0;
for (int i = 1; i < argc; i++) { for (int i = 1; i < argc; i++) {
@ -51,7 +116,6 @@ int main(int argc, char *argv[]) {
offset += n; offset += n;
} }
// 添加换行符
int n = snprintf(cmd + offset, sizeof(cmd) - offset, "\n"); int n = snprintf(cmd + offset, sizeof(cmd) - offset, "\n");
if (n < 0 || n >= (int)(sizeof(cmd) - offset)) { if (n < 0 || n >= (int)(sizeof(cmd) - offset)) {
fprintf(stderr, "Error: command too long to append newline.\n"); fprintf(stderr, "Error: command too long to append newline.\n");
@ -59,14 +123,12 @@ int main(int argc, char *argv[]) {
return EXIT_FAILURE; return EXIT_FAILURE;
} }
// 发送命令,使用 MSG_NOSIGNAL 避免 SIGPIPE
if (send(sock, cmd, strlen(cmd), MSG_NOSIGNAL) < 0) { if (send(sock, cmd, strlen(cmd), MSG_NOSIGNAL) < 0) {
perror("send"); perror("send");
close(sock); close(sock);
return EXIT_FAILURE; return EXIT_FAILURE;
} }
// 将 socket 封装为行缓冲文件流,自动处理 TCP 拆包,按完整行读取
FILE *stream = fdopen(sock, "r"); FILE *stream = fdopen(sock, "r");
if (!stream) { if (!stream) {
perror("fdopen"); perror("fdopen");
@ -74,7 +136,6 @@ int main(int argc, char *argv[]) {
return EXIT_FAILURE; return EXIT_FAILURE;
} }
// 按行读取并输出直到连接关闭get 模式服务端主动 close或出错
char *line = NULL; char *line = NULL;
size_t len = 0; size_t len = 0;
while (getline(&line, &len, stream) != -1) { while (getline(&line, &len, stream) != -1) {
@ -82,11 +143,10 @@ int main(int argc, char *argv[]) {
fflush(stdout); fflush(stdout);
} }
// 检查是否因读取错误退出(而非正常 EOF
if (ferror(stream)) { if (ferror(stream)) {
perror("recv"); perror("recv");
free(line); free(line);
fclose(stream); // 关闭 stream 同时关闭 socket fclose(stream);
return EXIT_FAILURE; return EXIT_FAILURE;
} }