Add moving tabs left and right with key shortcuts (#700)

- [x] I have disclosed use of any AI generated code in my commit
messages.
- If you are using an LLM, and do not fully understand the changes it is
making to the code base, do not create a PR.
- In our experience, AI generated code often results in overly complex
code that lacks enough context for a proper fix or feature inclusion.
This results in considerably longer code reviews. Due to this, AI
authored or partially authored PRs may be closed without comment.
- [x] I understand these changes in full and will be able to respond to
review comments.
- [x] My change is accurately described in the commit message.
- [x] My contribution is tested and working as described.
- [x] I have read the [Developer Certificate of
Origin](https://developercertificate.org/) and certify my contribution
under its conditions.


https://github.com/pop-os/cosmic-term/issues/441 This PR adds option to
move tabs with left and right with Ctrl+Shift+Left/Right Arrow or by
using buttons in view menu.
This commit is contained in:
Jeremy Soller 2026-10-01 14:15:27 -06:00 • committed by GitHub
commit 3ef31d750d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 51 additions and 1 deletions

View file

@ -132,6 +132,8 @@ zoom-reset = Default text size
zoom-out = Smaller text
next-tab = Next tab
previous-tab = Previous tab
move-tab-left = Move tab left
move-tab-right = Move tab right
split-horizontal = Split horizontal
split-vertical = Split vertical
pane-toggle-maximize = Maximize or restore pane

View file

@ -8,7 +8,7 @@ use cosmic::iced::keyboard::key::Physical;
use cosmic::widget::menu::action::MenuAction;
use cosmic::widget::menu::key_bind::KeyBind;
use cosmic::widget::pane_grid::Pane;
use cosmic::widget::segmented_button::ReorderEvent;
use cosmic::widget::segmented_button::{InsertPosition, ReorderEvent};
use cosmic::{
Application, ApplicationExt, Element, action,
app::{Core, Settings, Task, context_drawer},
@ -281,6 +281,8 @@ pub enum Action {
TabNewNoProfile,
TabNext,
TabPrev,
TabMoveLeft,
TabMoveRight,
ToggleFullscreen,
WindowClose,
WindowNew,
@ -334,6 +336,8 @@ impl Action {
Self::TabNewNoProfile => Message::TabNewNoProfile,
Self::TabNext => Message::TabNext,
Self::TabPrev => Message::TabPrev,
Self::TabMoveLeft => Message::TabMoveLeft,
Self::TabMoveRight => Message::TabMoveRight,
Self::ToggleFullscreen => Message::ToggleFullscreen,
Self::WindowClose => Message::WindowClose,
Self::WindowNew => Message::WindowNew,
@ -442,6 +446,8 @@ pub enum Message {
TabNewNoProfile,
TabNext,
TabPrev,
TabMoveLeft,
TabMoveRight,
TermEvent(pane_grid::Pane, segmented_button::Entity, TermEvent),
TermEventTx(mpsc::UnboundedSender<(pane_grid::Pane, segmented_button::Entity, TermEvent)>),
ToggleFullscreen,
@ -2960,6 +2966,33 @@ impl Application for App {
}
}
}
Message::TabMoveLeft => {
if let Some(tab_model) = self.pane_model.active_mut() {
let pos: usize = tab_model
.position(tab_model.active())
.expect("at least one tab is always open")
.into();
if pos > 0 {
let dragged = tab_model.iter().nth(pos).expect("tab unavailable");
let target = tab_model.iter().nth(pos - 1).expect("previous tab missing");
tab_model.reorder(dragged, target, InsertPosition::Before);
}
}
}
Message::TabMoveRight => {
if let Some(tab_model) = self.pane_model.active_mut() {
let len = tab_model.iter().count();
let pos: usize = tab_model
.position(tab_model.active())
.expect("at least one tab is always open")
.into();
if pos < len - 1 && len > 1 {
let dragged = tab_model.iter().nth(pos).expect("tab unavailable");
let target = tab_model.iter().nth(pos + 1).expect("next tab missing");
tab_model.reorder(dragged, target, InsertPosition::After);
}
}
}
Message::TermEvent(pane, entity, event) => {
match event {
TermEvent::Bell => {

View file

@ -195,6 +195,9 @@ pub fn menu_bar<'a>(
MenuItem::Button(fl!("next-tab"), None, Action::TabNext),
MenuItem::Button(fl!("previous-tab"), None, Action::TabPrev),
MenuItem::Divider,
MenuItem::Button(fl!("move-tab-left"), None, Action::TabMoveLeft),
MenuItem::Button(fl!("move-tab-right"), None, Action::TabMoveRight),
MenuItem::Divider,
MenuItem::Button(
fl!("split-horizontal"),
None,

View file

@ -87,6 +87,8 @@ pub enum KeyBindAction {
TabNew,
TabNext,
TabPrev,
TabMoveLeft,
TabMoveRight,
ToggleFullscreen,
WindowClose,
WindowNew,
@ -127,6 +129,8 @@ impl KeyBindAction {
Self::TabNew => Some(Action::TabNew),
Self::TabNext => Some(Action::TabNext),
Self::TabPrev => Some(Action::TabPrev),
Self::TabMoveLeft => Some(Action::TabMoveLeft),
Self::TabMoveRight => Some(Action::TabMoveRight),
Self::ToggleFullscreen => Some(Action::ToggleFullscreen),
Self::WindowClose => Some(Action::WindowClose),
Self::WindowNew => Some(Action::WindowNew),
@ -288,6 +292,8 @@ pub fn action_label(action: KeyBindAction) -> String {
KeyBindAction::TabNew => fl!("new-tab"),
KeyBindAction::TabNext => fl!("next-tab"),
KeyBindAction::TabPrev => fl!("previous-tab"),
KeyBindAction::TabMoveLeft => fl!("move-tab-left"),
KeyBindAction::TabMoveRight => fl!("move-tab-right"),
KeyBindAction::ToggleFullscreen => fl!("toggle-fullscreen"),
KeyBindAction::WindowClose => fl!("close-window"),
KeyBindAction::WindowNew => fl!("new-window"),
@ -322,6 +328,8 @@ pub fn shortcut_groups() -> Vec<ShortcutGroup> {
KeyBindAction::TabClose,
KeyBindAction::TabNext,
KeyBindAction::TabPrev,
KeyBindAction::TabMoveLeft,
KeyBindAction::TabMoveRight,
KeyBindAction::TabActivate0,
KeyBindAction::TabActivate1,
KeyBindAction::TabActivate2,
@ -470,6 +478,10 @@ fn fallback_shortcuts() -> Shortcuts {
bind!([Ctrl], "Tab", TabNext);
bind!([Ctrl, Shift], "Tab", TabPrev);
// Ctrl+Shift+PageUp/Down move tabs left and right
bind!([Ctrl, Shift], "PageUp", TabMoveLeft);
bind!([Ctrl, Shift], "PageDown", TabMoveRight);
// Ctrl+Shift+# activates tabs by index
bind!([Ctrl, Shift], "1", TabActivate0);
bind!([Ctrl, Shift], "2", TabActivate1);