feature: ipc commands will not return errors with useful messages upon failure (#317)
Some checks are pending
Build & Test / build (push) Waiting to run
Snap Publish / Snap (amd64) (push) Waiting to run
Snap Publish / Snap (arm64) (push) Waiting to run
Snap Publish / Snap (armhf) (push) Waiting to run

- ipc commands will not return errors with useful messages upon failure
- created a CommandController interface so that the executor will be easier to test later on
This commit is contained in:
Matthew Kosarek 2024-12-07 16:41:44 -05:00 committed by GitHub
parent e879c66ece
commit f8554ecb58
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 336 additions and 318 deletions

View File

@ -77,6 +77,7 @@ add_library(miracle-wm-implementation
src/scratchpad.h src/scratchpad.cpp
src/compositor_state.h src/compositor_state.cpp
src/math_helpers.h
src/command_controller.h
)
add_executable(miracle-wm

86
src/command_controller.h Normal file
View File

@ -0,0 +1,86 @@
/**
Copyright (C) 2024 Matthew Kosarek
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 Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
**/
#ifndef MIRACLE_WM_COMMAND_CONTROLLER_H
#define MIRACLE_WM_COMMAND_CONTROLLER_H
#include "direction.h"
#include "output.h"
#include <optional>
#include <string>
namespace miracle
{
/// Abstract interface used to send commands to miracle.
class CommandController
{
public:
virtual bool try_request_horizontal() = 0;
virtual bool try_request_vertical() = 0;
virtual bool try_toggle_layout(bool cycle_through_all) = 0;
virtual void try_toggle_resize_mode() = 0;
virtual bool try_resize(Direction direction, int pixels) = 0;
virtual bool try_set_size(std::optional<int> const& width, std::optional<int> const& height) = 0;
virtual bool try_move(Direction direction) = 0;
virtual bool try_move_by(Direction direction, int pixels) = 0;
virtual bool try_move_to(int x, int y) = 0;
virtual bool try_select(Direction direction) = 0;
virtual bool try_select_parent() = 0;
virtual bool try_select_child() = 0;
virtual bool try_select_floating() = 0;
virtual bool try_select_tiling() = 0;
virtual bool try_select_toggle() = 0;
virtual bool try_close_window() = 0;
virtual bool quit() = 0;
virtual bool try_toggle_fullscreen() = 0;
virtual bool select_workspace(int number, bool back_and_forth = true) = 0;
virtual bool select_workspace(std::string const& name, bool back_and_forth) = 0;
virtual bool next_workspace() = 0;
virtual bool prev_workspace() = 0;
virtual bool back_and_forth_workspace() = 0;
virtual bool next_workspace_on_output(Output const&) = 0;
virtual bool prev_workspace_on_output(Output const&) = 0;
virtual bool move_active_to_workspace(int number, bool back_and_forth = true) = 0;
virtual bool move_active_to_workspace_named(std::string const&, bool back_and_forth) = 0;
virtual bool move_active_to_next_workspace() = 0;
virtual bool move_active_to_prev_workspace() = 0;
virtual bool move_active_to_back_and_forth() = 0;
virtual bool move_to_scratchpad() = 0;
virtual bool show_scratchpad() = 0;
virtual bool toggle_floating() = 0;
virtual bool toggle_pinned_to_workspace() = 0;
virtual bool set_is_pinned(bool) = 0;
virtual bool toggle_tabbing() = 0;
virtual bool toggle_stacking() = 0;
virtual bool set_layout(LayoutScheme scheme) = 0;
virtual bool set_layout_default() = 0;
virtual void move_cursor_to_output(Output const&) = 0;
virtual bool try_select_next_output() = 0;
virtual bool try_select_prev_output() = 0;
virtual bool try_select_output(Direction direction) = 0;
virtual bool try_select_output(std::vector<std::string> const& names) = 0;
virtual bool try_move_active_to_output(Direction direction) = 0;
virtual bool try_move_active_to_current() = 0;
virtual bool try_move_active_to_primary() = 0;
virtual bool try_move_active_to_nonprimary() = 0;
virtual bool try_move_active_to_next() = 0;
virtual bool try_move_active(std::vector<std::string> const& names) = 0;
};
}
#endif // MIRACLE_WM_COMMAND_CONTROLLER_H

View File

@ -51,6 +51,7 @@ public:
mir::geometry::Point cursor_position;
uint32_t modifiers = 0;
bool has_clicked_floating_window = false;
std::vector<std::shared_ptr<Output>> output_list;
[[nodiscard]] std::shared_ptr<Container> active() const;

View File

@ -225,12 +225,10 @@ json mode_event_to_json(WindowManagerMode mode)
Ipc::Ipc(miral::MirRunner& runner,
miracle::WorkspaceManager& workspace_manager,
Policy& policy,
std::shared_ptr<mir::ServerActionQueue> const& queue,
IpcCommandExecutor& executor,
std::shared_ptr<Config> const& config) :
workspace_manager { workspace_manager },
policy { policy },
queue { queue },
executor { executor },
config { config }
{
@ -528,14 +526,20 @@ void Ipc::handle_command(miracle::Ipc::IpcClient& client, uint32_t payload_lengt
{
mir::log_debug("Processing i3_command: %s", buf);
auto result = parse_i3_command(buf);
if (result)
if (result.success)
{
const std::string msg = "[{\"success\": true}]";
send_reply(client, payload_type, msg);
}
else
{
const std::string msg = "[{\"success\": false, \"parse_error\": true}]";
json j = json::array();
j.push_back({
{ "success", false },
{ "parse_error", result.parse_error },
{ "error", result.error },
});
const std::string msg = to_string(j);
send_reply(client, payload_type, msg);
}
break;
@ -759,26 +763,9 @@ void Ipc::handle_writeable(miracle::Ipc::IpcClient& client)
client.write_buffer_len = 0;
}
bool Ipc::parse_i3_command(const char* command)
IpcValidationResult Ipc::parse_i3_command(const char* command)
{
{
std::unique_lock lock(pending_commands_mutex);
IpcCommandParser parser(command);
pending_commands.push_back(parser.parse());
}
queue->enqueue(this, [&]()
{
size_t num_processed = 0;
{
std::shared_lock lock(pending_commands_mutex);
for (auto const& c : pending_commands)
executor.process(c);
num_processed = pending_commands.size();
}
std::unique_lock lock(pending_commands_mutex);
pending_commands.erase(pending_commands.begin(), pending_commands.begin() + num_processed);
});
return true;
IpcCommandParser parser(command);
auto const pending_commands = parser.parse();
return executor.process(pending_commands);
}

View File

@ -24,7 +24,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#include "workspace_manager.h"
#include "workspace_observer.h"
#include <mir/fd.h>
#include <mir/server_action_queue.h>
#include <miral/runner.h>
#include <shared_mutex>
#include <vector>
@ -84,7 +83,6 @@ public:
Ipc(miral::MirRunner& runner,
WorkspaceManager&,
Policy& policy,
std::shared_ptr<mir::ServerActionQueue> const&,
IpcCommandExecutor&,
std::shared_ptr<Config> const&);
~Ipc();
@ -113,9 +111,6 @@ private:
std::unique_ptr<miral::FdHandle> socket_handle;
sockaddr_un* ipc_sockaddr = nullptr;
std::vector<IpcClient> clients;
std::vector<IpcParseResult> pending_commands;
mutable std::shared_mutex pending_commands_mutex;
std::shared_ptr<mir::ServerActionQueue> queue;
IpcCommandExecutor& executor;
std::shared_ptr<Config> config;
@ -124,7 +119,7 @@ private:
void handle_command(IpcClient& client, uint32_t payload_length, IpcType payload_type);
void send_reply(IpcClient& client, IpcType command_type, std::string const& payload);
void handle_writeable(IpcClient& client);
bool parse_i3_command(const char* command);
IpcValidationResult parse_i3_command(const char* command);
};
}

View File

@ -27,6 +27,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#include "window_helpers.h"
#define MIR_LOG_COMPONENT "miracle"
#include <format>
#include <mir/log.h>
#include <miral/application_info.h>
@ -100,7 +101,7 @@ protected:
}
IpcCommandExecutor::IpcCommandExecutor(
miracle::Policy& policy,
CommandController& policy,
WorkspaceManager& workspace_manager,
CompositorState const& state,
AutoRestartingLauncher& launcher,
@ -113,49 +114,57 @@ IpcCommandExecutor::IpcCommandExecutor(
{
}
void IpcCommandExecutor::process(miracle::IpcParseResult const& command_list)
IpcValidationResult IpcCommandExecutor::process(miracle::IpcParseResult const& command_list)
{
IpcValidationResult result;
for (auto const& command : command_list.commands)
{
switch (command.type)
{
case IpcCommandType::exec:
process_exec(command, command_list);
result = process_exec(command, command_list);
break;
case IpcCommandType::split:
process_split(command, command_list);
result = process_split(command, command_list);
break;
case IpcCommandType::focus:
process_focus(command, command_list);
result = process_focus(command, command_list);
break;
case IpcCommandType::move:
process_move(command, command_list);
result = process_move(command, command_list);
break;
case IpcCommandType::sticky:
process_sticky(command, command_list);
result = process_sticky(command, command_list);
break;
case IpcCommandType::exit:
policy.quit();
result = {};
break;
case IpcCommandType::input:
process_input(command, command_list);
result = process_input(command, command_list);
break;
case IpcCommandType::workspace:
process_workspace(command, command_list);
result = process_workspace(command, command_list);
break;
case IpcCommandType::layout:
process_layout(command, command_list);
result = process_layout(command, command_list);
break;
case IpcCommandType::scratchpad:
process_scratchpad(command, command_list);
result = process_scratchpad(command, command_list);
break;
case IpcCommandType::resize:
process_resize(command, command_list);
result = process_resize(command, command_list);
break;
default:
result = parse_error(std::format("Unsupported command type: %d", (int)command.type));
break;
}
if (!result.success)
return result;
}
return {};
}
miral::Window IpcCommandExecutor::get_window_meeting_criteria(IpcParseResult const& command_list)
@ -176,23 +185,27 @@ miral::Window IpcCommandExecutor::get_window_meeting_criteria(IpcParseResult con
return miral::Window {};
}
void IpcCommandExecutor::process_exec(miracle::IpcCommand const& command, miracle::IpcParseResult const& command_list)
IpcValidationResult IpcCommandExecutor::parse_error(std::string error)
{
mir::log_error("Parse Error: %s", error.c_str());
return {
.success = false,
.parse_error = true,
.error = std::move(error)
};
}
IpcValidationResult IpcCommandExecutor::process_exec(miracle::IpcCommand const& command, miracle::IpcParseResult const& command_list)
{
if (command.arguments.empty())
{
mir::log_warning("process_exec: no arguments were supplied");
return;
}
return parse_error("process_exec: no arguments were supplied");
bool no_startup_id = false;
if (!command.options.empty() && command.options[0] == "--no-startup-id")
no_startup_id = true;
if (command.arguments.empty())
{
mir::log_warning("process_exec: argument does not have a command to run");
return;
}
return parse_error("process_exec: argument does not have a command to run");
std::string exec_cmd;
for (auto const& arg : command.arguments)
@ -202,15 +215,13 @@ void IpcCommandExecutor::process_exec(miracle::IpcCommand const& command, miracl
StartupApp app { exec_cmd, false, no_startup_id };
launcher.launch(app);
return {};
}
void IpcCommandExecutor::process_split(miracle::IpcCommand const& command, miracle::IpcParseResult const& command_list)
IpcValidationResult IpcCommandExecutor::process_split(miracle::IpcCommand const& command, miracle::IpcParseResult const& command_list)
{
if (command.arguments.empty())
{
mir::log_warning("process_split: no arguments were supplied");
return;
}
return parse_error("process_split: no arguments were supplied");
if (command.arguments.front() == "vertical")
{
@ -226,27 +237,27 @@ void IpcCommandExecutor::process_split(miracle::IpcCommand const& command, mirac
}
else
{
mir::log_warning("process_split: unknown argument %s", command.arguments.front().c_str());
return;
return parse_error(std::format("process_split: unknown argument {}", command.arguments.front().c_str()));
}
return {};
}
void IpcCommandExecutor::process_focus(IpcCommand const& command, IpcParseResult const& command_list)
IpcValidationResult IpcCommandExecutor::process_focus(IpcCommand const& command, IpcParseResult const& command_list)
{
// https://i3wm.org/docs/userguide.html#_focusing_moving_containers
if (command.arguments.empty())
{
if (command_list.scope.empty())
{
mir::log_warning("Focus command expected scope but none was provided");
return;
return parse_error("Focus command expected scope but none was provided");
}
auto window = get_window_meeting_criteria(command_list);
if (window)
window_controller.select_active_window(window);
return;
return {};
}
auto const& arg = command.arguments.front();
@ -254,8 +265,7 @@ void IpcCommandExecutor::process_focus(IpcCommand const& command, IpcParseResult
{
if (command_list.scope.empty())
{
mir::log_warning("Focus 'workspace' command expected scope but none was provided");
return;
return parse_error("Focus 'workspace' command expected scope but none was provided");
}
auto window = get_window_meeting_criteria(command_list);
@ -279,13 +289,10 @@ void IpcCommandExecutor::process_focus(IpcCommand const& command, IpcParseResult
{
auto container = state.active();
if (!container)
return;
return parse_error("Active container does nto exist");
if (container->get_type() != ContainerType::leaf)
{
mir::log_warning("Cannot focus prev when a tiling window is not selected");
return;
}
return parse_error("Cannot focus prev when a tiling window is not selected");
if (auto parent = Container::as_parent(container->get_parent().lock()))
{
@ -301,13 +308,10 @@ void IpcCommandExecutor::process_focus(IpcCommand const& command, IpcParseResult
{
auto container = state.active();
if (!container)
return;
return parse_error("No container is selected");
if (container->get_type() != ContainerType::leaf)
{
mir::log_warning("Cannot focus prev when a tiling window is not selected");
return;
}
return parse_error("Cannot focus prev when a tiling window is not selected");
if (auto parent = Container::as_parent(container->get_parent().lock()))
{
@ -328,10 +332,7 @@ void IpcCommandExecutor::process_focus(IpcCommand const& command, IpcParseResult
else if (arg == "output")
{
if (command.arguments.size() < 2)
{
mir::log_error("process_focus: 'focus output' must have more than two arguments");
return;
}
return parse_error("process_focus: 'focus output' must have more than two arguments");
auto const& arg1 = command.arguments[1];
if (arg1 == "next")
@ -352,6 +353,8 @@ void IpcCommandExecutor::process_focus(IpcCommand const& command, IpcParseResult
policy.try_select_output(names);
}
}
return {};
}
namespace
@ -385,21 +388,15 @@ bool parse_move_distance(std::vector<std::string> const& arguments, int& index,
}
}
void IpcCommandExecutor::process_move(IpcCommand const& command, IpcParseResult const& command_list)
IpcValidationResult IpcCommandExecutor::process_move(IpcCommand const& command, IpcParseResult const& command_list)
{
auto active_output = policy.get_active_output();
auto const& active_output = state.active_output;
if (!active_output)
{
mir::log_warning("process_move: output is not set");
return;
}
return parse_error("process_move: output is not set");
// https://i3wm.org/docs/userguide.html#_focusing_moving_containers
if (command.arguments.empty())
{
mir::log_warning("process_move: move command expects arguments");
return;
}
return parse_error("process_move: move command expects arguments");
int index = 0;
auto const& arg0 = command.arguments[index++];
@ -428,15 +425,12 @@ void IpcCommandExecutor::process_move(IpcCommand const& command, IpcParseResult
else if (arg0 == "position")
{
if (command.arguments.size() < 2)
{
mir::log_error("process_move: move position expected a third argument");
return;
}
return parse_error("process_move: move position expected a third argument");
auto const& arg1 = command.arguments[index++];
if (arg1 == "center")
{
auto active = policy.get_state().active().get();
auto active = state.active().get();
auto area = active_output->get_area();
float x = (float)area.size.width.as_int() / 2.f - (float)active->get_visible_area().size.width.as_int() / 2.f;
float y = (float)area.size.height.as_int() / 2.f - (float)active->get_visible_area().size.height.as_int() / 2.f;
@ -444,7 +438,7 @@ void IpcCommandExecutor::process_move(IpcCommand const& command, IpcParseResult
}
else if (arg1 == "mouse")
{
auto const& position = policy.get_cursor_position();
auto const& position = state.cursor_position;
policy.try_move_to((int)position.x.as_int(), (int)position.y.as_int());
}
else
@ -453,39 +447,28 @@ void IpcCommandExecutor::process_move(IpcCommand const& command, IpcParseResult
int move_distance_y;
if (!parse_move_distance(command.arguments, index, total_size, move_distance_x))
{
mir::log_error("process_move: move position <x> <y>: unable to parse x");
return;
}
return parse_error("process_move: move position <x> <y>: unable to parse x");
if (!parse_move_distance(command.arguments, index, total_size, move_distance_y))
{
mir::log_error("process_move: move position <x> <y>: unable to parse y");
return;
}
return parse_error("process_move: move position <x> <y>: unable to parse y");
policy.try_move_to(move_distance_x, move_distance_y);
}
return;
return {};
}
else if (arg0 == "absolute")
{
auto const& arg1 = command.arguments[index++];
auto const& arg2 = command.arguments[index++];
if (arg1 != "position")
{
mir::log_error("process_move: move [absolute] ... expected 'position' as the third argument");
return;
}
return parse_error("process_move: move [absolute] ... expected 'position' as the third argument");
if (arg2 != "center")
{
mir::log_error("process_move: move absolute position ... expected 'center' as the third argument");
return;
}
return parse_error("process_move: move absolute position ... expected 'center' as the third argument");
float x = 0, y = 0;
for (auto const& output : policy.get_output_list())
for (auto const& output : state.output_list)
{
auto area = output->get_area();
float end_x = (float)area.size.width.as_int() + (float)area.top_left.x.as_int();
@ -496,30 +479,24 @@ void IpcCommandExecutor::process_move(IpcCommand const& command, IpcParseResult
y = end_y;
}
auto active = policy.get_state().active();
auto active = state.active();
float x_pos = x / 2.f - (float)active->get_visible_area().size.width.as_int() / 2.f;
float y_pos = y / 2.f - (float)active->get_visible_area().size.height.as_int() / 2.f;
policy.try_move_to((int)x_pos, (int)y_pos);
return;
return {};
}
else if (arg0 == "window" || arg0 == "container")
{
auto const back_and_forth = std::find(command.options.begin(), command.options.end(), "--no-auto-back-and-forth") == command.options.end();
auto const& arg1 = command.arguments[index++];
if (arg1 != "to")
{
mir::log_error("process_move: expected 'to' after 'move window/container ...'");
return;
}
return parse_error("process_move: expected 'to' after 'move window/container ...'");
auto const& arg2 = command.arguments[index++];
if (arg2 == "workspace")
{
if (command.arguments.size() <= 3)
{
mir::log_error("process_move: expected another argument after 'move container/window to output...'");
return;
}
return parse_error("process_move: expected another argument after 'move container/window to output...'");
auto const& arg3 = command.arguments[index++];
int number = -1;
@ -527,17 +504,17 @@ void IpcCommandExecutor::process_move(IpcCommand const& command, IpcParseResult
{
// TODO: Do we need to care about the name here?
policy.move_active_to_workspace(number, back_and_forth);
return;
return {};
}
else if (arg3 == "next")
{
policy.move_active_to_next_workspace();
return;
return {};
}
else if (arg3 == "prev")
{
policy.move_active_to_prev_workspace();
return;
return {};
}
else if (arg3 == "current")
{
@ -546,12 +523,12 @@ void IpcCommandExecutor::process_move(IpcCommand const& command, IpcParseResult
else if (arg3 == "back_and_forth")
{
policy.move_active_to_back_and_forth();
return;
return {};
}
else
{
policy.move_active_to_workspace_named(arg3, back_and_forth);
return;
return {};
}
}
else if (arg2 == "output")
@ -559,7 +536,7 @@ void IpcCommandExecutor::process_move(IpcCommand const& command, IpcParseResult
if (command.arguments.size() <= 3)
{
mir::log_error("process_move: expected another argument after 'move container/window to output...'");
return;
return {};
}
auto const& arg3 = command.arguments[index++];
@ -589,7 +566,7 @@ void IpcCommandExecutor::process_move(IpcCommand const& command, IpcParseResult
else if (arg0 == "scratchpad")
{
policy.move_to_scratchpad();
return;
return {};
}
if (direction < Direction::MAX)
@ -600,15 +577,14 @@ void IpcCommandExecutor::process_move(IpcCommand const& command, IpcParseResult
else
policy.try_move(direction);
}
return {};
}
void IpcCommandExecutor::process_sticky(IpcCommand const& command, IpcParseResult const& command_list)
IpcValidationResult IpcCommandExecutor::process_sticky(IpcCommand const& command, IpcParseResult const& command_list)
{
if (command.arguments.empty())
{
mir::log_warning("process_sticky: expects arguments");
return;
}
return parse_error("process_sticky: expects arguments");
auto const& arg0 = command.arguments[0];
if (arg0 == "enable")
@ -619,10 +595,11 @@ void IpcCommandExecutor::process_sticky(IpcCommand const& command, IpcParseResul
policy.toggle_pinned_to_workspace();
else
mir::log_warning("process_sticky: unknown arguments: %s", arg0.c_str());
return {};
}
// This command will be
void IpcCommandExecutor::process_input(IpcCommand const& command, IpcParseResult const& command_list)
IpcValidationResult IpcCommandExecutor::process_input(IpcCommand const& command, IpcParseResult const& command_list)
{
// Payloads appear in the following format:
// [type:X, xkb_Y, Z]
@ -630,19 +607,13 @@ void IpcCommandExecutor::process_input(IpcCommand const& command, IpcParseResult
// and Z is the value of that variable. Z may not be included at all, in which
// case the variable is set to the default.
if (command.arguments.size() < 2)
{
mir::log_warning("process_input: expects at least 2 arguments");
return;
}
return parse_error("process_input: expects at least 2 arguments");
const char* const TYPE_PREFIX = "type:";
const size_t TYPE_PREFIX_LEN = strlen(TYPE_PREFIX);
std::string_view type_str = command.arguments[0];
if (!type_str.starts_with("type:"))
{
mir::log_warning("process_input: 'type' string is misformatted: %s", command.arguments[0].c_str());
return;
}
return parse_error(std::format("process_input: 'type' string is misformatted: {}", command.arguments[0].c_str()));
std::string_view type = type_str.substr(TYPE_PREFIX_LEN);
assert(type == "keyboard");
@ -651,10 +622,7 @@ void IpcCommandExecutor::process_input(IpcCommand const& command, IpcParseResult
const char* const XKB_PREFIX = "xkb_";
const size_t XKB_PREFIX_LEN = strlen(XKB_PREFIX);
if (!xkb_str.starts_with(XKB_PREFIX))
{
mir::log_warning("process_input: 'xkb' string is misformatted: %s", command.arguments[1].c_str());
return;
}
return parse_error(std::format("process_input: 'xkb' string is misformatted: {}", command.arguments[1].c_str()));
std::string_view xkb_variable_name = xkb_str.substr(XKB_PREFIX_LEN);
assert(xkb_variable_name == "model"
@ -674,18 +642,16 @@ void IpcCommandExecutor::process_input(IpcCommand const& command, IpcParseResult
}
else
{
mir::log_warning("process_input: > 3 arguments were provided but only <= 3 are expected");
return;
return parse_error("process_input: > 3 arguments were provided but only <= 3 are expected");
}
return {};
}
void IpcCommandExecutor::process_workspace(IpcCommand const& command, IpcParseResult const& command_list)
IpcValidationResult IpcCommandExecutor::process_workspace(IpcCommand const& command, IpcParseResult const& command_list)
{
if (command.arguments.empty())
{
mir::log_error("process_workspace: no arguments provided");
return;
}
return parse_error("process_workspace: no arguments provided");
std::string const& arg0 = command.arguments[0];
if (arg0 == "next")
@ -694,14 +660,14 @@ void IpcCommandExecutor::process_workspace(IpcCommand const& command, IpcParseRe
policy.prev_workspace();
else if (arg0 == "next_on_output")
{
if (auto const* output = policy.get_active_output())
if (auto const& output = state.active_output)
policy.next_workspace_on_output(*output);
else
mir::log_error("process_workspace: next_on_output has no output to go next on");
}
else if (arg0 == "prev_on_output")
{
if (auto const* output = policy.get_active_output())
if (auto const& output = state.active_output)
policy.prev_workspace_on_output(*output);
else
mir::log_error("process_workspace: prev_on_output has no output to go prev on");
@ -722,7 +688,7 @@ void IpcCommandExecutor::process_workspace(IpcCommand const& command, IpcParseRe
if (command.arguments.size() < 3)
{
policy.select_workspace(number, back_and_forth);
return;
return {};
}
// We have "workspace number <name>"
@ -735,9 +701,11 @@ void IpcCommandExecutor::process_workspace(IpcCommand const& command, IpcParseRe
policy.select_workspace(*arg1, back_and_forth);
}
}
return {};
}
void IpcCommandExecutor::process_layout(IpcCommand const& command, IpcParseResult const& command_list)
IpcValidationResult IpcCommandExecutor::process_layout(IpcCommand const& command, IpcParseResult const& command_list)
{
// https://i3wm.org/docs/userguide.html#manipulating_layout
std::string const& arg0 = command.arguments[0];
@ -754,10 +722,7 @@ void IpcCommandExecutor::process_layout(IpcCommand const& command, IpcParseResul
else if (arg0 == "toggle")
{
if (command.arguments.size() == 1)
{
mir::log_error("process_layout: expected argument after 'layout toggle ...'");
return;
}
return parse_error("process_layout: expected argument after 'layout toggle ...'");
if (command.arguments.size() == 2)
{
@ -767,18 +732,15 @@ void IpcCommandExecutor::process_layout(IpcCommand const& command, IpcParseResul
else if (arg1 == "all")
policy.try_toggle_layout(true);
else
mir::log_error("process_layout: expected split/all after 'layout toggle X'");
return parse_error("process_layout: expected split/all after 'layout toggle X'");
return;
return {};
}
else
{
auto container = policy.get_state().active();
auto container = state.active();
if (!container)
{
mir::log_error("process_layout: container unavailable");
return;
}
return parse_error("process_layout: container unavailable");
auto current_type = container->get_layout();
size_t index = 0;
@ -844,24 +806,21 @@ void IpcCommandExecutor::process_layout(IpcCommand const& command, IpcParseResul
policy.set_layout(LayoutScheme::horizontal);
}
}
return {};
}
void IpcCommandExecutor::process_scratchpad(IpcCommand const& command, IpcParseResult const& command_list)
IpcValidationResult IpcCommandExecutor::process_scratchpad(IpcCommand const& command, IpcParseResult const& command_list)
{
if (command.arguments.empty())
{
mir::log_error("process_scratchpad: no arguments provided");
return;
}
return parse_error("process_scratchpad: no arguments provided");
std::string const& arg0 = command.arguments[0];
if (arg0 != "show")
{
mir::log_error("process_scratchpad: all scratchpad commands must be 'scratchpad show'");
return;
}
return parse_error("process_scratchpad: all scratchpad commands must be 'scratchpad show'");
policy.show_scratchpad();
return {};
}
namespace
@ -869,6 +828,7 @@ namespace
struct ResizeAdjust
{
bool success = true;
std::string error;
Direction direction = Direction::MAX;
int first = 0;
int second = 0;
@ -877,14 +837,11 @@ struct ResizeAdjust
ResizeAdjust parse_resize(CompositorState const& state, ArgumentsIndexer& indexer, int multiplier)
{
if (!indexer.next())
{
mir::log_error("process_resize: expected argument after 'resize grow'");
return { false };
}
return { .success = false, .error = "process_resize: expected argument after 'resize grow'" };
auto const& container = state.active();
if (!container)
return { .success = false };
return { .success = false, .error = "No container is selcted" };
ResizeAdjust result;
if (indexer.current() == "width" || indexer.current() == "horizontal")
@ -913,9 +870,7 @@ ResizeAdjust parse_resize(CompositorState const& state, ArgumentsIndexer& indexe
}
else
{
mir::log_error("Unknown direction value: %s", indexer.current().c_str());
result.success = false;
return result;
return { .success = false, .error = std::format("Unknown direction value: {}", indexer.current().c_str()) };
}
int available_space = 0;
@ -932,19 +887,12 @@ ResizeAdjust parse_resize(CompositorState const& state, ArgumentsIndexer& indexe
int first = 0;
if (!indexer.parse_move_distance(available_space, first))
{
result.success = false;
return result;
}
return { .success = false, .error = "cannot parse the first value" };
if (indexer.next())
{
if (indexer.current() != "or")
{
mir::log_error("parse_resize: expected 'or'");
result.success = false;
return result;
}
return { .success = false, .error = "expected 'or' after first value" };
}
int second = 0;
@ -957,6 +905,7 @@ ResizeAdjust parse_resize(CompositorState const& state, ArgumentsIndexer& indexe
struct SetResizeResult
{
bool success = true;
std::string error;
std::optional<int> width;
std::optional<int> height;
};
@ -965,21 +914,15 @@ SetResizeResult parse_set_resize(CompositorState const& state, ArgumentsIndexer&
{
auto const& container = state.active();
if (!container)
return { .success = false };
return { .success = false, .error = "Container is not selected" };
SetResizeResult result;
int width = 0, height = 0;
if (!indexer.parse_move_distance(container->get_output()->get_area().size.width.as_value(), width))
{
mir::log_error("parse_set_resize: invalid width");
return { .success = false };
}
return { .success = false, .error = "invalid width" };
if (!indexer.parse_move_distance(container->get_output()->get_area().size.height.as_value(), height))
{
mir::log_error("parse_set_resize: invalid height");
return { .success = false };
}
return { .success = false, .error = "invalid height" };
if (width != 0)
result.width = width;
@ -990,13 +933,10 @@ SetResizeResult parse_set_resize(CompositorState const& state, ArgumentsIndexer&
}
}
void IpcCommandExecutor::process_resize(IpcCommand const& command, IpcParseResult const& command_list)
IpcValidationResult IpcCommandExecutor::process_resize(IpcCommand const& command, IpcParseResult const& command_list)
{
if (command.arguments.empty())
{
mir::log_error("process_resize: no arguments provided");
return;
}
return parse_error("process_resize: no arguments provided");
ArgumentsIndexer indexer(command);
auto const& arg0 = indexer.current();
@ -1004,7 +944,7 @@ void IpcCommandExecutor::process_resize(IpcCommand const& command, IpcParseResul
{
auto adjust = parse_resize(state, indexer, 1);
if (!adjust.success)
return;
return parse_error(adjust.error);
policy.try_resize(adjust.direction, adjust.first);
}
@ -1012,7 +952,7 @@ void IpcCommandExecutor::process_resize(IpcCommand const& command, IpcParseResul
{
auto adjust = parse_resize(state, indexer, -1);
if (!adjust.success)
return;
return parse_error(adjust.error);
policy.try_resize(adjust.direction, adjust.first);
}
@ -1020,13 +960,12 @@ void IpcCommandExecutor::process_resize(IpcCommand const& command, IpcParseResul
{
auto result = parse_set_resize(state, indexer);
if (!result.success)
return;
return parse_error(result.error);
policy.try_set_size(result.width, result.height);
}
else
{
mir::log_error("process_resize: unexpected argument: %s", arg0.c_str());
return;
}
return parse_error(std::format("process_resize: unexpected argument: {}", arg0.c_str()));
return {};
}

View File

@ -25,42 +25,51 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
namespace miracle
{
class Policy;
class CommandController;
class WorkspaceManager;
class AutoRestartingLauncher;
class WindowController;
struct IpcValidationResult
{
bool success = true;
bool parse_error = false;
std::string error;
};
/// Processes all commands coming from i3 IPC. This class is mostly for organizational
/// purposes, as a lot of logic is associated with processing these operations.
class IpcCommandExecutor
{
public:
IpcCommandExecutor(
Policy&,
CommandController&,
WorkspaceManager&,
CompositorState const&,
AutoRestartingLauncher&,
WindowController&);
void process(IpcParseResult const&);
IpcValidationResult process(IpcParseResult const&);
private:
Policy& policy;
CommandController& policy;
WorkspaceManager& workspace_manager;
CompositorState const& state;
AutoRestartingLauncher& launcher;
WindowController& window_controller;
miral::Window get_window_meeting_criteria(IpcParseResult const&);
void process_exec(IpcCommand const&, IpcParseResult const&);
void process_split(IpcCommand const&, IpcParseResult const&);
void process_focus(IpcCommand const&, IpcParseResult const&);
void process_move(IpcCommand const&, IpcParseResult const&);
void process_sticky(IpcCommand const&, IpcParseResult const&);
void process_input(IpcCommand const&, IpcParseResult const&);
void process_workspace(IpcCommand const&, IpcParseResult const&);
void process_layout(IpcCommand const&, IpcParseResult const&);
void process_scratchpad(IpcCommand const&, IpcParseResult const&);
void process_resize(IpcCommand const&, IpcParseResult const&);
IpcValidationResult process_exec(IpcCommand const&, IpcParseResult const&);
IpcValidationResult process_split(IpcCommand const&, IpcParseResult const&);
IpcValidationResult process_focus(IpcCommand const&, IpcParseResult const&);
IpcValidationResult process_move(IpcCommand const&, IpcParseResult const&);
IpcValidationResult process_sticky(IpcCommand const&, IpcParseResult const&);
IpcValidationResult process_input(IpcCommand const&, IpcParseResult const&);
IpcValidationResult process_workspace(IpcCommand const&, IpcParseResult const&);
IpcValidationResult process_layout(IpcCommand const&, IpcParseResult const&);
IpcValidationResult process_scratchpad(IpcCommand const&, IpcParseResult const&);
IpcValidationResult process_resize(IpcCommand const&, IpcParseResult const&);
IpcValidationResult parse_error(std::string error);
};
} // miracle

View File

@ -110,7 +110,7 @@ Policy::Policy(
window_controller(tools, animator, state),
i3_command_executor(*this, workspace_manager, compositor_state, external_client_launcher, window_controller),
surface_tracker { surface_tracker },
ipc { std::make_shared<Ipc>(runner, workspace_manager, *this, server.the_main_loop(), i3_command_executor, config) },
ipc { std::make_shared<Ipc>(runner, workspace_manager, *this, i3_command_executor, config) },
scratchpad_(window_controller, state),
self { std::make_shared<Self>(*this) }
{
@ -257,7 +257,7 @@ bool Policy::handle_pointer_event(MirPointerEvent const* event)
state.cursor_position = { x, y };
// Select the output first
for (auto const& output : output_list)
for (auto const& output : state.output_list)
{
if (output->point_is_in_output(static_cast<int>(x), static_cast<int>(y)))
{
@ -360,10 +360,10 @@ void Policy::advise_new_window(miral::WindowInfo const& window_info)
{
mir::log_warning("create_container: output unavailable");
auto window = window_info.window();
if (!output_list.empty())
if (!state.output_list.empty())
{
// Our output is gone! Let's try to add it to a different output
output_list.front()->add_immediately(window);
state.output_list.front()->add_immediately(window);
}
else
{
@ -505,7 +505,7 @@ void Policy::advise_output_create(miral::Output const& output)
auto output_content = std::make_shared<Output>(
output, workspace_manager, output.extents(), window_manager_tools,
floating_window_manager, state, config, window_controller, animator);
output_list.push_back(output_content);
state.output_list.push_back(output_content);
workspace_manager.request_first_available_workspace(output_content.get());
if (state.active_output == nullptr)
{
@ -527,7 +527,7 @@ void Policy::advise_output_create(miral::Output const& output)
void Policy::advise_output_update(miral::Output const& updated, miral::Output const& original)
{
for (auto& output : output_list)
for (auto& output : state.output_list)
{
if (output->get_output().is_same_output(original))
{
@ -539,7 +539,7 @@ void Policy::advise_output_update(miral::Output const& updated, miral::Output co
void Policy::advise_output_delete(miral::Output const& output)
{
for (auto it = output_list.begin(); it != output_list.end(); it++)
for (auto it = state.output_list.begin(); it != state.output_list.end(); it++)
{
auto other_output = *it;
if (other_output->get_output().is_same_output(output))
@ -556,8 +556,8 @@ void Policy::advise_output_delete(miral::Output const& output)
workspace_manager.delete_workspace(w);
};
output_list.erase(it);
if (output_list.empty())
state.output_list.erase(it);
if (state.output_list.empty())
{
// All nodes should become orphaned
for (auto& window : other_output->collect_all_windows())
@ -573,7 +573,7 @@ void Policy::advise_output_delete(miral::Output const& output)
}
else
{
state.active_output = output_list.front();
state.active_output = state.output_list.front();
state.active_output->set_is_active(true);
for (auto& window : other_output->collect_all_windows())
{
@ -663,7 +663,7 @@ mir::geometry::Rectangle Policy::confirm_inherited_move(
void Policy::advise_application_zone_create(miral::Zone const& application_zone)
{
for (auto const& output : output_list)
for (auto const& output : state.output_list)
{
output->advise_application_zone_create(application_zone);
}
@ -671,7 +671,7 @@ void Policy::advise_application_zone_create(miral::Zone const& application_zone)
void Policy::advise_application_zone_update(miral::Zone const& updated, miral::Zone const& original)
{
for (auto const& output : output_list)
for (auto const& output : state.output_list)
{
output->advise_application_zone_update(updated, original);
}
@ -679,7 +679,7 @@ void Policy::advise_application_zone_update(miral::Zone const& updated, miral::Z
void Policy::advise_application_zone_delete(miral::Zone const& application_zone)
{
for (auto const& output : output_list)
for (auto const& output : state.output_list)
{
output->advise_application_zone_delete(application_zone);
}
@ -1315,15 +1315,15 @@ void Policy::move_cursor_to_output(Output const& output)
bool Policy::try_select_next_output()
{
for (size_t i = 0; i < output_list.size(); i++)
for (size_t i = 0; i < state.output_list.size(); i++)
{
if (output_list[i] == state.active_output)
if (state.output_list[i] == state.active_output)
{
size_t j = i + 1;
if (j == output_list.size())
if (j == state.output_list.size())
j = 0;
move_cursor_to_output(*output_list[j]);
move_cursor_to_output(*state.output_list[j]);
return true;
}
}
@ -1333,15 +1333,15 @@ bool Policy::try_select_next_output()
bool Policy::try_select_prev_output()
{
for (int i = output_list.size() - 1; i >= 0; i++)
for (int i = state.output_list.size() - 1; i >= 0; i++)
{
if (output_list[i] == state.active_output)
if (state.output_list[i] == state.active_output)
{
size_t j = i - 1;
if (j < 0)
j = output_list.size() - 1;
j = state.output_list.size() - 1;
move_cursor_to_output(*output_list[j]);
move_cursor_to_output(*state.output_list[j]);
return true;
}
}
@ -1353,7 +1353,7 @@ std::shared_ptr<Output> const& Policy::_next_output_in_direction(Direction direc
{
auto const& active = state.active_output;
auto const& active_area = active->get_area();
for (auto const& output : output_list)
for (auto const& output : state.output_list)
{
if (output == state.active_output)
continue;
@ -1432,7 +1432,7 @@ std::shared_ptr<Output> const& Policy::_next_output_in_list(std::vector<std::str
if (next == names.size())
next = 0;
for (auto const& output : output_list)
for (auto const& output : state.output_list)
{
if (output->get_output().name() == names[next])
return output;
@ -1499,20 +1499,20 @@ bool Policy::try_move_active_to_current()
bool Policy::try_move_active_to_primary()
{
if (output_list.empty())
if (state.output_list.empty())
return false;
if (!can_move_container())
return false;
if (state.active()->get_output() == output_list[0].get())
if (state.active()->get_output() == state.output_list[0].get())
return false;
auto container = state.active();
container->get_output()->delete_container(container);
state.unfocus(container);
output_list[0]->graft(container);
state.output_list[0]->graft(container);
if (container->window().value())
window_controller.select_active_window(container->window().value());
return true;
@ -1521,20 +1521,20 @@ bool Policy::try_move_active_to_primary()
bool Policy::try_move_active_to_nonprimary()
{
constexpr int MIN_SIZE_TO_HAVE_NONPRIMARY_OUTPUT = 2;
if (output_list.size() < MIN_SIZE_TO_HAVE_NONPRIMARY_OUTPUT)
if (state.output_list.size() < MIN_SIZE_TO_HAVE_NONPRIMARY_OUTPUT)
return false;
if (!can_move_container())
return false;
if (state.active_output != output_list[0])
if (state.active_output != state.output_list[0])
return false;
auto container = state.active();
container->get_output()->delete_container(container);
state.unfocus(container);
output_list[1]->graft(container);
state.output_list[1]->graft(container);
if (container->window().value())
window_controller.select_active_window(container->window().value());
return true;
@ -1545,16 +1545,16 @@ bool Policy::try_move_active_to_next()
if (!can_move_container())
return false;
auto it = std::find(output_list.begin(), output_list.end(), state.active_output);
if (it == output_list.end())
auto it = std::find(state.output_list.begin(), state.output_list.end(), state.active_output);
if (it == state.output_list.end())
{
mir::log_error("Policy::try_move_active_to_next: cannot find active output in list");
return false;
}
it++;
if (it == output_list.end())
it = output_list.begin();
if (it == state.output_list.end())
it = state.output_list.begin();
if (*it == state.active_output)
return false;

View File

@ -20,6 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#include "animator.h"
#include "auto_restarting_launcher.h"
#include "command_controller.h"
#include "compositor_state.h"
#include "config.h"
#include "ipc.h"
@ -49,7 +50,7 @@ class Container;
class ContainerGroupContainer;
class WindowToolsAccessor;
class Policy : public miral::WindowManagementPolicy
class Policy : public miral::WindowManagementPolicy, public CommandController
{
public:
Policy(
@ -101,61 +102,61 @@ public:
// Requests
bool try_request_horizontal();
bool try_request_vertical();
bool try_toggle_layout(bool cycle_through_all);
void try_toggle_resize_mode();
bool try_resize(Direction direction, int pixels);
bool try_set_size(std::optional<int> const& width, std::optional<int> const& height);
bool try_move(Direction direction);
bool try_move_by(Direction direction, int pixels);
bool try_move_to(int x, int y);
bool try_select(Direction direction);
bool try_select_parent();
bool try_select_child();
bool try_select_floating();
bool try_select_tiling();
bool try_select_toggle();
bool try_close_window();
bool quit();
bool try_toggle_fullscreen();
bool select_workspace(int number, bool back_and_forth = true);
bool select_workspace(std::string const& name, bool back_and_forth);
bool next_workspace();
bool prev_workspace();
bool back_and_forth_workspace();
bool next_workspace_on_output(Output const&);
bool prev_workspace_on_output(Output const&);
bool move_active_to_workspace(int number, bool back_and_forth = true);
bool move_active_to_workspace_named(std::string const&, bool back_and_forth);
bool move_active_to_next_workspace();
bool move_active_to_prev_workspace();
bool move_active_to_back_and_forth();
bool move_to_scratchpad();
bool show_scratchpad();
bool toggle_floating();
bool toggle_pinned_to_workspace();
bool set_is_pinned(bool);
bool toggle_tabbing();
bool toggle_stacking();
bool set_layout(LayoutScheme scheme);
bool set_layout_default();
void move_cursor_to_output(Output const&);
bool try_select_next_output();
bool try_select_prev_output();
bool try_select_output(Direction direction);
bool try_select_output(std::vector<std::string> const& names);
bool try_move_active_to_output(Direction direction);
bool try_move_active_to_current();
bool try_move_active_to_primary();
bool try_move_active_to_nonprimary();
bool try_move_active_to_next();
bool try_move_active(std::vector<std::string> const& names);
bool try_request_horizontal() override;
bool try_request_vertical() override;
bool try_toggle_layout(bool cycle_through_all) override;
void try_toggle_resize_mode() override;
bool try_resize(Direction direction, int pixels) override;
bool try_set_size(std::optional<int> const& width, std::optional<int> const& height) override;
bool try_move(Direction direction) override;
bool try_move_by(Direction direction, int pixels) override;
bool try_move_to(int x, int y) override;
bool try_select(Direction direction) override;
bool try_select_parent() override;
bool try_select_child() override;
bool try_select_floating() override;
bool try_select_tiling() override;
bool try_select_toggle() override;
bool try_close_window() override;
bool quit() override;
bool try_toggle_fullscreen() override;
bool select_workspace(int number, bool back_and_forth = true) override;
bool select_workspace(std::string const& name, bool back_and_forth) override;
bool next_workspace() override;
bool prev_workspace() override;
bool back_and_forth_workspace() override;
bool next_workspace_on_output(Output const&) override;
bool prev_workspace_on_output(Output const&) override;
bool move_active_to_workspace(int number, bool back_and_forth = true) override;
bool move_active_to_workspace_named(std::string const&, bool back_and_forth) override;
bool move_active_to_next_workspace() override;
bool move_active_to_prev_workspace() override;
bool move_active_to_back_and_forth() override;
bool move_to_scratchpad() override;
bool show_scratchpad() override;
bool toggle_floating() override;
bool toggle_pinned_to_workspace() override;
bool set_is_pinned(bool) override;
bool toggle_tabbing() override;
bool toggle_stacking() override;
bool set_layout(LayoutScheme scheme) override;
bool set_layout_default() override;
void move_cursor_to_output(Output const&) override;
bool try_select_next_output() override;
bool try_select_prev_output() override;
bool try_select_output(Direction direction) override;
bool try_select_output(std::vector<std::string> const& names) override;
bool try_move_active_to_output(Direction direction) override;
bool try_move_active_to_current() override;
bool try_move_active_to_primary() override;
bool try_move_active_to_nonprimary() override;
bool try_move_active_to_next() override;
bool try_move_active(std::vector<std::string> const& names) override;
// Getters
[[nodiscard]] Output const* get_active_output() const { return state.active_output.get(); }
[[nodiscard]] std::vector<std::shared_ptr<Output>> const& get_output_list() const { return output_list; }
[[nodiscard]] std::vector<std::shared_ptr<Output>> const& get_output_list() const { return state.output_list; }
[[nodiscard]] geom::Point const& get_cursor_position() const { return state.cursor_position; }
[[nodiscard]] CompositorState const& get_state() const { return state; }
@ -173,7 +174,6 @@ private:
bool is_starting_ = true;
CompositorState& state;
std::vector<std::shared_ptr<Output>> output_list;
AllocationHint pending_allocation;
std::vector<miral::Window> orphaned_window_list;
miral::WindowManagerTools window_manager_tools;