Compare commits

...

10 Commits

Author SHA1 Message Date
4bda2718ac add nix flake
Some checks failed
Build & run tests / build (macos-latest) (push) Has been cancelled
Build & run tests / build (ubuntu-latest) (push) Has been cancelled
2025-08-26 14:26:43 -07:00
44a16ff4e2 NEWS.org: update 2025-08-24 17:45:42 +03:00
d66a29cf5e scm: add support for logging
Add functions debug, info, warning, critical which log to the mu logging
facilities.
2025-08-24 17:36:15 +03:00
a69e6fad1d mu4e: implement mu4e-mu-scm-repl
With `mu4e-mu-scm-server` set to non-nil, a mu with scm support / --listen will
start a Guile SCM REPL listening on a Unix Domain Socket.

You can connect to this socket using the command mu4e-mu-scm-repl; this depends
on the geiser-guile package.
2025-08-24 11:54:08 +03:00
02a0dc4333 mu-server: implement exposing the socket-path
Add the scm-socket-path to the ping-properties we expose for mu4e.
2025-08-24 11:54:08 +03:00
9a2d481fc3 mu: add --listen option for server
Add a --listen option for the server

Rework the option code to share the --listen / socket-path code between scm and
server subcommands.

Move option off the stack in mu.cc, seems it's too big, at least when using gdb.
2025-08-24 11:54:08 +03:00
d5a0fce4cf scm: implement blocking / non-blocking modes
Implement running the REPL on background thread. That way, we can _share_ the
store&.
2025-08-24 11:54:08 +03:00
81ff303d2e meson.build: check for pthread_setname_np 2025-08-23 09:12:47 +03:00
6b89f4abae scm: message_parts: use cons instead of append
It's faster.
2025-08-22 08:13:55 +03:00
3b7e5507f7 scm: to_scm: use cons, not append
It's faster.
2025-08-22 08:08:32 +03:00
20 changed files with 612 additions and 191 deletions

View File

@ -293,23 +293,31 @@
property ~:hide-if-no-unread~, which hides the maildir/bookmark from the
main-view if there are no unread messages which the corresponding query.
- 1.12.12: it now possible to create Emacs bookmarks for both messages
- 1.12.12: it now possible to create Emacs bookmarks for both messages
(default) and queries. See the new variable ~mu4e-emacs-bookmark-policy~.
- 1.12.12: ~mu4e-get-mail-command~ which specifies the shell command to use for
getting mail, can now also be a function return that shell-command. This
makes it easier to use different shell commands in different situations.
- 1.12.12: ~mu4e-get-mail-command~ which specifies the shell command to use
for getting mail, can now also be a function return that shell-command.
This makes it easier to use different shell commands in different
situations.
- 1.12.13: with an SCM-enabled ~mu~, you can set ~mu4e-mu-scm-server~ to non-nil
and connect to the SCM REPL with ~M-x mu4e-mu-scm-repl~ after restart (see
their docstrings for details)
*** scm
- 1.12.12: add new guile/scheme binding in ~scm/~. These are to replace the
long-deprecated ~guile/~ bindings. For now, this is all rather new and
experimental, but the basics are there.
- 1.12.12: add new guile/scheme binding in ~scm/~. These are to replace the
long-deprecated ~guile/~ bindings. For now, this is all rather new and
experimental, but the basics are there.
This requires a slightly newer gmime (3.2.8?) than the one ~mu~ requires.
This requires a slightly newer gmime (3.2.8?) than the one ~mu~ requires.
- 1.12.13: add the ~--listen~ flag for ~mu scm~, to start a REPL on a Unix domain
sockets. See the reference manual for details.
- 1.12.13: add the ~--listen~ flag for ~mu scm~ and ~mu server~, to start a REPL
on a Unix domain socket. See the reference manual for details.
- 1.12.13: ~mu scm~ also gained support for labels and logging; furthermore,
~mfind~ was made much faster.
*** Contributors

27
flake.lock generated Normal file
View File

@ -0,0 +1,27 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1756125398,
"narHash": "sha256-XexyKZpf46cMiO5Vbj+dWSAXOnr285GHsMch8FBoHbc=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "3b9f00d7a7bf68acd4c4abb9d43695afb04e03a5",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

72
flake.nix Normal file
View File

@ -0,0 +1,72 @@
{
description = "mu and mu4e email package for emacs";
inputs = { nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable"; };
outputs = { self, nixpkgs }: {
packages.x86_64-linux.default =
with import nixpkgs { system = "x86_64-linux"; };
stdenv.mkDerivation {
name = "mu4e";
src = self;
nativeBuildInputs = [
pkg-config
meson
ninja
texinfo
#installShellFiles
emacs
python3
#makeWrapper # need?
cmake
cld2
];
buildInputs = [ glib gmime3 xapian ];
mesonFlags = [
# "--debug"
"-Dguile=disabled"
"-Dreadline=disabled"
"-Demacs=${emacs}/bin/emacs"
];
# preConfigure = ''
# echo "=== DEBUG: Environment before meson configure ==="
# echo "PWD: $(pwd)"
# echo "Python version:"
# python3 --version || echo "python3 not available"
# echo "Python path:"
# #which python3 || echo "python3 not in PATH"
# echo "PATH: $PATH"
# echo "Meson version:"
# meson --version
# echo "Available files in current directory:"
# ls -la
# echo "Checking for Python scripts:"
# find . -name "*.py" -type f | head -10
# echo "=============================================="
# '';
meta = with lib; {
description = "Email indexer and Emacs client (mu4e)";
longDescription = ''
mu is a tool for dealing with email messages stored in the
Maildir-format. mu's purpose in life is to help you to quickly find the
messages you need; in addition, it allows you to view messages, extract
attachments, create new maildirs, and so on.
mu4e is an Emacs-based e-mail client that uses mu as its back-end.
'';
license = licenses.gpl3Plus;
homepage = "https://www.djcbsoftware.nl/code/mu/";
changelog = "https://github.com/djcb/mu/blob/v${version}/NEWS.org";
maintainers = with maintainers; [ antono chvp peterhoeg ];
platforms = platforms.unix;
mainProgram = "mu";
};
};
};
}

View File

@ -132,7 +132,8 @@ struct Server::Private {
{}
~Private() {
indexer().stop();
if (have_indexer_)
indexer().stop();
if (!tmp_dir_.empty())
remove_directory(tmp_dir_);
}
@ -145,7 +146,7 @@ struct Server::Private {
// acccessors
Store& store() { return store_; }
const Store& store() const { return store_; }
Indexer& indexer() { return store().indexer(); }
Indexer& indexer() { have_indexer_ = true; return store().indexer(); }
void do_index(const Indexer::Config& conf);
//CommandMap& command_map() const { return command_map_; }
@ -212,6 +213,8 @@ private:
const CommandHandler command_handler_;
std::atomic<bool> keep_going_{};
std::string tmp_dir_;
bool have_indexer_{};
};
static void
@ -1021,15 +1024,19 @@ Server::Private::ping_handler(const Command& cmd)
for (auto&& addr : store().config().get<Config::Id::PersonalAddresses>())
addrs.add(addr);
output_sexp(Sexp()
.put_props(":pong", "mu")
.put_props(":props",
Sexp().put_props(
":version", VERSION,
":personal-addresses", std::move(addrs),
":database-path", store().path(),
":root-maildir", store().root_maildir(),
":doccount", storecount)));
Sexp props = Sexp().put_props(
":version", VERSION,
":personal-addresses", std::move(addrs),
":database-path", store().path(),
":root-maildir", store().root_maildir(),
":doccount", storecount);
if (!options_.socket_path.empty())
props.put_props(
":scm-socket-path", options_.socket_path);
output_sexp(Sexp().put_props(":pong", "mu")
.put_props(":props", std::move(props)));
}
void
@ -1107,7 +1114,9 @@ Server::Private::view_mark_as_read(Store::Id docid, Message&& msg, bool rename)
if (rename)
move_opts |= Store::MoveOptions::ChangeName;
const auto ids{Store::id_vec(unwrap(store().move_message(docid, {}, nflags, move_opts)))};
const auto ids{Store::id_vec(
unwrap(store().move_message(docid, {},
nflags, move_opts)))};
for (auto&& [id, moved_msg]: store().find_messages(ids))
output(mu_format("({} {})", id == docid ? ":view" : ":update",
msg_sexp_str(moved_msg, id, {})));
@ -1142,7 +1151,10 @@ Server::Private::view_handler(const Command& cmd)
Server::Server(Store& store, const Server::Options& opts, Server::Output output)
: priv_{std::make_unique<Private>(store, opts, output)}
{}
{
mu_message("created server with store @ {}; maildir @ {}",
store.path(), store.root_maildir());
}
Server::~Server() = default;

View File

@ -51,6 +51,7 @@ public:
struct Options {
bool allow_temp_file; /**< temp file optimization allowed? */
std::string socket_path; /**< Socket path or empty */
};
/**

View File

@ -61,10 +61,16 @@ directly through the emacs process input/output. This is noticeably faster for
commands with a lot of output, esp. when the the temp-file uses a in-memory
file-system.
** --listen
If set, the server starts an SCM REPL as well, which listens on a Unix domain
socket. This corresponds to the *--listen* options for *mu scm**.
The store object (including the Xapian database) is shared between the server and the REPL.
* PERFORMANCE
As an indication for the relative performance, we can simulate something ~mu4e~
does; we take overall time of 50 such requests:
does. We take the overall time of 50 such requests:
#+begin_src sh
time build/mu/mu server --allow-temp-file --eval '(find :query "\"\"" :include-related t :threads t :maxnum 50000)' >/dev/null
@ -78,7 +84,6 @@ time build/mu/mu server --allow-temp-file --eval '(find :query "\"\"" :include-r
| 1.10 | 5.7s |
| 1.11 (master) | 2.8s |
#+include: "muhome.inc" :minlevel 2
#+include: "common-options.inc" :minlevel 1
@ -87,4 +92,5 @@ time build/mu/mu server --allow-temp-file --eval '(find :query "\"\"" :include-r
* SEE ALSO
{{{man-link(mu,1)}}}
{{{man-link(mu,1)}}},
{{{man-link(mu-scm,1)}}}

View File

@ -165,6 +165,19 @@ else
message('no wordexp, no command-line option expansion')
endif
pthread_setname_np_code = '''
#include <pthread.h>
int main() { return pthread_setname_np(pthread_self(), "test"); }
'''
pthread_setname_np_check = cxx.compiles(
pthread_setname_np_code,
name: 'pthread_setname_np check',
args: '-D_GNU_SOURCE'
)
if pthread_setname_np_check
config_h_data.set('HAVE_PTHREAD_SETNAME_NP', 1)
endif
if not get_option('tests').disabled()
# only needed for tests
cp=find_program('cp')

View File

@ -1,5 +1,5 @@
/*
** Copyright (C) 2020-2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
** Copyright (C) 2020-2025 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
**
** This program is free software; you can redistribute it and/or modify it
** under the terms of the GNU General Public License as published by the
@ -29,6 +29,10 @@
#include "mu-cmd.hh"
#include "mu-server.hh"
#if BUILD_SCM
#include "scm/mu-scm.hh"
#endif/*BUILD_SCM*/
#include "utils/mu-utils.hh"
#include "utils/mu-readline.hh"
@ -60,6 +64,8 @@ install_sig_handler()
}
/*
* djcb@djcbsoftware.nl
*
* Markers for/after the length cookie that precedes the expression we write to
* output. We use octal 376, 377 (ie, 0xfe, 0xff) as they will never occur in
* utf8
@ -102,6 +108,46 @@ report_error(const Mu::Error& err) noexcept
Server::OutputFlags::Flush);
}
static void
maybe_setup_readline(const Mu::Options& opts)
{
tty = ::isatty(::fileno(stdout));
// Note, the readline stuff is inactive unless on a tty.
const auto histpath{opts.runtime_path(RuntimePath::Cache) + "/history"};
setup_readline(histpath, 50);
}
// get the SCM socket path we're listening on, or empty if not listening
static std::string
maybe_listen_path(const Mu::Store& store, const Mu::Options& opts)
{
#ifdef BUILD_SCM
if (!opts.scm.socket_path)
return {};
const auto res = Mu::Scm::run(store, opts, false/*!block*/);
if (!res) {
mu_warning("failed to start scm socket: {}", res.error().what());
return {};
} else
return *opts.scm.socket_path;
#endif /*BUILD_SCM*/
return {};
}
static bool
maybe_eval(Server& server, const Mu::Options& opts)
{
const auto eval = std::string{opts.server.commands ?
"(help :full t)" : opts.server.eval};
if (!eval.empty()) {
server.invoke(eval);
return true;
} else
return false;
}
Result<void>
Mu::mu_cmd_server(const Mu::Options& opts) try {
@ -110,31 +156,23 @@ Mu::mu_cmd_server(const Mu::Options& opts) try {
if (!store)
return Err(store.error());
Server::Options sopts{};
sopts.allow_temp_file = opts.server.allow_temp_file;
// empty when we're not listening
const auto socket_path = maybe_listen_path(*store, opts);
Server::Options sopts{opts.server.allow_temp_file, socket_path};
Server server{*store, sopts, output_stdout};
mu_message("created server with store @ {}; maildir @ {}; debug-mode {};"
"readline: {}",
store->path(), store->root_maildir(),
opts.debug ? "yes" : "no",
have_readline() ? "yes" : "no");
tty = ::isatty(::fileno(stdout));
const auto eval = std::string{opts.server.commands ? "(help :full t)" : opts.server.eval};
if (!eval.empty()) {
server.invoke(eval);
if (maybe_eval(server, opts))
return Ok();
}
// Note, the readline stuff is inactive unless on a tty.
const auto histpath{opts.runtime_path(RuntimePath::Cache) + "/history"};
setup_readline(histpath, 50);
maybe_setup_readline(opts);
install_sig_handler();
mu_println(";; Welcome to the " PACKAGE_STRING " command-server{}\n"
";; Use (help) to get a list of commands, (quit) to quit.",
mu_println(";; Welcome to the " PACKAGE_STRING " command-server{}",
opts.debug ? " (debug-mode)" : "");
if (!socket_path.empty())
mu_println(";; SCM socket listening on {}", socket_path);
mu_println(";; Use (help) to get a list of commands, (quit) to quit.");
bool do_quit{};
while (!MuTerminate && !do_quit) {
@ -151,7 +189,6 @@ Mu::mu_cmd_server(const Mu::Options& opts) try {
mu_message ("shutting down due to signal {}", MuTerminate.load());
shutdown_readline();
return Ok();
} catch (const Error& er) { /* note: user-level error, "OK" for mu */

View File

@ -1,5 +1,5 @@
/*
** Copyright (C) 2010-2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
** Copyright (C) 2010-2025 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
**
** This program is free software; you can redistribute it and/or modify it
** under the terms of the GNU General Public License as published by the
@ -73,7 +73,7 @@ cmd_scm(const Store& store, const Options& opts)
return Err(Error::Code::InvalidArgument,
"scm/guile is not available in this build");
#else
return Mu::Scm::run(Mu::Scm::Config{store, opts});
return Mu::Scm::run(store, opts, true/*blocking*/);
#endif /*BUILD_SCM*/
}

View File

@ -616,6 +616,24 @@ sub_remove(CLI::App& sub, Options& opts)
->type_name("<files>");
}
static std::string
concoct_socket_path()
{
return join_paths(g_get_user_runtime_dir(),
mu_format("mu-scm-{}.sock", ::getpid()));
}
static void
add_listen_flag(CLI::App& sub, Options& opts)
{
sub.add_flag("--listen", opts.scm.listen,
"Start SCM REPL on a domain socket");
sub.callback([&opts]{
if (opts.scm.listen)
opts.scm.socket_path = concoct_socket_path();
});
}
static void
sub_server(CLI::App& sub, Options& opts)
{
@ -627,18 +645,23 @@ sub_server(CLI::App& sub, Options& opts)
sub.add_flag("--allow-temp-file", opts.server.allow_temp_file,
"Allow for the temp-file optimization")
->excludes("--commands");
#if BUILD_SCM
add_listen_flag(sub, opts);
#endif/*BUILD_SCM*/
}
static void
sub_scm(CLI::App& sub, Options& opts)
{
sub.add_flag("--listen", opts.scm.listen,
"Start listening on a domain socket");
add_listen_flag(sub, opts);
sub.add_option("script-path", opts.scm.script_path, "Path to script")
->type_name("<path>")
->excludes("--listen");
sub.add_option("script-args", opts.scm.params, "Parameters for script")
->type_name("<parameters>");
}
static void
@ -787,7 +810,6 @@ AssocPairs<SubCommand, CommandInfo, Options::SubCommandNum> SubCommandInfos= {{
}};
static ScriptInfos
add_scripts(CLI::App& app, Options& opts)
{

View File

@ -46,13 +46,13 @@ struct Options {
/*
* general options
*/
bool quiet; /**< don't give any output */
bool debug; /**< log debug-level info */
bool version; /**< request mu version */
bool log_stderr; /**< log to stderr */
bool nocolor; /**< don't use use ansi-colors */
bool verbose; /**< verbose output */
std::string muhome; /**< alternative mu dir */
bool quiet; /**< don't give any output */
bool debug; /**< log debug-level info */
bool version; /**< request mu version */
bool log_stderr; /**< log to stderr */
bool nocolor; /**< don't use use ansi-colors */
bool verbose; /**< verbose output */
std::string muhome; /**< alternative mu dir */
/**
* Whether by default, we should show color
@ -260,6 +260,8 @@ struct Options {
OptString script_path; /**< Path to script (optional) */
StringVec params; /**< Parameters for script (after "--") */
bool listen; /**< Whether to start listening on a socket */
Option<std::string> socket_path; /**< path for the '--listen'
* Unix domain socket */
} scm;

View File

@ -77,6 +77,10 @@ handle_result(const Result<void>& res, const Mu::Options& opts)
return res.error().exit_code();
}
namespace {
Options opts; // it's big, don't want it on the stack
}
int
main(int argc, char* argv[]) try
{
@ -93,34 +97,36 @@ main(int argc, char* argv[]) try
/*
* read command-line options
*/
const auto opts{Options::make(argc, argv)};
if (!opts) {
output_error(opts.error().what(), !Options::default_no_color());
return opts.error().exit_code();
} else if (!opts->sub_command) {
auto optsres{Options::make(argc, argv)};
if (!optsres) {
output_error(optsres.error().what(), !Options::default_no_color());
return optsres.error().exit_code();
} else if (!optsres->sub_command) {
// nothing more to do.
return 0;
}
opts = std::move(*optsres);
// setup logging
Logger::Options lopts{Logger::Options::None};
if (opts->log_stderr)
if (opts.log_stderr)
lopts |= Logger::Options::StdOutErr;
if (opts->debug)
if (opts.debug)
lopts |= Logger::Options::Debug;
if (!!g_getenv("MU_TEST"))
lopts |= Logger::Options::File;
const auto logger{Logger::make(opts->runtime_path(RuntimePath::LogFile), lopts)};
const auto logger{Logger::make(opts.runtime_path(RuntimePath::LogFile), lopts)};
if (!logger) {
output_error(logger.error().what(), !opts->nocolor);
output_error(logger.error().what(), !opts.nocolor);
return logger.error().exit_code();
}
/*
* handle sub command
*/
return handle_result(mu_cmd_execute(*opts), *opts);
return handle_result(mu_cmd_execute(opts), opts);
// exceptions should have been handled earlier, but catch them here,
// just in case...

View File

@ -26,7 +26,6 @@
(require 'mu4e-helpers)
;;; Configuration
(defcustom mu4e-mu-home nil
"Location of an alternate mu home directory.
@ -93,6 +92,42 @@ stop/start mu4e."
:group 'mu4e
:safe 'booleanp)
(defcustom mu4e-mu-scm-server nil
"Tell the mu server to start an SCM/Guile REPL.
This starts the REPL on a Unix domain socket, which can be
connected to using various tools. This only works if mu has been
built with SCM support (if in doubt, check the output output of
\"mu info\" for the line \"scm-support\"); see Info
node `(mu-scm) Top' for details on mu-scm.
The REPL uses the same server instance that mu4e uses.
Changing this variable only affects the next time the mu4e
server is started."
:type 'boolean
:group 'mu4e
:safe 'booleanp)
(defun mu4e-mu-scm-repl ()
"Start a mu-scm REPL using geiser.
See `mu4e-mu-scm-server' to enable this; and requires the
`geiser-guile' package.
The REPL uses the same server instance that mu4e uses.
Note: this REPL is not to be confused with the mu REPL as per
`mu4e-server-repl'."
(interactive)
(unless (require 'geiser-guile nil 'noerror)
(mu4e-error "geiser-guile not found; please install"))
(let ((sock (plist-get (mu4e-server-properties) :scm-socket-path)))
(unless sock
(mu4e-error "socket-path unavailable"))
(when (fboundp 'geiser-connect-local)
(geiser-connect-local 'guile sock))))
;; Cached data
(defvar mu4e-maildir-list)
@ -467,6 +502,7 @@ As per issue #2198."
`(,(when mu4e-mu-debug "--debug")
"server"
,(when mu4e-mu-allow-temp-file "--allow-temp-file")
,(when mu4e-mu-scm-server "--listen")
,(when mu4e-mu-home (format "--muhome=%s" mu4e-mu-home)))))
(defun mu4e--version-check ()
@ -491,12 +527,15 @@ As per issue #2198."
(mu4e-message "Found mu version %s" version)))))
(defun mu4e-server-repl ()
"Start a mu4e-server repl.
"Start a mu4e-server REPL.
This is meant for debugging/testing - the repl is designed for
This is meant for debugging/testing - the REPL is designed for
machines, not for humans.
You cannot run the repl when mu4e is running (or vice-versa)."
You cannot run the REPL when mu4e is running (or vice-versa).
Not to be confused with the SCM/Guile REPL, as per
`mu4e-mu-scm-server'."
(interactive)
(if (mu4e-running-p)
(mu4e-error "Cannot run repl when mu4e is running")

View File

@ -171,14 +171,13 @@ subr_cc_message_parts(SCM message_scm) try {
auto mime_part{GMIME_PART(part.mime_object().object())};
SCM mime_part_scm{to_scm(mime_part)};
SCM alist_scm{to_scm(idx, parts[idx])};
SCM item{scm_cons(mime_part_scm, alist_scm)};
parts_scm = scm_append_x(
scm_list_2(parts_scm,
scm_list_1(
scm_cons(mime_part_scm, alist_scm))));
parts_scm = scm_cons(item, parts_scm);
}
return parts_scm;
return scm_reverse_x(parts_scm, SCM_EOL);
} catch (const ScmError& err) {
err.throw_scm();

View File

@ -21,7 +21,8 @@
;; after printing UNIX-CONNECT:<socket-file>\n on stdout
(let ((socket-path (getenv "MU_SCM_SOCKET_PATH")))
(when socket-path
(format #t "UNIX-CONNECT:~a\n" socket-path)
(info "listening on domain socket ~a" socket-path)
(format #t "~a\n" socket-path)
(run-server
(make-unix-domain-server-socket #:path socket-path))))

View File

@ -127,7 +127,8 @@ to_sort_field_id(SCM field, const char *func, int pos)
const auto sym{from_scm<std::string>(scm_symbol_to_string(field), func, pos)};
if (const auto field_opt{field_from_name(sym)}; !field_opt) {
throw ScmError{ScmError::Id::WrongType, func, pos, field, "sort-field-symbol"};
throw ScmError{ScmError::Id::WrongType, func, pos, field,
"sort-field-symbol"};
} else
return field_opt->id;
}
@ -144,7 +145,8 @@ subr_cc_store_mfind(SCM store_scm, SCM query_scm, SCM related_scm, SCM skip_dups
const auto skip_dups(from_scm<bool>(skip_dups_scm, func, 4));
if (!scm_is_false(sort_field_scm) && !scm_is_symbol(sort_field_scm))
throw ScmError{ScmError::Id::WrongType, func, 5, sort_field_scm, "#f or sort-field-symbol"};
throw ScmError{ScmError::Id::WrongType, func, 5, sort_field_scm,
"#f or sort-field-symbol"};
const auto sort_field_id = to_sort_field_id(sort_field_scm, func, 5);
const auto reverse(from_scm<bool>(reverse_scm, func, 6));

View File

@ -16,23 +16,31 @@
** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
**
*/
#include "config.h"
#include "mu-scm.hh"
#include <thread>
#include <unistd.h>
#include <errno.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include "mu-utils.hh"
#include "config.h"
#include "mu-scm-types.hh"
#ifdef HAVE_PTHREAD_SETNAME_NP
#include <pthread.h>
#endif
using namespace Mu;
using namespace Mu::Scm;
namespace {
static const Mu::Scm::Config *config{};
static SCM mu_mod; // The mu module
SCM mu_mod; // The mu module
}
/**
@ -57,14 +65,45 @@ init_options(const Options& opts)
}
static void
init_module_mu(void* _data)
init_misc()
{
init_options(config->options);
init_store(config->store);
init_message();
init_mime();
scm_define(make_symbol("level-critical"), to_scm(G_LOG_LEVEL_CRITICAL));
scm_define(make_symbol("level-warning"), to_scm(G_LOG_LEVEL_WARNING));
scm_define(make_symbol("level-info"), to_scm(G_LOG_LEVEL_INFO));
scm_define(make_symbol("level-debug"), to_scm(G_LOG_LEVEL_DEBUG));
}
static SCM
subr_cc_log(SCM level_scm, SCM str_scm) try {
constexpr auto func{"cc-log"};
const auto level{static_cast<GLogLevelFlags>(from_scm<int>(level_scm, func, 1))};
if (level != G_LOG_LEVEL_CRITICAL && level != G_LOG_LEVEL_WARNING &&
level != G_LOG_LEVEL_INFO && level != G_LOG_LEVEL_DEBUG)
throw ScmError{ScmError::Id::WrongType, func, 1, level_scm, "level"};
const auto str{from_scm<std::string>(str_scm, func, 2)};
g_log("mu-scm", level, "%s", str.c_str());
return SCM_UNSPECIFIED;
} catch (const ScmError& err) {
err.throw_scm();
}
static void
init_subrs()
{
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wcast-function-type"
scm_c_define_gsubr("cc-log", 2/*req*/, 0/*opt*/, 0/*rst*/,
reinterpret_cast<scm_t_subr>(subr_cc_log));
#pragma GCC diagnostic pop
}
static const Result<std::string>
make_mu_scm_path(const std::string& fname) {
@ -84,23 +123,20 @@ make_mu_scm_path(const std::string& fname) {
}
namespace {
static std::string mu_scm_path;
static std::string mu_scm_repl_path;
static std::string mu_scm_socket_path;
std::string mu_scm_path;
std::string mu_scm_repl_path;
std::string mu_scm_socket_path;
constexpr auto SOCKET_PATH_ENV = "MU_SCM_SOCKET_PATH";
using StrVec = std::vector<std::string>;
StrVec scm_args;
std::thread scm_worker;
}
static Result<void>
prepare_run(const Mu::Scm::Config& conf)
prepare_run(const Mu::Options& opts)
{
if (config)
return Err(Error{Error::Code::AccessDenied,
"already prepared"});
config = &conf;
// do a checks _before_ entering guile, so we get a bit more civilized
// error message.
if (const auto path = make_mu_scm_path("mu-scm.scm"); path)
mu_scm_path = *path;
else
@ -111,8 +147,8 @@ prepare_run(const Mu::Scm::Config& conf)
else
return Err(path.error());
if (config->options.scm.script_path) {
const auto path{config->options.scm.script_path->c_str()};
if (opts.scm.script_path) {
const auto path{opts.scm.script_path->c_str()};
if (const auto res = ::access(path, R_OK); res != 0) {
return Err(Error::Code::InvalidArgument,
"cannot read '{}': {}", path, ::strerror(errno));
@ -122,71 +158,130 @@ prepare_run(const Mu::Scm::Config& conf)
return Ok();
}
// make a unique unix-socket path
static std::string
maybe_set_uds_path(bool set)
static void
prepare_script(const Options& opts, StrVec& args)
{
if (set) {
GRand* grand{g_rand_new()};
auto path = join_paths(g_get_user_runtime_dir(),
mu_format("mu-scm-socket-{:08x}",
g_rand_int(grand)));
g_rand_free(grand);
g_setenv(SOCKET_PATH_ENV, path.c_str(), 1);
return path;
} else {
g_unsetenv(SOCKET_PATH_ENV);
return {};
static std::string cmd; // keep alive
// XXX: couldn't get another combination of -l/-s/-e/-c to work
// a) invokes `main' with arguments, and
// b) exits (rather than drop to a shell)
// but, what works is to manually specify (main ....)
cmd = "(main " + quote(*opts.scm.script_path);
for (const auto& scriptarg : opts.scm.params)
cmd += " " + quote(scriptarg);
cmd += ")";
args.emplace_back("-l");
args.emplace_back(*opts.scm.script_path);
args.emplace_back("-c");
args.emplace_back(cmd);
}
static void
maybe_remove_socket_path()
{
struct stat statbuf{};
const auto sock{mu_scm_socket_path};
// opportunistic, so no real warnings, but be careful deleting!
if (const int res = ::stat(sock.c_str(), &statbuf); res != 0) {
mu_debug("can't stat '{}'; err={}", sock, -res);
} else if ((statbuf.st_mode & S_IFMT) != S_IFSOCK) {
mu_debug("{} is not a socket", sock);
} else if (const int ulres = ::unlink(sock.c_str()); ulres != 0) {
mu_debug("failed to unlink '{}'; err={}", sock, -ulres);
} else {
mu_debug("unlinked {}", sock);
}
}
Result<void>
Mu::Scm::run(const Mu::Scm::Config& conf)
static void
prepare_shell(const Options& opts, StrVec& args)
{
if (const auto res = prepare_run(conf); !res)
return Err(res.error());
// drop us into an interactive shell/repl or start listening on a domain socket.
if (opts.scm.listen && opts.scm.socket_path) {
mu_scm_socket_path = *opts.scm.socket_path;
g_setenv(SOCKET_PATH_ENV, mu_scm_socket_path.c_str(), 1);
mu_info("setting up socket-path {}", mu_scm_socket_path);
::atexit(maybe_remove_socket_path); //opportunistic cleanup
}
else
g_unsetenv(SOCKET_PATH_ENV);
scm_boot_guile(0, {}, [](void *data, int argc, char **argv) {
mu_mod = scm_c_define_module ("mu", init_module_mu, {});
std::vector<const char*> args {
"mu",
"-l", mu_scm_path.c_str(),
};
std::string cmd;
const auto opts{config->options.scm};
// if a script-path was specified, run a script
if (opts.script_path) {
// XXX: couldn't get another combination of -l/-s/-e/-c to work
// a) invokes `main' with arguments, and
// b) exits (rather than drop to a shell)
// but, what works is to manually specify (main ....)
cmd = "(main " + quote(*opts.script_path);
for (const auto& scriptarg : opts.params)
cmd += " " + quote(scriptarg);
cmd += ")";
for (const auto& arg: {
"-l", opts.script_path->c_str(),
"-c", cmd.c_str()})
args.emplace_back(arg);
} else {
// otherwise, drop us into an interactive shell/repl
// or start listening on a domain socket.
mu_scm_socket_path =
maybe_set_uds_path(config->options.scm.listen);
args.emplace_back("--no-auto-compile");
args.emplace_back("-l");
args.emplace_back(mu_scm_repl_path.c_str());
}
/* ahem...*/
scm_shell(std::size(args), const_cast<char**>(args.data()));
}, {}); // never returns.
return Ok();
args.emplace_back("--no-auto-compile");
args.emplace_back("-l");
args.emplace_back(mu_scm_repl_path);
}
struct ModMuData { const Mu::Store& store; const Mu::Options& opts; };
static void
init_module_mu(void* data)
{
const ModMuData& conf{*reinterpret_cast<ModMuData*>(data)};
init_options(conf.opts);
init_misc();
init_subrs();
init_store(conf.store);
init_message();
init_mime();
}
static void
run_scm(const Mu::Store& store, const Mu::Options& opts)
{
static ModMuData mu_data{store, opts};
scm_boot_guile(0, {},
[](auto _data, auto _argc, auto _argv) {
mu_mod = scm_c_define_module ("mu", init_module_mu, &mu_data);
std::vector<char*> args;
std::transform(scm_args.begin(),
scm_args.end(), std::back_inserter(args),
[&](const std::string& strarg){
/* ahem...*/
return const_cast<char*>(strarg.c_str());
});
scm_shell(args.size(), args.data());
}, {}); // never returns.
}
Result<void>
Mu::Scm::run(const Mu::Store& store, const Mu::Options& opts, bool blocking)
{
if (const auto res = prepare_run(opts); !res)
return Err(res.error());
scm_args = {"mu", "-l", mu_scm_path};
// do env stuff _before_ starting guile / threads.
if (opts.scm.script_path)
prepare_script(opts, scm_args);
else
prepare_shell(opts, scm_args);
// in the non-blocking case, we start guile in a
// background thread; otherwise it will block.
if (!blocking) {
auto worker = std::thread([&](){
#ifdef HAVE_PTHREAD_SETNAME_NP
pthread_setname_np(pthread_self(), "mu-scm");
#endif /*HAVE_PTHREAD_SETNAME_NP*/
run_scm(store, opts);
});
worker.detach();
} else
run_scm(store, opts);
return Ok();
}
#ifdef BUILD_TESTS
@ -199,7 +294,6 @@ Mu::Scm::run(const Mu::Scm::Config& conf)
#include <mu-store.hh>
#include "utils/mu-test-utils.hh"
static void
test_scm_script()
{
@ -233,13 +327,8 @@ test_scm_script()
Mu::Options opts{};
opts.scm.script_path = join_paths(MU_SCM_SRCDIR, "mu-scm-test.scm");
Mu::Scm::Config scm_conf {
/*.store =*/ *store,
/*.options =*/ opts
};
{
const auto res = Mu::Scm::run(scm_conf);
const auto res = Mu::Scm::run(*store, opts, false /*blocks*/);
assert_valid_result(res);
}
}

View File

@ -39,28 +39,24 @@
*
*/
namespace Mu::Scm {
/**
* Configuration object
* Start a guile REPL or program
*
*/
struct Config {
const Mu::Store& store;
const Options& options;
};
/**
* Start a guile shell
*
* Initialize the Scm sub-system, then start a shell or run a script,
* Initialize the Scm sub-system, then start a REPL or run a script,
* based on the configuration.
*
* @param conf a Config object
* Unless 'blocking' is false or there is some pre-guile error, this
* method never returns. If blocking is false, it runs in the
* background.
*
* @param store a Store object
* @param opts options
* @param blocking whether to block (or run in the background)
*
* @return Ok() or some error
*/
Result<void> run(const Config& conf);
Result<void> run(const Store& store, const Options& opts,
bool blocking=true);
/**
* Helpers
@ -223,15 +219,18 @@ namespace Mu::Scm {
return scm_from_utf8_string(val);
else if constexpr (std::is_same_v<Type, std::vector<std::string>>) {
SCM lst{SCM_EOL};
for (const auto& s: val)
lst = scm_append_x(scm_list_2(lst,
scm_list_1(to_scm(s))));
for (auto it = val.end(); it-- != val.begin();)
lst = scm_cons(to_scm(*it), lst);
return lst;
}
else if constexpr (std::is_same_v<Type, bool>)
return scm_from_bool(val);
else if constexpr (std::is_same_v<Type, size_t>)
return scm_from_size_t(val);
else if constexpr (std::is_same_v<Type, int>)
return scm_from_int(val);
else if constexpr (std::is_same_v<Type, GLogLevelFlags>)
return scm_from_int(static_cast<int>(val));
else if constexpr (std::is_same_v<Type, int64_t>)
return scm_from_int64(val);
else if constexpr (std::is_same_v<Type, uint64_t>)

View File

@ -21,6 +21,7 @@
:use-module (system foreign)
:use-module (rnrs bytevectors)
:use-module (ice-9 optargs)
:use-module (ice-9 format)
:use-module (ice-9 binary-ports)
#:export (
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -100,11 +101,16 @@
%options
;; %preferences
;; logging
debug
info
warning
critical
;; helpers
string->time
time->string))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; Helpers
@ -637,3 +643,43 @@ UTC time.
(let ((t (if utc? (gmtime time-t) (localtime time-t))))
(strftime format t))
#f)))
;; Logging to mu's log
(define (mlog level frmstr . args)
"Log to the mu logger.
LEVEL is the logging-level, a symbol one of:
debug, info, warning, critical
FRMSTR is a format string like `(format)', and the ARGS are the parameters for
that format string."
(let ((msg (if (null? args)
frmstr
(apply format #f frmstr args))))
(cc-log level msg)))
(define (info frm . args)
"Log a message at info level to the mu logger.
FRM is the format string and ARGS are its arguments, see `(format)' for details
on the precise format."
(apply mlog level-info frm args))
(define (warning frm . args)
"Log a message at warning level to the mu logger.
FRM is the format string and ARGS are its arguments, see `(format)' for details
on the precise format."
(apply mlog level-warning frm args))
(define (critical frm . args)
"Log a message at critical level to the mu logger.
FRM is the format string and ARGS are its arguments, see `(format)' for details
on the precise format."
(apply mlog level-critical frm args))
(define (debug frm . args)
"Log a message at debug level to the mu logger.
FRM is the format string and ARGS are its arguments, see `(format)' for details
on the precise format."
(apply mlog level-debug frm args))

View File

@ -107,6 +107,7 @@ Indices
* Starting the REPL::
* Listening on a Unix Domain Socket::
* Hooking up with GNU/Emacs and Geiser::
* Hooking up with Mu4e::
@end menu
This chapter walks you through the installation and basic setup.
@ -183,19 +184,19 @@ REPL over a Unix domain socket, using the @t{--listen} flag.
When you start @command{mu scm} with the @t{--listen} flag, it prints a
(randomized) UNIX domain socket name and blocks after that; for instance:
@example
$mu scm --listen
UNIX-CONNECT:/run/user/1000/mu-scm-socket-6ef6222e
$ mu scm --listen
/run/user/1000/mu-scm-15269.sock
@end example
You can connect to this with exernal tools, for instance with @command{socat}:
You can connect to this with external tools, for instance with @command{socat}:
@example
socat - UNIX-CONNECT:/run/user/1000/mu-scm-socket-6ef6222e
$ socat - UNIX-CONNECT:/run/user/1000/mu-scm-15269.sock
GNU Guile 3.0.9
Copyright (C) 1995-2023 Free Software Foundation, Inc.
Guile comes with ABSOLUTELY NO WARRANTY; for details type `,show w'.
This program is free software, and you are welcome to redistribute it
under certain conditions; type `,show c' for details.
Guile comes with ABSOLUTELY NO WARRANTY; for details type `,show w'. This
program is free software, and you are welcome to redistribute it under certain
conditions; type `,show c' for details.
Enter `,help' for help.
scheme@@(guile-user)>
@ -216,7 +217,7 @@ domain socket, as discussed in @xref{Listening on a Unix Domain Socket}.
Assuming you have installed the @t{guile-geiser} package, the following snippet
makes that easy:
@lisp
(require 'guiser-guile)
(require 'geiser-guile)
(defvar mu-scm-listen-command "mu scm --listen"
"mu command to start an scm repl listening on a socket.")
@ -229,13 +230,32 @@ Connect to mu's scm (guile) interface through Geiser."
:name "*mu-scm-repl*"
:command `("sh" "-c" ,mu-scm-listen-command)
:filter (lambda (_proc chunk)
(when (string-match "^UNIX-CONNECT:\\(.*\\)$" chunk)
(when (stringng-match "^\\(mu-scm.*\\.sock\\)$" chunk)
(geiser-connect-local 'guile (match-string 1 chunk))))))
@end lisp
After evaluating this, you can use @command{M-x mu-scm-geiser-connect} to start
the REPL, with all the Geiser bells & whistles.
@node Hooking up with Mu4e
@section Hooking up with Mu4e
@cindex Mu4e
If you use @t{mu4e}, connecting to the SCM server is even easier than with
``plain'' Emacs.
First tell @t{mu4e} to starts it server with with @t{--listen} parameter:
@lisp
(setq mu4e-mu-scm-server t)
@end lisp
After that, (re)start @t{mu4e}.
Now should be able to connect to the REPL using @t{M-x mu4e-mu-scm-repl}. Like
@ref{Hooking up with GNU/Emacs and Geiser}, this depends on the @t{geiser-guile}
package.
The SCM instance uses the same database/store instance that @t{mu4e} uses.
@node Shell
@chapter Shell
@ -919,6 +939,26 @@ their default values.
@node Helpers
@section Helpers
@deffn {Scheme Procedure} debug frm . args
@end deffn
@deffn {Scheme Procedure} info frm . args
@end deffn
@deffn {Scheme Procedure} warning frm . args
@end deffn
@deffn {Scheme Procedure} critical frm . args
@end deffn
Log using @t{mu}'s logging facilities at the given level (debug, info, warning
or critical). @code{frm} and @code{args} are expected to be a format string and
its arguments, respectively, according to Guile's @code{format} function.
Note: where the log output exactly appears (i.e., log-file, journal, console,
nowhere) dependis on the particulars of the system and how mu was started; see
the @t{mu (1)} man-page for details on the latter.
@deffn {Scheme Procedure} string->time timestr [#:utc? (assoc-ref %preferences 'utc?)]
@end deffn
Convert some ISO-8601-style time-string to a seconds-since-epoch @t{time_t}