Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/user/keybinds.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,12 @@ These take no argument.
|--------|--------------|
| `window-focus-left` / `window-focus-right` | Move focus to the adjacent window along the row. |
| `window-focus-up` / `window-focus-down` | Move focus to the adjacent window along the column. |
| `window-focus-or-workspace-up` / `window-focus-or-workspace-down` | Move focus up or down within the column; at the boundary, switch to the adjacent workspace and restore its focus. |
| `window-focus-next` | Cycle focus to the next mapped window on the active workspace. |
| `window-move-to-workspace-next` / `window-move-to-workspace-previous` | Move the focused window to the adjacent workspace and follow it. These actions do not wrap around. |
| `column-move-left` / `column-move-right` | Move the focused window's column left or right. |
| `window-move-up` / `window-move-down` | Move the focused window up or down within its column. |
| `window-move-or-workspace-up` / `window-move-or-workspace-down` | Move the focused window up or down within its column; at the boundary, move it to the adjacent workspace. |
| `window-consume-left` | Pull the focused window into the column to its left. |
| `window-expel-right` | Pop the focused window out of its column into a new column to the right. |
| `window-cycle-width` | Cycle the focused column through its preset widths. |
Expand Down
4 changes: 4 additions & 0 deletions src/config/keybind_parse.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -187,11 +187,15 @@ namespace umbriel {
{"window-focus-down", "", KeybindAction::WindowFocusDown},
{"window-focus-left", "", KeybindAction::WindowFocusLeft},
{"window-focus-next", "", KeybindAction::WindowFocusNext},
{"window-focus-or-workspace-down", "", KeybindAction::WindowFocusOrWorkspaceDown},
{"window-focus-or-workspace-up", "", KeybindAction::WindowFocusOrWorkspaceUp},
{"window-focus-right", "", KeybindAction::WindowFocusRight},
{"window-focus-switch-floating", "", KeybindAction::WindowFocusSwitchFloating},
{"window-focus-up", "", KeybindAction::WindowFocusUp},
{"window-modify-width", "<delta>", KeybindAction::WindowModifyWidth, ActionArgKind::WidthDelta},
{"window-move-down", "", KeybindAction::WindowMoveDown},
{"window-move-or-workspace-down", "", KeybindAction::WindowMoveOrWorkspaceDown},
{"window-move-or-workspace-up", "", KeybindAction::WindowMoveOrWorkspaceUp},
{"window-move-to-output-down", "", KeybindAction::WindowMoveToOutputDown},
{"window-move-to-output-left", "", KeybindAction::WindowMoveToOutputLeft},
{"window-move-to-output-right", "", KeybindAction::WindowMoveToOutputRight},
Expand Down
4 changes: 4 additions & 0 deletions src/config/keybind_parse.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,15 @@ namespace umbriel {
WindowFocusRight,
WindowFocusUp,
WindowFocusDown,
WindowFocusOrWorkspaceUp,
WindowFocusOrWorkspaceDown,
WindowFocusSwitchFloating,
ColumnMoveLeft,
ColumnMoveRight,
WindowMoveUp,
WindowMoveDown,
WindowMoveOrWorkspaceUp,
WindowMoveOrWorkspaceDown,
WindowConsumeLeft,
WindowExpelRight,
WindowCycleWidth,
Expand Down
4 changes: 4 additions & 0 deletions src/scene/cheatsheet_rows.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,8 @@ namespace {
case A::WindowFocusRight:
case A::WindowFocusUp:
case A::WindowFocusDown:
case A::WindowFocusOrWorkspaceUp:
case A::WindowFocusOrWorkspaceDown:
case A::WindowFocusNext:
case A::WindowFocusId:
case A::WindowFocusSwitchFloating:
Expand All @@ -278,6 +280,8 @@ namespace {
case A::ColumnMoveRight:
case A::WindowMoveUp:
case A::WindowMoveDown:
case A::WindowMoveOrWorkspaceUp:
case A::WindowMoveOrWorkspaceDown:
case A::WindowConsumeLeft:
case A::WindowExpelRight:
case A::WindowCycleWidth:
Expand Down
54 changes: 54 additions & 0 deletions src/server/actions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,31 @@ namespace umbriel {
return true;
}

template <int Direction>
bool actionFocusVerticalOrWorkspace(Server& server, const Keybind& /*bind*/, std::string* /*error*/) {
if (Workspace* workspace = activeWorkspace(server)) {
if (View* target = workspace->focusVertical(Direction)) {
server.focusView(target, FocusReason::Directional);
} else {
// No window in this direction within the current workspace.
// Switch to the adjacent workspace, matching Niri's behavior.
WorkspaceGroup* group = workspace->group();
if (group == nullptr) {
return true;
}
const size_t index = workspace->index();
if (Direction < 0 && index == 0) {
return true;
}
Workspace* targetWorkspace = group->workspaceAt(index + static_cast<size_t>(Direction));
if (targetWorkspace != nullptr && targetWorkspace != group->active()) {
group->select(targetWorkspace);
}
}
}
return true;
}

template <int Direction> bool actionMoveColumn(Server& server, const Keybind& /*bind*/, std::string* /*error*/) {
if (Workspace* workspace = activeWorkspace(server)) {
workspace->moveFocusedColumn(Direction);
Expand All @@ -449,6 +474,31 @@ namespace umbriel {
return true;
}

template <int Direction>
bool actionMoveVerticalOrWorkspace(Server& server, const Keybind& /*bind*/, std::string* /*error*/) {
if (Workspace* workspace = activeWorkspace(server)) {
if (!workspace->moveFocusedVertical(Direction)) {
Workspace* source = activeWorkspace(server);
if (source == nullptr || source->group() == nullptr) {
return true;
}
WorkspaceGroup* group = source->group();
const size_t index = source->index();
if (Direction < 0 && index == 0) {
return true;
}
Workspace* target = group->workspaceAt(index + static_cast<size_t>(Direction));
if (target == nullptr || target == source) {
return true;
}
if (View* view = source->focusedView()) {
moveViewToWorkspace(server, *view, *target);
}
}
}
return true;
}

bool actionConsumeLeft(Server& server, const Keybind& /*bind*/, std::string* /*error*/) {
if (Workspace* workspace = activeWorkspace(server)) {
workspace->consumeFocusedLeft();
Expand Down Expand Up @@ -950,11 +1000,15 @@ namespace umbriel {
&actionFocusAdjacent<1>,
&actionFocusVertical<-1>,
&actionFocusVertical<1>,
&actionFocusVerticalOrWorkspace<-1>,
&actionFocusVerticalOrWorkspace<1>,
&actionFocusSwitchFloating,
&actionMoveColumn<-1>,
&actionMoveColumn<1>,
&actionMoveVertical<-1>,
&actionMoveVertical<1>,
&actionMoveVerticalOrWorkspace<-1>,
&actionMoveVerticalOrWorkspace<1>,
&actionConsumeLeft,
&actionExpelRight,
&actionCycleWidth<1>,
Expand Down
99 changes: 98 additions & 1 deletion tests/harness/checks/610_output_actions.sh
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,103 @@ if [[ $returned_workspace != "$start_workspace" ]]; then
exit 1
fi

# The cross-workspace variants first use an available vertical neighbor. Only
# the workspace boundary falls through to workspace navigation.
spawn_client vertical-local
wait_for_windows 2
local_id=$("$UMBRIEL" windows --json | jq -r '.[] | select(.title == "vertical-local") | .id')
accepts "window-focus:$local_id"
accepts "window-consume-left"
stacked=false
for _ in $(seq 40); do
if "$UMBRIEL" windows --json | jq -e \
'length == 2 and (.[0].workspace == .[1].workspace) and ([.[].y] | unique | length == 2)' > /dev/null; then
stacked=true
break
fi
sleep 0.1
done
if [[ $stacked != true ]]; then
echo "expected two rows in one column before vertical navigation"
exit 1
fi
read -r top_id bottom_id <<< "$("$UMBRIEL" windows --json | jq -r 'sort_by(.y) | "\(.[0].id) \(.[1].id)"')"
accepts "window-focus:$top_id"
accepts "window-focus-or-workspace-down"
bottom_active=false
for _ in $(seq 40); do
bottom_active=$("$UMBRIEL" windows --json | jq -r --arg id "$bottom_id" '.[] | select(.id == $id) | .active')
[[ $bottom_active == true ]] && break
sleep 0.1
done
if [[ $bottom_active != true ]]; then
echo "expected focus-down variant to use the lower row before changing workspaces"
exit 1
fi
if ! "$UMBRIEL" windows --json | jq -e --arg workspace "$start_workspace" \
'all(.[]; .workspace == $workspace)' > /dev/null; then
echo "vertical neighbor focus unexpectedly changed workspaces"
exit 1
fi

accepts "window-move-or-workspace-up"
local_moved=false
for _ in $(seq 40); do
local_moved=$("$UMBRIEL" windows --json | jq -r --arg id "$bottom_id" --arg other "$top_id" \
'([.[] | select(.id == $id) | .y][0]) < ([.[] | select(.id == $other) | .y][0])')
[[ $local_moved == true ]] && break
sleep 0.1
done
if [[ $local_moved != true ]]; then
echo "expected move-up variant to reorder rows before changing workspaces"
exit 1
fi
accepts "window-close"
wait_for_windows 1

accepts "window-focus-or-workspace-down"
active_now=true
for _ in $(seq 40); do
active_now=$("$UMBRIEL" windows --json | jq -r '.[0].active')
[[ $active_now == false ]] && break
sleep 0.1
done
if [[ $active_now != false ]]; then
echo "expected focus-down variant to switch at the workspace boundary"
exit 1
fi
accepts "window-focus-or-workspace-up"
for _ in $(seq 40); do
active_now=$("$UMBRIEL" windows --json | jq -r '.[0].active')
[[ $active_now == true ]] && break
sleep 0.1
done
if [[ $active_now != true ]]; then
echo "expected focus-up variant to return and restore focus"
exit 1
fi

accepts "window-move-or-workspace-down"
for _ in $(seq 40); do
returned_workspace=$("$UMBRIEL" windows --json | jq -r '.[0].workspace')
[[ $returned_workspace == "$moved_workspace" ]] && break
sleep 0.1
done
if [[ $returned_workspace != "$moved_workspace" ]]; then
echo "expected move-down variant to cross the workspace boundary"
exit 1
fi
accepts "window-move-or-workspace-up"
for _ in $(seq 40); do
returned_workspace=$("$UMBRIEL" windows --json | jq -r '.[0].workspace')
[[ $returned_workspace == "$start_workspace" ]] && break
sleep 0.1
done
if [[ $returned_workspace != "$start_workspace" ]]; then
echo "expected move-up variant to return to $start_workspace"
exit 1
fi

# window-modify-width: Headless output is 1280x720 with the shipped defaults (gap 8, border 2): viewport 1260, so -0.2 shrinks a column by about 252px.
# The exact geometry math lives in 110_scrolling_layout.sh (624 wide at 0.5).
before_w=$(jq -r '.[0].w' <<< "$("$UMBRIEL" windows --json)")
Expand Down Expand Up @@ -309,4 +406,4 @@ if [[ $min_h -ge 600 ]]; then
exit 1
fi

echo "directional actions reject on one output; workspace moves preserve width and other actions behave"
echo "local and cross-workspace actions, width preservation, centering, and layout switching behave"
7 changes: 7 additions & 0 deletions tests/unit/cheatsheet_rows.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,13 @@ UMBRIEL_TEST(groupTitlesArePlainTextNotMarkup) {
}
}

UMBRIEL_TEST(crossWorkspaceDirectionalActionsUseExpectedGroups) {
CHECK(umbriel::groupForAction(KeybindAction::WindowFocusOrWorkspaceUp) == umbriel::Group::Focus);
CHECK(umbriel::groupForAction(KeybindAction::WindowFocusOrWorkspaceDown) == umbriel::Group::Focus);
CHECK(umbriel::groupForAction(KeybindAction::WindowMoveOrWorkspaceUp) == umbriel::Group::MoveSize);
CHECK(umbriel::groupForAction(KeybindAction::WindowMoveOrWorkspaceDown) == umbriel::Group::MoveSize);
}

UMBRIEL_TEST(everyActionMapsToAGroupWithATitle) {
// groupForAction has no default arm to fall through to, so a new action that
// is never grouped would show up here.
Expand Down