feature: supporting I3 IPC enough to make waybar's workspaces function (#35)

* Switching workspaces now emits a signal to i3 IPC clients which makes it so that waybar works
* Refactored a lot of workspace code for readability and usability
This commit is contained in:
Matthew Kosarek 2024-02-21 18:54:59 -05:00 committed by GitHub
parent 2a11993588
commit b6d1ae752e
14 changed files with 812 additions and 156 deletions

View File

@ -24,7 +24,7 @@ jobs:
run: sudo apt-add-repository ppa:mir-team/release
- name: Install dependencies
run: sudo apt-get install libmiral-dev libgtest-dev libyaml-cpp-dev libglib2.0-dev libevdev-dev
run: sudo apt-get install libmiral-dev libgtest-dev libyaml-cpp-dev libglib2.0-dev libevdev-dev nlohmann-json3-dev
- name: Configure CMake
run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}}

View File

@ -19,6 +19,7 @@ pkg_check_modules(MIRAL miral REQUIRED)
pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
pkg_check_modules(YAML REQUIRED IMPORTED_TARGET yaml-cpp)
pkg_check_modules(LIBEVDEV REQUIRED IMPORTED_TARGET libevdev)
find_package(nlohmann_json 3.2.0 REQUIRED)
add_executable(miracle-wm
src/main.cpp
@ -31,11 +32,13 @@ add_executable(miracle-wm
src/miracle_config.h
src/screen.cpp
src/workspace_manager.cpp
src/ipc.cpp
src/auto_restarting_launcher.cpp
src/workspace_observer.cpp
)
target_include_directories(miracle-wm PUBLIC SYSTEM ${MIRAL_INCLUDE_DIRS})
target_link_libraries( miracle-wm ${MIRAL_LDFLAGS} PkgConfig::YAML PkgConfig::GLIB PkgConfig::LIBEVDEV)
target_link_libraries( miracle-wm ${MIRAL_LDFLAGS} PkgConfig::YAML PkgConfig::GLIB PkgConfig::LIBEVDEV nlohmann_json::nlohmann_json)
install(PROGRAMS ${CMAKE_BINARY_DIR}/bin/miracle-wm
DESTINATION ${CMAKE_INSTALL_PREFIX}/bin

View File

@ -60,6 +60,7 @@ parts:
- libglib2.0-0
- libevdev-dev
- libevdev2
- nlohmann-json3-dev
stage-packages:
- libmiral6
- mir-graphics-drivers-desktop

406
src/ipc.cpp Normal file
View File

@ -0,0 +1,406 @@
#define MIR_LOG_COMPONENT "miracle_ipc"
#include "ipc.h"
#include "screen.h"
#include <fcntl.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <sys/un.h>
#include <unistd.h>
#include <mir/log.h>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
using namespace miracle;
static const char ipc_magic[] = {'i', '3', '-', 'i', 'p', 'c'};
#define IPC_HEADER_SIZE (sizeof(ipc_magic) + 8)
#define event_mask(ev) (1 << (ev & 0x7F))
namespace
{
struct sockaddr_un *ipc_user_sockaddr() {
auto ipc_sockaddr = (sockaddr_un*)malloc(sizeof(struct sockaddr_un));
if (ipc_sockaddr == nullptr)
{
mir::log_error("Can't allocate ipc_sockaddr");
exit(1);
}
ipc_sockaddr->sun_family = AF_UNIX;
int path_size = sizeof(ipc_sockaddr->sun_path);
// Env var typically set by logind, e.g. "/run/user/<user-id>"
const char *dir = getenv("XDG_RUNTIME_DIR");
if (!dir)
dir = "/tmp";
if (path_size <= snprintf(ipc_sockaddr->sun_path, path_size,
"%s/miracle-wm-ipc.%u.%i.sock", dir, getuid(), getpid()))
{
mir::log_error("Socket path won't fit into ipc_sockaddr->sun_path");
exit(1);
}
return ipc_sockaddr;
}
json workspace_to_json(std::shared_ptr<Screen> const& screen, int key)
{
bool is_focused = screen->get_active_workspace() == key;
auto area = screen->get_area();
return {
{"num", key},
{"id", key},
{"type", "workspace"},
{"name", std::to_string(key)},
{"visible", screen->is_active() && is_focused},
{"focused", screen->is_active() && is_focused},
{"urgent", false},
{"output", screen->get_output().name()},
{"rect", {
{"x", area.top_left.x.as_int()},
{"y", area.top_left.y.as_int()},
{"width", area.size.width.as_int()},
{"height", area.size.height.as_int()},
}}
};
}
}
Ipc::Ipc(miral::MirRunner& runner, miracle::WorkspaceManager& workspace_manager)
: workspace_manager{workspace_manager}
{
auto ipc_socket_raw = socket(AF_UNIX, SOCK_STREAM, 0);
if (ipc_socket_raw == -1)
{
mir::log_error("Unable to create ipc socket");
exit(1);
}
if (fcntl(ipc_socket_raw, F_SETFD, FD_CLOEXEC) == -1) {
mir::log_error("Unable to set CLOEXEC on IPC socket");
exit(1);
}
if (fcntl(ipc_socket_raw, F_SETFL, O_NONBLOCK) == -1) {
mir::log_error("Unable to set NONBLOCK on IPC socket");
exit(1);
}
ipc_sockaddr = ipc_user_sockaddr();
if (getenv("SWAYSOCK") != nullptr && access(getenv("SWAYSOCK"), F_OK) == -1)
{
strncpy(ipc_sockaddr->sun_path, getenv("SWAYSOCK"), sizeof(ipc_sockaddr->sun_path) - 1);
ipc_sockaddr->sun_path[sizeof(ipc_sockaddr->sun_path) - 1] = 0;
}
unlink(ipc_sockaddr->sun_path);
if (bind(ipc_socket_raw, (struct sockaddr *)ipc_sockaddr, sizeof(*ipc_sockaddr)) == -1)
{
mir::log_error("Unable to bind IPC socket");
exit(1);
}
if (listen(ipc_socket_raw, 3) == -1) {
mir::log_error("Unable to listen on IPC socket");
exit(1);
}
// Set i3 IPC socket path so that i3-msg works out of the box
setenv("I3SOCK", ipc_sockaddr->sun_path, 1);
setenv("SWAYSOCK", ipc_sockaddr->sun_path, 1);
ipc_socket = mir::Fd{ipc_socket_raw};
socket_handle = runner.register_fd_handler(ipc_socket, [&](int fd)
{
int client_fd = accept(ipc_socket, NULL, NULL);
if (client_fd == -1) {
mir::log_error("Unable to accept IPC client connection");
return;
}
int flags;
if ((flags = fcntl(client_fd, F_GETFD)) == -1
|| fcntl(client_fd, F_SETFD, flags|FD_CLOEXEC) == -1) {
mir::log_error("Unable to set CLOEXEC on IPC client socket");
close(client_fd);
return;
}
if ((flags = fcntl(client_fd, F_GETFL)) == -1
|| fcntl(client_fd, F_SETFL, flags|O_NONBLOCK) == -1) {
mir::log_error("Unable to set NONBLOCK on IPC client socket");
close(client_fd);
return;
}
auto mir_fd = mir::Fd{client_fd};
clients.push_back({
mir_fd,
runner.register_fd_handler(mir_fd, [this](int fd)
{
auto& client = get_client(fd);
int read_available;
if (ioctl(client.client_fd, FIONREAD, &read_available) == -1) {
mir::log_error("Unable to read IPC socket buffer size");
disconnect(client);
return;
}
if (client.pending_read_length > 0) {
if ((uint32_t)read_available >= client.pending_read_length) {
// Reset pending values.
uint32_t pending_length = client.pending_read_length;
IpcCommandType pending_type = client.pending_type;
client.pending_read_length = 0;
handle_command(client, pending_length, pending_type);
}
return;
}
if (read_available < (int) IPC_HEADER_SIZE) {
return;
}
uint8_t buf[IPC_HEADER_SIZE];
// Should be fully available, because read_available >= IPC_HEADER_SIZE
ssize_t received = recv(client.client_fd, buf, IPC_HEADER_SIZE, 0);
if (received == -1)
{
mir::log_error("Unable to receive header from IPC client");
disconnect(client);
return;
}
if (memcmp(buf, ipc_magic, sizeof(ipc_magic)) != 0) {
mir::log_error("IPC header check failed");
disconnect(client);
return;
}
memcpy(&client.pending_read_length, buf + sizeof(ipc_magic), sizeof(uint32_t));
memcpy(&client.pending_type, buf + sizeof(ipc_magic) + sizeof(uint32_t), sizeof(uint32_t));
if (read_available - received >= (long)client.pending_read_length)
{
// Reset pending values.
uint32_t pending_length = client.pending_read_length;
IpcCommandType pending_type = client.pending_type;
client.pending_read_length = 0;
handle_command(client, pending_length, pending_type);
}
})
});
});
}
void Ipc::on_created(std::shared_ptr<Screen> const& info, int key)
{
json j = {
{"change", "init"},
{"old", nullptr},
{"current", workspace_to_json(info, key)}
};
auto serialized_value = to_string(j);
for (auto& client : clients)
{
if ((client.subscribed_events & event_mask(IPC_EVENT_WORKSPACE)) == 0) {
continue;
}
send_reply(client, IPC_EVENT_WORKSPACE, serialized_value);
}
}
void Ipc::on_removed(std::shared_ptr<Screen> const& screen, int key)
{
json j = {
{"change", "empty"},
{"current", workspace_to_json(screen, key)}
};
auto serialized_value = to_string(j);
for (auto& client : clients)
{
if ((client.subscribed_events & event_mask(IPC_EVENT_WORKSPACE)) == 0) {
continue;
}
send_reply(client, IPC_EVENT_WORKSPACE, serialized_value);
}
}
void Ipc::on_focused(
std::shared_ptr<Screen> const& previous,
int previous_key,
std::shared_ptr<Screen> const& current,
int current_key)
{
json j = {
{"change", "focus"},
{"current", workspace_to_json(current, current_key)}
};
if (previous)
j["old"] = workspace_to_json(previous, previous_key);
else
j["old"] = nullptr;
auto serialized_value = to_string(j);
for (auto& client : clients)
{
if ((client.subscribed_events & event_mask(IPC_EVENT_WORKSPACE)) == 0) {
continue;
}
send_reply(client, IPC_EVENT_WORKSPACE, serialized_value);
}
}
Ipc::IpcClient &Ipc::get_client(int fd)
{
for (auto& client : clients)
{
if (client.client_fd == fd)
return client;
}
throw std::runtime_error("Could not find IPC client");
}
void Ipc::disconnect(Ipc::IpcClient& client)
{
auto it = std::find_if(clients.begin(), clients.end(), [&](IpcClient const& other)
{
return other.client_fd.operator int() == client.client_fd.operator int();
});
if (it != clients.end())
{
shutdown(client.client_fd, SHUT_RDWR);
mir::log_info("Disconnected client: %d", (int)client.client_fd);
clients.erase(it);
}
else
{
mir::log_error("Unable to disconnect client");
}
}
void Ipc::handle_command(miracle::Ipc::IpcClient &client, uint32_t payload_length, miracle::IpcCommandType payload_type)
{
char *buf = (char*)malloc(payload_length + 1);
if (!buf)
{
mir::log_error("Unable to allocate IPC payload");
disconnect(client);
return;
}
if (payload_length > 0) {
// Payload should be fully available
ssize_t received = recv(client.client_fd, buf, payload_length, 0);
if (received == -1)
{
mir::log_error("Unable to receive payload from IPC client");
disconnect(client);
free(buf);
return;
}
}
buf[payload_length] = '\0';
switch (payload_type)
{
case IPC_GET_WORKSPACES:
{
json j = json::array();
for (int i = 0; i < WorkspaceManager::NUM_WORKSPACES; i++)
{
auto workspace = workspace_manager.get_workspaces()[i];
if (workspace)
j.push_back(workspace_to_json(workspace, i));
}
auto json_string = to_string(j);
send_reply(client, payload_type, json_string);
break;
}
case IPC_SUBSCRIBE:
{
json j = json::parse(buf);
for (auto const& i : j)
{
std::string event_type = i.template get<std::string>();
if (event_type == "workspace")
{
client.subscribed_events |= event_mask(IPC_EVENT_WORKSPACE);
const std::string msg = "{\"success\": true}";
send_reply(client, payload_type, msg);
}
}
break;
}
default:
mir::log_warning("Unknown payload type: %d", payload_type);
disconnect(client);
return;
}
}
void Ipc::send_reply(miracle::Ipc::IpcClient &client, miracle::IpcCommandType command_type, const std::string &payload)
{
const uint32_t payload_length = payload.size();
char data[IPC_HEADER_SIZE];
memcpy(data, ipc_magic, sizeof(ipc_magic));
memcpy(data + sizeof(ipc_magic), &payload_length, sizeof(payload_length));
memcpy(data + sizeof(ipc_magic) + sizeof(payload_length), &command_type, sizeof(command_type));
auto new_buffer_size = client.buffer.size();
while (client.write_buffer_len + IPC_HEADER_SIZE + payload_length >= new_buffer_size) {
if (new_buffer_size == 0) new_buffer_size = 1;
new_buffer_size *= 2;
}
if (new_buffer_size > 4e6) { // 4 MB
mir::log_error("Client write buffer too big (%zu), disconnecting client", client.buffer.size());
disconnect(client);
return;
}
client.buffer.resize(new_buffer_size);
memcpy(client.buffer.data() + client.write_buffer_len, data, IPC_HEADER_SIZE);
client.write_buffer_len += IPC_HEADER_SIZE;
memcpy(client.buffer.data() + client.write_buffer_len, payload.c_str(), payload_length);
client.write_buffer_len += payload_length;
handle_writeable(client);
}
void Ipc::handle_writeable(miracle::Ipc::IpcClient &client)
{
while (client.write_buffer_len > 0)
{
ssize_t written = write(client.client_fd, client.buffer.data(), client.write_buffer_len);
if (written == -1 && errno == EAGAIN) {
return;
} else if (written == -1) {
mir::log_error("Unable to send data from queue to IPC client");
disconnect(client);
return;
}
memmove(client.buffer.data(), client.buffer.data() + written, client.write_buffer_len - written);
client.write_buffer_len -= written;
}
client.write_buffer_len = 0;
}

90
src/ipc.h Normal file
View File

@ -0,0 +1,90 @@
#ifndef MIRACLEWM_IPC_H
#define MIRACLEWM_IPC_H
#include "workspace_manager.h"
#include "workspace_observer.h"
#include <miral/runner.h>
#include <mir/fd.h>
#include <vector>
struct sockaddr_un;
namespace miracle
{
/// This it taken directly from SWAY
enum IpcCommandType {
// i3 command types - see i3's I3_REPLY_TYPE constants
IPC_COMMAND = 0,
IPC_GET_WORKSPACES = 1,
IPC_SUBSCRIBE = 2,
IPC_GET_OUTPUTS = 3,
IPC_GET_TREE = 4,
IPC_GET_MARKS = 5,
IPC_GET_BAR_CONFIG = 6,
IPC_GET_VERSION = 7,
IPC_GET_BINDING_MODES = 8,
IPC_GET_CONFIG = 9,
IPC_SEND_TICK = 10,
IPC_SYNC = 11,
IPC_GET_BINDING_STATE = 12,
// sway-specific command types
IPC_GET_INPUTS = 100,
IPC_GET_SEATS = 101,
// Events sent from sway to clients. Events have the highest bits set.
IPC_EVENT_WORKSPACE = ((1<<31) | 0),
IPC_EVENT_OUTPUT = ((1<<31) | 1),
IPC_EVENT_MODE = ((1<<31) | 2),
IPC_EVENT_WINDOW = ((1<<31) | 3),
IPC_EVENT_BARCONFIG_UPDATE = ((1<<31) | 4),
IPC_EVENT_BINDING = ((1<<31) | 5),
IPC_EVENT_SHUTDOWN = ((1<<31) | 6),
IPC_EVENT_TICK = ((1<<31) | 7),
// sway-specific event types
IPC_EVENT_BAR_STATE_UPDATE = ((1<<31) | 20),
IPC_EVENT_INPUT = ((1<<31) | 21),
};
/// Inter process communication for compositor clients (e.g. waybar).
/// This class will implement I3's interface: https://i3wm.org/docs/ipc.html
/// plus some of the sway-specific items.
/// It may be extended in the future.
class Ipc : public WorkspaceObserver
{
public:
Ipc(miral::MirRunner& runner, WorkspaceManager&);
void on_created(std::shared_ptr<Screen> const& info, int key) override;
void on_removed(std::shared_ptr<Screen> const& info, int key) override;
void on_focused(std::shared_ptr<Screen> const& previous, int, std::shared_ptr<Screen> const& current, int) override;
private:
struct IpcClient
{
mir::Fd client_fd;
std::unique_ptr<miral::FdHandle> handle;
uint32_t pending_read_length = 0;
IpcCommandType pending_type;
std::vector<char> buffer;
int write_buffer_len = 0;
int subscribed_events = 0;
};
WorkspaceManager& workspace_manager;
mir::Fd ipc_socket;
std::unique_ptr<miral::FdHandle> socket_handle;
sockaddr_un* ipc_sockaddr = nullptr;
std::vector<IpcClient> clients;
void disconnect(IpcClient& client);
IpcClient& get_client(int fd);
void handle_command(IpcClient& client, uint32_t payload_length, IpcCommandType payload_type);
void send_reply(IpcClient& client, IpcCommandType command_type, std::string const& payload);
void handle_writeable(IpcClient& client);
};
}
#endif //MIRACLEWM_IPC_H

View File

@ -4,7 +4,6 @@
#include "miracle_config.h"
#include "auto_restarting_launcher.h"
#include <miral/set_window_management_policy.h>
#include <miral/external_client.h>
#include <miral/runner.h>
#include <miral/window_management_options.h>
@ -14,7 +13,6 @@
#include <miral/wayland_extensions.h>
#include <miral/display_configuration_option.h>
#include <miral/add_init_callback.h>
#include <mir/log.h>
using namespace miral;

View File

@ -71,8 +71,18 @@ MiracleWindowManagementPolicy::MiracleWindowManagementPolicy(
internal_client_launcher{internal_client_launcher},
runner{runner},
config{config},
workspace_manager{WorkspaceManager(tools)}
workspace_manager{WorkspaceManager(
tools,
workspace_observer_registrar,
[&]() { return get_active_output(); })},
ipc{std::make_shared<Ipc>(runner, workspace_manager)}
{
workspace_observer_registrar.register_interest(ipc);
}
MiracleWindowManagementPolicy::~MiracleWindowManagementPolicy()
{
workspace_observer_registrar.unregister_interest(*ipc);
}
bool MiracleWindowManagementPolicy::handle_keyboard_event(MirKeyboardEvent const* event)
@ -102,118 +112,118 @@ bool MiracleWindowManagementPolicy::handle_keyboard_event(MirKeyboardEvent const
}
return true;
case RequestVertical:
if(active_output) active_output->screen->get_active_tree().request_vertical();
if(active_output) active_output->get_active_tree().request_vertical();
return true;
case RequestHorizontal:
if(active_output) active_output->screen->get_active_tree().request_horizontal();
if(active_output) active_output->get_active_tree().request_horizontal();
return true;
case ToggleResize:
if(active_output) active_output->screen->get_active_tree().toggle_resize_mode();
if(active_output) active_output->get_active_tree().toggle_resize_mode();
return true;
case MoveUp:
if (active_output && active_output->screen->get_active_tree().try_move_active_window(Direction::up))
if (active_output && active_output->get_active_tree().try_move_active_window(Direction::up))
return true;
return false;
case MoveDown:
if (active_output && active_output->screen->get_active_tree().try_move_active_window(Direction::down))
if (active_output && active_output->get_active_tree().try_move_active_window(Direction::down))
return true;
return false;
case MoveLeft:
if (active_output && active_output->screen->get_active_tree().try_move_active_window(Direction::left))
if (active_output && active_output->get_active_tree().try_move_active_window(Direction::left))
return true;
return false;
case MoveRight:
if (active_output && active_output->screen->get_active_tree().try_move_active_window(Direction::right))
if (active_output && active_output->get_active_tree().try_move_active_window(Direction::right))
return true;
return false;
case SelectUp:
if (active_output && (active_output->screen->get_active_tree().try_resize_active_window(Direction::up)
|| active_output->screen->get_active_tree().try_select_next(Direction::up)))
if (active_output && (active_output->get_active_tree().try_resize_active_window(Direction::up)
|| active_output->get_active_tree().try_select_next(Direction::up)))
return true;
return false;
case SelectDown:
if (active_output && (active_output->screen->get_active_tree().try_resize_active_window(Direction::down)
|| active_output->screen->get_active_tree().try_select_next(Direction::down)))
if (active_output && (active_output->get_active_tree().try_resize_active_window(Direction::down)
|| active_output->get_active_tree().try_select_next(Direction::down)))
return true;
return false;
case SelectLeft:
if (active_output && (active_output->screen->get_active_tree().try_resize_active_window(Direction::left)
|| active_output->screen->get_active_tree().try_select_next(Direction::left)))
if (active_output && (active_output->get_active_tree().try_resize_active_window(Direction::left)
|| active_output->get_active_tree().try_select_next(Direction::left)))
return true;
return false;
case SelectRight:
if (active_output && (active_output->screen->get_active_tree().try_resize_active_window(Direction::right)
|| active_output->screen->get_active_tree().try_select_next(Direction::right)))
if (active_output && (active_output->get_active_tree().try_resize_active_window(Direction::right)
|| active_output->get_active_tree().try_select_next(Direction::right)))
return true;
return false;
case QuitActiveWindow:
if (active_output) active_output->screen->get_active_tree().close_active_window();
if (active_output) active_output->get_active_tree().close_active_window();
return true;
case QuitCompositor:
runner.stop();
return true;
case Fullscreen:
if (active_output) active_output->screen->get_active_tree().try_toggle_active_fullscreen();
if (active_output) active_output->get_active_tree().try_toggle_active_fullscreen();
return true;
case SelectWorkspace1:
if (active_output) workspace_manager.request_workspace(active_output->screen, '1');
if (active_output) workspace_manager.request_workspace(active_output, 1);
break;
case SelectWorkspace2:
if (active_output) workspace_manager.request_workspace(active_output->screen, '2');
if (active_output) workspace_manager.request_workspace(active_output, 2);
break;
case SelectWorkspace3:
if (active_output) workspace_manager.request_workspace(active_output->screen, '3');
if (active_output) workspace_manager.request_workspace(active_output, 3);
break;
case SelectWorkspace4:
if (active_output) workspace_manager.request_workspace(active_output->screen, '4');
if (active_output) workspace_manager.request_workspace(active_output, 4);
break;
case SelectWorkspace5:
if (active_output) workspace_manager.request_workspace(active_output->screen, '5');
if (active_output) workspace_manager.request_workspace(active_output, 5);
break;
case SelectWorkspace6:
if (active_output) workspace_manager.request_workspace(active_output->screen, '6');
if (active_output) workspace_manager.request_workspace(active_output, 6);
break;
case SelectWorkspace7:
if (active_output) workspace_manager.request_workspace(active_output->screen, '7');
if (active_output) workspace_manager.request_workspace(active_output, 7);
break;
case SelectWorkspace8:
if (active_output) workspace_manager.request_workspace(active_output->screen, '8');
if (active_output) workspace_manager.request_workspace(active_output, 8);
break;
case SelectWorkspace9:
if (active_output) workspace_manager.request_workspace(active_output->screen, '9');
if (active_output) workspace_manager.request_workspace(active_output, 9);
break;
case SelectWorkspace0:
if (active_output) workspace_manager.request_workspace(active_output->screen, '0');
if (active_output) workspace_manager.request_workspace(active_output, 0);
break;
case MoveToWorkspace1:
if (active_output) workspace_manager.move_active_to_workspace(active_output->screen, '1');
if (active_output) workspace_manager.move_active_to_workspace(active_output, 1);
break;
case MoveToWorkspace2:
if (active_output) workspace_manager.move_active_to_workspace(active_output->screen, '2');
if (active_output) workspace_manager.move_active_to_workspace(active_output, 2);
break;
case MoveToWorkspace3:
if (active_output) workspace_manager.move_active_to_workspace(active_output->screen, '3');
if (active_output) workspace_manager.move_active_to_workspace(active_output, 3);
break;
case MoveToWorkspace4:
if (active_output) workspace_manager.move_active_to_workspace(active_output->screen, '4');
if (active_output) workspace_manager.move_active_to_workspace(active_output, 4);
break;
case MoveToWorkspace5:
if (active_output) workspace_manager.move_active_to_workspace(active_output->screen, '5');
if (active_output) workspace_manager.move_active_to_workspace(active_output, 5);
break;
case MoveToWorkspace6:
if (active_output) workspace_manager.move_active_to_workspace(active_output->screen, '6');
if (active_output) workspace_manager.move_active_to_workspace(active_output, 6);
break;
case MoveToWorkspace7:
if (active_output) workspace_manager.move_active_to_workspace(active_output->screen, '7');
if (active_output) workspace_manager.move_active_to_workspace(active_output, 7);
break;
case MoveToWorkspace8:
if (active_output) workspace_manager.move_active_to_workspace(active_output->screen, '8');
if (active_output) workspace_manager.move_active_to_workspace(active_output, 8);
break;
case MoveToWorkspace9:
if (active_output) workspace_manager.move_active_to_workspace(active_output->screen, '9');
if (active_output) workspace_manager.move_active_to_workspace(active_output, 9);
break;
case MoveToWorkspace0:
if (active_output) workspace_manager.move_active_to_workspace(active_output->screen, '0');
if (active_output) workspace_manager.move_active_to_workspace(active_output, 0);
break;
default:
std::cerr << "Unknown key_command: " << key_command << std::endl;
@ -227,15 +237,22 @@ bool MiracleWindowManagementPolicy::handle_pointer_event(MirPointerEvent const*
auto x = miral::toolkit::mir_pointer_event_axis_value(event, MirPointerAxis::mir_pointer_axis_x);
auto y = miral::toolkit::mir_pointer_event_axis_value(event, MirPointerAxis::mir_pointer_axis_y);
for (auto const& pair : output_list)
for (auto const& output : output_list)
{
if (pair->screen->point_is_in_output(static_cast<int>(x), static_cast<int>(y)))
if (output->point_is_in_output(static_cast<int>(x), static_cast<int>(y)))
{
if (active_output != pair)
if (active_output != output)
{
active_output = pair;
if (active_output) active_output->set_is_active(false);
active_output = output;
active_output->set_is_active(true);
workspace_manager.request_focus(output->get_active_workspace());
}
if (output->get_active_workspace() >= 0)
{
active_output->get_active_tree().select_window_from_point(static_cast<int>(x), static_cast<int>(y));
}
active_output->screen->get_active_tree().select_window_from_point(static_cast<int>(x), static_cast<int>(y));
break;
}
}
@ -254,15 +271,15 @@ auto MiracleWindowManagementPolicy::place_new_window(
}
pending_output = active_output;
return active_output->screen->get_active_tree().allocate_position(requested_specification);
return active_output->get_active_tree().allocate_position(requested_specification);
}
void MiracleWindowManagementPolicy::_add_to_output_immediately(miral::Window& window, std::shared_ptr<OutputInfo>& output)
void MiracleWindowManagementPolicy::_add_to_output_immediately(miral::Window& window, std::shared_ptr<Screen>& output)
{
miral::WindowSpecification spec;
spec = output->screen->get_active_tree().allocate_position(spec);
spec = output->get_active_tree().allocate_position(spec);
window_manager_tools.modify_window(window, spec);
output->screen->get_active_tree().advise_new_window(window_manager_tools.info_for(window));
output->get_active_tree().advise_new_window(window_manager_tools.info_for(window));
}
void MiracleWindowManagementPolicy::advise_new_window(miral::WindowInfo const& window_info)
@ -287,7 +304,7 @@ void MiracleWindowManagementPolicy::advise_new_window(miral::WindowInfo const& w
return;
}
shared_output->screen->get_active_tree().advise_new_window(window_info);
shared_output->get_active_tree().advise_new_window(window_info);
pending_output.reset();
}
@ -295,7 +312,7 @@ void MiracleWindowManagementPolicy::handle_window_ready(miral::WindowInfo &windo
{
for (auto const& output : output_list)
{
if (output->screen->get_active_tree().handle_window_ready(window_info))
if (output->get_active_tree().handle_window_ready(window_info))
break;
}
}
@ -303,14 +320,14 @@ void MiracleWindowManagementPolicy::handle_window_ready(miral::WindowInfo &windo
void MiracleWindowManagementPolicy::advise_focus_gained(const miral::WindowInfo &window_info)
{
for (auto const& output : output_list)
output->screen->get_active_tree().advise_focus_gained(window_info.window());
output->get_active_tree().advise_focus_gained(window_info.window());
window_manager_tools.raise_tree(window_info.window());
}
void MiracleWindowManagementPolicy::advise_focus_lost(const miral::WindowInfo &window_info)
{
for (auto const& output : output_list)
output->screen->get_active_tree().advise_focus_lost(window_info.window());
output->get_active_tree().advise_focus_lost(window_info.window());
}
void MiracleWindowManagementPolicy::advise_delete_window(const miral::WindowInfo &window_info)
@ -324,7 +341,7 @@ void MiracleWindowManagementPolicy::advise_delete_window(const miral::WindowInfo
}
for (auto const& output : output_list)
output->screen->get_active_tree().advise_delete_window(window_info.window());
output->get_active_tree().advise_delete_window(window_info.window());
}
void MiracleWindowManagementPolicy::advise_move_to(miral::WindowInfo const& window_info, geom::Point top_left)
@ -334,10 +351,9 @@ void MiracleWindowManagementPolicy::advise_move_to(miral::WindowInfo const& wind
void MiracleWindowManagementPolicy::advise_output_create(miral::Output const& output)
{
WindowTreeOptions options = { config.get_gap_size_x(), config.get_gap_size_y() };
auto new_tree = std::make_shared<OutputInfo>(
output,
std::make_shared<Screen>(workspace_manager, output.extents(), window_manager_tools, options));
workspace_manager.request_first_available_workspace(new_tree->screen);
auto new_tree = std::make_shared<Screen>(
output, workspace_manager, output.extents(), window_manager_tools, options);
workspace_manager.request_first_available_workspace(new_tree);
output_list.push_back(new_tree);
if (active_output == nullptr)
active_output = new_tree;
@ -357,9 +373,9 @@ void MiracleWindowManagementPolicy::advise_output_update(miral::Output const& up
{
for (auto& output : output_list)
{
if (output->output.is_same_output(original))
if (output->get_output().is_same_output(original))
{
for (auto& workspace : output->screen->get_workspaces())
for (auto& workspace : output->get_workspaces())
{
workspace.tree.set_output_area(updated.extents());
}
@ -373,7 +389,7 @@ void MiracleWindowManagementPolicy::advise_output_delete(miral::Output const& ou
for (auto it = output_list.begin(); it != output_list.end();)
{
auto other_output = *it;
if (other_output->output.is_same_output(output))
if (other_output->get_output().is_same_output(output))
{
it = output_list.erase(it);
if (other_output == active_output)
@ -381,7 +397,7 @@ void MiracleWindowManagementPolicy::advise_output_delete(miral::Output const& ou
if (output_list.empty())
{
// All nodes should become orphaned
for (auto& workspace : other_output->screen->get_workspaces())
for (auto& workspace : other_output->get_workspaces())
{
workspace.tree.foreach_node([&](auto node)
{
@ -397,9 +413,9 @@ void MiracleWindowManagementPolicy::advise_output_delete(miral::Output const& ou
else
{
active_output = output_list[0];
for (auto& workspace : other_output->screen->get_workspaces())
for (auto& workspace : other_output->get_workspaces())
{
active_output->screen->get_active_tree().add_tree(workspace.tree);
active_output->get_active_tree().add_tree(workspace.tree);
}
}
}
@ -412,7 +428,7 @@ void MiracleWindowManagementPolicy::advise_state_change(miral::WindowInfo const&
{
for (auto const& output : output_list)
{
if (output->screen->get_active_tree().advise_state_change(window_info, state))
if (output->get_active_tree().advise_state_change(window_info, state))
{
break;
}
@ -430,7 +446,7 @@ void MiracleWindowManagementPolicy::handle_modify_window(
for (auto const& output : output_list)
{
bool found = false;
for (auto& workspace : output->screen->get_workspaces())
for (auto& workspace : output->get_workspaces())
{
if (workspace.tree.advise_fullscreen_window(window_info))
{
@ -447,7 +463,7 @@ void MiracleWindowManagementPolicy::handle_modify_window(
for (auto const& output : output_list)
{
bool found = false;
for (auto& workspace : output->screen->get_workspaces())
for (auto& workspace : output->get_workspaces())
{
if (workspace.tree.advise_restored_window(window_info))
{
@ -464,7 +480,7 @@ void MiracleWindowManagementPolicy::handle_modify_window(
for (auto const& output :output_list)
{
bool found = false;
for (auto& workspace : output->screen->get_workspaces())
for (auto& workspace : output->get_workspaces())
{
if (workspace.tree.constrain(window_info))
{
@ -493,7 +509,7 @@ MiracleWindowManagementPolicy::confirm_placement_on_display(
for (auto const& output : output_list)\
{
bool found = false;
for (auto& workspace : output->screen->get_workspaces())
for (auto& workspace : output->get_workspaces())
{
if (workspace.tree.confirm_placement_on_display(window_info, new_state, modified_placement))
{
@ -536,7 +552,7 @@ void MiracleWindowManagementPolicy::advise_application_zone_create(miral::Zone c
{
for (auto const& output : output_list)
{
output->screen->advise_application_zone_create(application_zone);
output->advise_application_zone_create(application_zone);
}
}
@ -544,7 +560,7 @@ void MiracleWindowManagementPolicy::advise_application_zone_update(miral::Zone c
{
for (auto const& output : output_list)
{
output->screen->advise_application_zone_update(updated, original);
output->advise_application_zone_update(updated, original);
}
}
@ -552,6 +568,6 @@ void MiracleWindowManagementPolicy::advise_application_zone_delete(miral::Zone c
{
for (auto const& output : output_list)
{
output->screen->advise_application_zone_delete(application_zone);
output->advise_application_zone_delete(application_zone);
}
}

View File

@ -4,6 +4,7 @@
#include "screen.h"
#include "miracle_config.h"
#include "workspace_manager.h"
#include "ipc.h"
#include <miral/window_manager_tools.h>
#include <miral/window_management_policy.h>
@ -21,12 +22,6 @@ class MirRunner;
namespace miracle
{
struct OutputInfo
{
miral::Output output;
std::shared_ptr<Screen> screen;
};
class MiracleWindowManagementPolicy : public miral::WindowManagementPolicy
{
public:
@ -36,7 +31,7 @@ public:
miral::InternalClientLauncher const&,
miral::MirRunner&,
MiracleConfig const&);
~MiracleWindowManagementPolicy() override = default;
~MiracleWindowManagementPolicy() override;
bool handle_keyboard_event(MirKeyboardEvent const* event) override;
bool handle_pointer_event(MirPointerEvent const* event) override;
@ -80,19 +75,23 @@ public:
void advise_application_zone_update(miral::Zone const& updated, miral::Zone const& original) override;
void advise_application_zone_delete(miral::Zone const& application_zone) override;
std::shared_ptr<Screen> const& get_active_output() { return active_output; }
private:
std::shared_ptr<OutputInfo> active_output;
std::vector<std::shared_ptr<OutputInfo>> output_list;
std::weak_ptr<OutputInfo> pending_output;
std::shared_ptr<Screen> active_output;
std::vector<std::shared_ptr<Screen>> output_list;
std::weak_ptr<Screen> pending_output;
std::vector<Window> orphaned_window_list;
miral::WindowManagerTools window_manager_tools;
miral::ExternalClientLauncher const external_client_launcher;
miral::InternalClientLauncher const internal_client_launcher;
miral::MirRunner& runner;
MiracleConfig const& config;
WorkspaceObserverRegistrar workspace_observer_registrar;
WorkspaceManager workspace_manager;
std::shared_ptr<Ipc> ipc;
void _add_to_output_immediately(Window&, std::shared_ptr<OutputInfo>&);
void _add_to_output_immediately(Window&, std::shared_ptr<Screen>&);
};
}

View File

@ -5,11 +5,13 @@
using namespace miracle;
Screen::Screen(
miral::Output const& output,
WorkspaceManager& workspace_manager,
geom::Rectangle const& area,
miral::WindowManagerTools const& tools,
WindowTreeOptions const& options)
: workspace_manager{workspace_manager},
: output{output},
workspace_manager{workspace_manager},
area{area},
tools{tools},
options{options}
@ -27,16 +29,15 @@ WindowTree &Screen::get_active_tree()
throw std::runtime_error("Unable to find the active tree. We shouldn't be here");
}
void Screen::advise_new_workspace(char workspace)
void Screen::advise_new_workspace(int workspace)
{
workspaces.push_back({
workspace,
WindowTree(this, tools, options)
});
make_workspace_active(workspace);
}
void Screen::advise_workspace_deleted(char workspace)
void Screen::advise_workspace_deleted(int workspace)
{
for (auto it = workspaces.begin(); it != workspaces.end(); it++)
{
@ -48,7 +49,7 @@ void Screen::advise_workspace_deleted(char workspace)
}
}
bool Screen::make_workspace_active(char key)
bool Screen::advise_workspace_active(int key)
{
for (auto& workspace : workspaces)
{
@ -93,6 +94,17 @@ void Screen::show(ScreenWorkspaceInfo& info)
info.tree.show();
}
const ScreenWorkspaceInfo &Screen::get_workspace(int key)
{
for (auto const& workspace : workspaces)
{
if (workspace.workspace == key)
return workspace;
}
mir::fatal_error("Cannot find workspace with key: %c", key);
}
void Screen::advise_application_zone_create(miral::Zone const& application_zone)
{
if (application_zone.extents().contains(area))

View File

@ -3,6 +3,7 @@
#include "window_tree.h"
#include <memory>
#include <miral/output.h>
namespace miracle
{
@ -17,7 +18,7 @@ struct NodeResurrection
struct ScreenWorkspaceInfo
{
char workspace;
int workspace;
WindowTree tree;
std::vector<NodeResurrection> nodes_to_resurrect;
};
@ -29,6 +30,7 @@ class Screen
{
public:
Screen(
miral::Output const& output,
WorkspaceManager& workspace_manager,
geom::Rectangle const& area,
miral::WindowManagerTools const& tools,
@ -36,10 +38,12 @@ public:
~Screen() = default;
WindowTree& get_active_tree();
void advise_new_workspace(char workspace);
void advise_workspace_deleted(char workspace);
bool make_workspace_active(char workspace);
int get_active_workspace() const { return active_workspace; }
void advise_new_workspace(int workspace);
void advise_workspace_deleted(int workspace);
bool advise_workspace_active(int workspace);
std::vector<ScreenWorkspaceInfo>& get_workspaces() { return workspaces; }
ScreenWorkspaceInfo const& get_workspace(int key);
void advise_application_zone_create(miral::Zone const& application_zone);
void advise_application_zone_update(miral::Zone const& updated, miral::Zone const& original);
void advise_application_zone_delete(miral::Zone const& application_zone);
@ -47,15 +51,20 @@ public:
geom::Rectangle const& get_area() { return area; }
std::vector<miral::Zone> const& get_app_zones() { return application_zone_list; }
miral::Output const& get_output() { return output; }
bool is_active() const { return is_active_; }
void set_is_active(bool new_is_active) { is_active_ = new_is_active; }
private:
miral::Output output;
WorkspaceManager& workspace_manager;
miral::WindowManagerTools tools;
geom::Rectangle area;
WindowTreeOptions options;
char active_workspace = '\0';
int active_workspace = -1;
std::vector<ScreenWorkspaceInfo> workspaces;
std::vector<miral::Zone> application_zone_list;
bool is_active_ = false;
void hide(ScreenWorkspaceInfo&);
void show(ScreenWorkspaceInfo&);

View File

@ -1,63 +1,67 @@
#define MIR_LOG_COMPONENT "workspace_manager"
#include "workspace_manager.h"
#include "screen.h"
#include <mir/log.h>
using namespace mir::geometry;
using namespace miral;
using namespace miracle;
namespace
{
char DEFAULT_WORKSPACES[] = {'1','2','3','4','5','6','7','8','9', '0'};
}
WorkspaceManager::WorkspaceManager(WindowManagerTools const& tools) :
tools_{tools}
WorkspaceManager::WorkspaceManager(
WindowManagerTools const& tools,
WorkspaceObserverRegistrar& registry,
std::function<std::shared_ptr<Screen> const()> const& get_active_screen) :
tools_{tools},
registry{registry},
get_active_screen{get_active_screen}
{
}
std::shared_ptr<Screen> WorkspaceManager::request_workspace(std::shared_ptr<Screen> screen, char key)
std::shared_ptr<Screen> WorkspaceManager::request_workspace(std::shared_ptr<Screen> screen, int key)
{
for (auto workspace : workspaces)
if (workspaces[key] != nullptr)
{
if (workspace.key == key)
auto workspace = workspaces[key];
auto active_workspace = workspace->get_active_workspace();
if (active_workspace == key)
{
workspace.screen->make_workspace_active(key);
return workspace.screen;
mir::log_warning("Same workspace selected twice in a row");
return workspace;
}
request_focus(key);
return workspace;
}
workspaces.push_back({
key,
screen
});
workspaces[key] = screen;
screen->advise_new_workspace(key);
request_focus(key);
registry.advise_created(workspaces[key], key);
return screen;
}
bool WorkspaceManager::request_first_available_workspace(std::shared_ptr<Screen> screen)
{
for (int i = 0; i < 10; i++)
for (int i = 1; i < NUM_WORKSPACES; i++)
{
bool can_use = true;
for (auto workspace : workspaces)
if (workspaces[i] == nullptr)
{
if (workspace.key == DEFAULT_WORKSPACES[i])
{
can_use = false;
}
}
if (can_use)
{
request_workspace(screen, DEFAULT_WORKSPACES[i]);
request_workspace(screen, i);
return true;
}
}
if (workspaces[0] == nullptr)
{
request_workspace(screen, 0);
return true;
}
return false;
}
bool WorkspaceManager::move_active_to_workspace(std::shared_ptr<Screen> screen, char workspace)
bool WorkspaceManager::move_active_to_workspace(std::shared_ptr<Screen> screen, int workspace)
{
auto window = tools_.active_window();
if (!window)
@ -81,17 +85,32 @@ bool WorkspaceManager::move_active_to_workspace(std::shared_ptr<Screen> screen,
return true;
}
bool WorkspaceManager::delete_workspace(char key)
bool WorkspaceManager::delete_workspace(int key)
{
for (auto it = workspaces.begin(); it != workspaces.end(); it++)
if (workspaces[key])
{
if (it->key == key)
{
it->screen->advise_workspace_deleted(key);
workspaces.erase(it);
return true;
}
workspaces[key]->advise_workspace_deleted(key);
registry.advise_removed(workspaces[key], key);
workspaces[key] = nullptr;
return true;
}
return false;
}
}
void WorkspaceManager::request_focus(int key)
{
if (!workspaces[key])
return;
auto active_screen = get_active_screen();
workspaces[key]->advise_workspace_active(key);
if (active_screen != nullptr)
{
auto active_workspace = active_screen->get_active_workspace();
registry.advise_focused(active_screen, active_workspace, workspaces[key], key);
}
else
registry.advise_focused(nullptr, -1, workspaces[key], key);
}

View File

@ -1,11 +1,14 @@
#ifndef WORKSPACE_MANAGER_H
#define WORKSPACE_MANAGER_H
#include "workspace_observer.h"
#include <memory>
#include <vector>
#include <miral/window_manager_tools.h>
#include <list>
#include <map>
#include <functional>
namespace miracle
{
@ -18,45 +21,36 @@ using miral::Workspace;
class Screen;
// TODO:
// As it stands, this isn't exactly what I want. What I do want
// is a workspace manager that is output-aware. The data structure
// should be a global WorkspaceManager who holds a mapping of Screens -> Workspaces.
// Workspaces should have a name and be organized according to that name. This
// goes to mean that we should ignore the idea of a vector of workspaces, and settle
// for char-encoded workspaces instead.
struct WorkspaceInfo
{
char key;
std::shared_ptr<Screen> screen;
};
class WorkspaceManager
{
public:
explicit WorkspaceManager(WindowManagerTools const& tools);
explicit WorkspaceManager(
WindowManagerTools const& tools,
WorkspaceObserverRegistrar& registry,
std::function<std::shared_ptr<Screen> const()> const& get_active_screen);
virtual ~WorkspaceManager() = default;
/// Request the workspace. If it does not yet exist, then one
/// is created on the current Screen. If it does exist, we navigate
/// to the screen containing that workspace and show it if it
/// isn't already shown.
std::shared_ptr<Screen> request_workspace(std::shared_ptr<Screen> screen, char workspace);
std::shared_ptr<Screen> request_workspace(std::shared_ptr<Screen> screen, int workspace);
bool request_first_available_workspace(std::shared_ptr<Screen> screen);
bool move_active_to_workspace(std::shared_ptr<Screen> screen, char workspace);
bool move_active_to_workspace(std::shared_ptr<Screen> screen, int workspace);
bool delete_workspace(char workspace);
bool delete_workspace(int workspace);
void request_focus(int workspace);
static int constexpr NUM_WORKSPACES = 10;
std::array<std::shared_ptr<Screen>, NUM_WORKSPACES> const& get_workspaces() { return workspaces; }
private:
WindowManagerTools tools_;
std::vector<WorkspaceInfo> workspaces;
void erase_if_empty(std::vector<std::shared_ptr<Workspace>>::iterator const& old_workspace);
WorkspaceObserverRegistrar& registry;
std::function<std::shared_ptr<Screen> const()> get_active_screen;
std::array<std::shared_ptr<Screen>, NUM_WORKSPACES> workspaces;
};
}

View File

@ -0,0 +1,64 @@
#include "workspace_observer.h"
using namespace miracle;
namespace
{
static int NEXT_ID = 0;
}
WorkspaceObserver::WorkspaceObserver()
: id{NEXT_ID}
{}
int WorkspaceObserver::get_id() const
{
return id;
}
void WorkspaceObserverRegistrar::register_interest(std::weak_ptr<WorkspaceObserver> observer)
{
observers.push_back(observer);
}
void WorkspaceObserverRegistrar::unregister_interest(miracle::WorkspaceObserver& observer)
{
observers.erase(std::remove_if(observers.begin(), observers.end(), [&observer](std::weak_ptr<WorkspaceObserver> const& other)
{
if (other.expired())
return true;
return other.lock()->get_id() == observer.get_id();
}));
}
void WorkspaceObserverRegistrar::advise_created(std::shared_ptr<Screen> const& info, int key)
{
for (auto& observer : observers)
{
if (!observer.expired())
observer.lock()->on_created(info, key);
}
}
void WorkspaceObserverRegistrar::advise_removed(std::shared_ptr<Screen> const& info, int key)
{
for (auto& observer : observers)
{
if (!observer.expired())
observer.lock()->on_removed(info, key);
}
}
void WorkspaceObserverRegistrar::advise_focused(
std::shared_ptr<Screen> const& previous,
int previous_key,
std::shared_ptr<Screen> const& current,
int current_key)
{
for (auto& observer : observers)
{
if (!observer.expired())
observer.lock()->on_focused(previous, previous_key, current, current_key);
}
}

45
src/workspace_observer.h Normal file
View File

@ -0,0 +1,45 @@
#ifndef MIRACLEWM_WORKSPACE_OBSERVER_H
#define MIRACLEWM_WORKSPACE_OBSERVER_H
#include "screen.h"
#include <mir/executor.h>
#include <memory>
namespace miracle
{
class Screen;
class WorkspaceObserver
{
public:
virtual ~WorkspaceObserver() = default;
virtual void on_created(std::shared_ptr<Screen> const&, int) = 0;
virtual void on_removed(std::shared_ptr<Screen> const&, int) = 0;
virtual void on_focused(std::shared_ptr<Screen> const& previous, int, std::shared_ptr<Screen> const& current, int) = 0;
int get_id() const;
protected:
WorkspaceObserver();
private:
int id;
};
class WorkspaceObserverRegistrar
{
public:
WorkspaceObserverRegistrar() = default;
void register_interest(std::weak_ptr<WorkspaceObserver>);
void unregister_interest(WorkspaceObserver&);
void advise_created(std::shared_ptr<Screen> const&, int);
void advise_removed(std::shared_ptr<Screen> const&, int);
void advise_focused(std::shared_ptr<Screen> const& previous, int, std::shared_ptr<Screen> const& current, int);
private:
std::vector<std::weak_ptr<WorkspaceObserver>> observers;
};
} // miracle
#endif //MIRACLEWM_WORKSPACE_OBSERVER_H