Quitting the TUI now asks whether to shut down or detach
This commit is contained in:
parent
c887a6c9b8
commit
7db8d2092a
|
|
@ -315,6 +315,14 @@ std::string formatDegrees(double deg);
|
||||||
// Returns "—" when then_ms is 0 (never).
|
// Returns "—" when then_ms is 0 (never).
|
||||||
std::string formatTimeAgo(long long now_ms, long long then_ms);
|
std::string formatTimeAgo(long long now_ms, long long then_ms);
|
||||||
|
|
||||||
|
// Whether the dashboard can be sent to the background, i.e. whether it is
|
||||||
|
// running inside a tmux pane that a client can be detached from. Pass getenv
|
||||||
|
//("TMUX"): tmux sets it to the server socket path, so a set, non-empty value is
|
||||||
|
// both the test and what lets `tmux detach-client` find the right client.
|
||||||
|
// Takes the value as an argument rather than reading the environment itself so
|
||||||
|
// the rule is testable.
|
||||||
|
bool backgroundAvailable(const char* tmux_env);
|
||||||
|
|
||||||
// Last path segment of an MQTT topic ("GGS/FWT/Tower/CamEvent" -> "CamEvent").
|
// Last path segment of an MQTT topic ("GGS/FWT/Tower/CamEvent" -> "CamEvent").
|
||||||
// Every row in the connectivity view shares the same prefix, which only eats
|
// Every row in the connectivity view shares the same prefix, which only eats
|
||||||
// width. Returns the input unchanged when there is no '/'.
|
// width. Returns the input unchanged when there is no '/'.
|
||||||
|
|
|
||||||
10
scripts/fgc
10
scripts/fgc
|
|
@ -14,8 +14,8 @@
|
||||||
# scripts/fgc peek print what the pane shows right now
|
# scripts/fgc peek print what the pane shows right now
|
||||||
# scripts/fgc restart [args...] stop, then start
|
# scripts/fgc restart [args...] stop, then start
|
||||||
#
|
#
|
||||||
# Detaching is Ctrl-b d. NOTE: the dashboard's own `q` key *exits the program*,
|
# Detaching is Ctrl-b d. The dashboard's own `q` asks first, offering both:
|
||||||
# it does not detach — that is the one thing to get right when attached.
|
# quit the program, or background it (which detaches, same as Ctrl-b d).
|
||||||
#
|
#
|
||||||
# Only one instance may run. The binary enforces that itself with a lockfile
|
# Only one instance may run. The binary enforces that itself with a lockfile
|
||||||
# (see src/core/SingleInstance.cpp), so a bare ./build/fire_gimbal_control is
|
# (see src/core/SingleInstance.cpp), so a bare ./build/fire_gimbal_control is
|
||||||
|
|
@ -159,7 +159,7 @@ cmd_start() {
|
||||||
tmux set-option -t "$pane" status-left-length 30 >/dev/null
|
tmux set-option -t "$pane" status-left-length 30 >/dev/null
|
||||||
tmux set-option -t "$pane" status-right-length 80 >/dev/null
|
tmux set-option -t "$pane" status-right-length 80 >/dev/null
|
||||||
tmux set-option -t "$pane" status-right \
|
tmux set-option -t "$pane" status-right \
|
||||||
' Ctrl-b d = detach, keeps running | q = quit the program ' >/dev/null
|
' Ctrl-b d = detach, keeps running | q = quit or background ' >/dev/null
|
||||||
|
|
||||||
tmux respawn-pane -k -t "$win" -c "$repo" "$launch" >/dev/null
|
tmux respawn-pane -k -t "$win" -c "$repo" "$launch" >/dev/null
|
||||||
|
|
||||||
|
|
@ -182,7 +182,7 @@ cmd_start() {
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "fgc: started detached as PID $(pane_pid) (session '$session')"
|
echo "fgc: started detached as PID $(pane_pid) (session '$session')"
|
||||||
echo " attach: scripts/fgc attach (Ctrl-b d to detach; 'q' quits the program)"
|
echo " attach: scripts/fgc attach (Ctrl-b d to detach; 'q' asks quit/background)"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Hidden: the command tmux runs inside the pane. Not for direct use.
|
# Hidden: the command tmux runs inside the pane. Not for direct use.
|
||||||
|
|
@ -228,7 +228,7 @@ cmd_attach() {
|
||||||
if [[ -n "${TMUX:-}" ]]; then
|
if [[ -n "${TMUX:-}" ]]; then
|
||||||
die "already inside tmux; use 'tmux switch-client -t $session'"
|
die "already inside tmux; use 'tmux switch-client -t $session'"
|
||||||
fi
|
fi
|
||||||
echo "fgc: attaching — Ctrl-b d detaches and leaves it running ('q' would quit it)" >&2
|
echo "fgc: attaching — Ctrl-b d detaches and leaves it running ('q' asks first)" >&2
|
||||||
exec tmux attach-session -t "$target"
|
exec tmux attach-session -t "$target"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -132,6 +132,11 @@ const std::vector<HelpSection>& helpCatalog() {
|
||||||
"still, and the LOG title shows 'scrollback'."}},
|
"still, and the LOG title shows 'scrollback'."}},
|
||||||
{"Home/End",
|
{"Home/End",
|
||||||
"Jump to the top/bottom; on the log, End resumes following the tail.", {}},
|
"Jump to the top/bottom; on the log, End resumes following the tail.", {}},
|
||||||
|
{"q",
|
||||||
|
"Ask whether to quit or leave the dashboard running in the background.", {
|
||||||
|
"Quitting stops capture and closes the camera; backgrounding detaches the",
|
||||||
|
"tmux client (same as Ctrl-b d) and leaves everything running. Ctrl-C still",
|
||||||
|
"shuts down immediately."}},
|
||||||
{"p",
|
{"p",
|
||||||
"Freeze the 10 Hz repaint so terminal text selection survives.", {
|
"Freeze the 10 Hz repaint so terminal text selection survives.", {
|
||||||
"Any keypress repaints, so scrolling while paused drops the selection -",
|
"Any keypress repaints, so scrolling while paused drops the selection -",
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
|
#include <cstdlib>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
|
@ -209,6 +210,27 @@ Element keyBar(Overlay overlay) {
|
||||||
return flexbox(std::move(e));
|
return flexbox(std::move(e));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The "are you sure?" block that replaces the key bar when q is pressed.
|
||||||
|
//
|
||||||
|
// q and Ctrl-b d are one keystroke apart and mean opposite things when the tower
|
||||||
|
// is operated over ssh, and nothing on screen said so at the moment of choosing
|
||||||
|
// (scripts/fgc resorts to warning about it in its tmux status line). This asks.
|
||||||
|
Element exitPrompt(bool can_background) {
|
||||||
|
Element bg = can_background
|
||||||
|
? hbox({text(" b ") | bold,
|
||||||
|
text("Background \xE2\x80\x94 detach, leave it running "
|
||||||
|
"(same as Ctrl-b d)")})
|
||||||
|
: hbox({text(" b ") | dim,
|
||||||
|
text("Background \xE2\x80\x94 unavailable, needs scripts/fgc") | dim});
|
||||||
|
return vbox({
|
||||||
|
text(" Exit the dashboard? ") | bold | color(Color::Black) | bgcolor(Color::Yellow),
|
||||||
|
hbox({text(" q ") | bold,
|
||||||
|
text("Quit \xE2\x80\x94 stops capture, closes the camera, ends the program")}),
|
||||||
|
bg,
|
||||||
|
hbox({text(" Esc ") | bold, text("Cancel") | dim}),
|
||||||
|
}) | border | color(Color::Yellow);
|
||||||
|
}
|
||||||
|
|
||||||
Element badge(const std::string& s, bool on, Color on_color) {
|
Element badge(const std::string& s, bool on, Color on_color) {
|
||||||
auto e = text(" " + s + " ");
|
auto e = text(" " + s + " ");
|
||||||
return on ? (e | color(on_color) | bold) : (e | dim);
|
return on ? (e | color(on_color) | bold) : (e | dim);
|
||||||
|
|
@ -1044,6 +1066,7 @@ void TuiUi::uiLoop() {
|
||||||
int help_sel = 0;
|
int help_sel = 0;
|
||||||
bool gimbal_dump_requested = false; // auto-pull a dump the first time
|
bool gimbal_dump_requested = false; // auto-pull a dump the first time
|
||||||
bool calib_prompt = false; // a yes/no calib-save question is showing
|
bool calib_prompt = false; // a yes/no calib-save question is showing
|
||||||
|
bool exit_prompt = false; // 'q' asked; awaiting quit/background/cancel
|
||||||
|
|
||||||
// Per-pane scroll positions. UI-thread-only view state (the renderer lambda
|
// Per-pane scroll positions. UI-thread-only view state (the renderer lambda
|
||||||
// and CatchEvent below both run on this thread), so unlike screen_/paused_
|
// and CatchEvent below both run on this thread), so unlike screen_/paused_
|
||||||
|
|
@ -1101,7 +1124,8 @@ void TuiUi::uiLoop() {
|
||||||
|
|
||||||
Element bottom = command_mode
|
Element bottom = command_mode
|
||||||
? hbox({text(" : ") | inverted, input->Render() | flex}) | border
|
? hbox({text(" : ") | inverted, input->Render() | flex}) | border
|
||||||
: keyBar(overlay);
|
: exit_prompt ? exitPrompt(backgroundAvailable(std::getenv("TMUX")))
|
||||||
|
: keyBar(overlay);
|
||||||
|
|
||||||
// An open overlay takes over the whole body (the dashboard panels are
|
// An open overlay takes over the whole body (the dashboard panels are
|
||||||
// hidden) so it has the full height — otherwise tall content like the
|
// hidden) so it has the full height — otherwise tall content like the
|
||||||
|
|
@ -1167,6 +1191,31 @@ void TuiUi::uiLoop() {
|
||||||
}
|
}
|
||||||
return false; // let the Input edit the buffer
|
return false; // let the Input edit the buffer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The exit question owns the keyboard while it is up: every key is
|
||||||
|
// consumed, so a stray press cannot reach the dashboard behind it.
|
||||||
|
if (exit_prompt) {
|
||||||
|
if (e == Event::Escape) { exit_prompt = false; return true; }
|
||||||
|
if (e == Event::Return) { exit_prompt = false; if (sink_) sink_("exit"); return true; }
|
||||||
|
if (e.is_character()) {
|
||||||
|
const std::string& k = e.character();
|
||||||
|
if (k == "q" || k == "Q") {
|
||||||
|
exit_prompt = false;
|
||||||
|
if (sink_) sink_("exit");
|
||||||
|
} else if ((k == "b" || k == "B") && backgroundAvailable(std::getenv("TMUX"))) {
|
||||||
|
exit_prompt = false;
|
||||||
|
// Detach the client viewing this pane; the program keeps
|
||||||
|
// running and drawing into the pty, exactly as Ctrl-b d does.
|
||||||
|
// A failure must never read as "quit", so on error we say so
|
||||||
|
// and stay put.
|
||||||
|
if (std::system("tmux detach-client") != 0)
|
||||||
|
LOG_WARN << "could not detach from tmux; still running "
|
||||||
|
"(detach with Ctrl-b d, or press q to quit)";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true; // swallow everything else
|
||||||
|
}
|
||||||
|
|
||||||
const int n = static_cast<int>(helpCatalog().size());
|
const int n = static_cast<int>(helpCatalog().size());
|
||||||
if (overlay == Overlay::Help) { // help pane navigation
|
if (overlay == Overlay::Help) { // help pane navigation
|
||||||
// Selecting a different section replaces the body wholesale, so the
|
// Selecting a different section replaces the body wholesale, so the
|
||||||
|
|
@ -1287,7 +1336,7 @@ void TuiUi::uiLoop() {
|
||||||
if (c == "j") { help_sel = (help_sel + 1) % n; help_scroll = {}; return true; }
|
if (c == "j") { help_sel = (help_sel + 1) % n; help_scroll = {}; return true; }
|
||||||
if (c == "k") { help_sel = (help_sel - 1 + n) % n; help_scroll = {}; return true; }
|
if (c == "k") { help_sel = (help_sel - 1 + n) % n; help_scroll = {}; return true; }
|
||||||
}
|
}
|
||||||
if (c == "q") { if (sink_) sink_("exit"); return true; }
|
if (c == "q") { exit_prompt = true; return true; }
|
||||||
if (c == "s") { if (sink_) sink_("start"); return true; }
|
if (c == "s") { if (sink_) sink_("start"); return true; }
|
||||||
if (c == "x") { if (sink_) sink_("stop"); return true; }
|
if (c == "x") { if (sink_) sink_("stop"); return true; }
|
||||||
if (c == "h") { if (sink_) sink_("gimbal home"); return true; }
|
if (c == "h") { if (sink_) sink_("gimbal home"); return true; }
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,12 @@ std::string formatTimeAgo(long long now_ms, long long then_ms) {
|
||||||
return buf;
|
return buf;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool backgroundAvailable(const char* tmux_env) {
|
||||||
|
// An empty TMUX is not "in tmux": the variable exists but names no server,
|
||||||
|
// so detach-client would have nothing to talk to.
|
||||||
|
return tmux_env != nullptr && *tmux_env != '\0';
|
||||||
|
}
|
||||||
|
|
||||||
std::string topicLeaf(const std::string& topic) {
|
std::string topicLeaf(const std::string& topic) {
|
||||||
const auto slash = topic.rfind('/');
|
const auto slash = topic.rfind('/');
|
||||||
if (slash == std::string::npos) return topic;
|
if (slash == std::string::npos) return topic;
|
||||||
|
|
|
||||||
|
|
@ -118,3 +118,9 @@ TEST_CASE("formatClockMs renders wall-clock time, and never for 0") {
|
||||||
CHECK(t[8] == '.');
|
CHECK(t[8] == '.');
|
||||||
CHECK(t.substr(9) == "678"); // the ms field comes straight off the input
|
CHECK(t.substr(9) == "678"); // the ms field comes straight off the input
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("backgroundAvailable only accepts a set, non-empty TMUX") {
|
||||||
|
CHECK(backgroundAvailable("/tmp/tmux-1000/default,12345,0"));
|
||||||
|
CHECK_FALSE(backgroundAvailable(nullptr)); // not under tmux at all
|
||||||
|
CHECK_FALSE(backgroundAvailable("")); // set but names no server
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue