diff --git a/src/equicordplugins/channelTabs/components/ChannelTab.tsx b/src/equicordplugins/channelTabs/components/ChannelTab.tsx
index 9df3ae3b1..3a040761d 100644
--- a/src/equicordplugins/channelTabs/components/ChannelTab.tsx
+++ b/src/equicordplugins/channelTabs/components/ChannelTab.tsx
@@ -99,6 +99,32 @@ export const NotificationDot = ({ channelIds }: { channelIds: string[]; }) => {
: null;
};
+interface TabNumberBadgeProps {
+ number: number;
+ position: "left" | "right";
+ isSelected: boolean;
+ isCompact: boolean;
+ isHovered: boolean;
+}
+
+export const TabNumberBadge = ({ number, position, isSelected, isCompact, isHovered }: TabNumberBadgeProps) => {
+ // hide badge if:
+ // 1. tab is currently selected
+ // 2. tab is compact AND not hovered
+ const shouldHide = isSelected || (isCompact && !isHovered);
+
+ if (shouldHide) return null;
+
+ return (
+
+ {number}
+
+ );
+};
+
function ChannelTabContent(props: ChannelTabsProps & {
guild?: Guild,
channel?: Channel;
@@ -247,6 +273,9 @@ export default function ChannelTab(props: ChannelTabsProps & { index: number; })
const [isClosing, setIsClosing] = useState(false);
const [isDragging, setIsDragging] = useState(false);
const [isDropTarget, setIsDropTarget] = useState(false);
+ const [isHovered, setIsHovered] = useState(false);
+
+ const { showTabNumbers, tabNumberPosition } = settings.use(["showTabNumbers", "tabNumberPosition"]);
useEffect(() => {
if (isEntering) {
@@ -273,6 +302,7 @@ export default function ChannelTab(props: ChannelTabsProps & { index: number; })
const startX = e.clientX;
const startWidth = ref.current?.getBoundingClientRect().width || 0;
const baseWidth = 192; // 12rem in pixels (assuming 16px base font)
+ let pendingScale = settings.store.tabWidthScale;
document.body.style.cursor = "ew-resize";
document.body.style.userSelect = "none";
@@ -284,7 +314,12 @@ export default function ChannelTab(props: ChannelTabsProps & { index: number; })
// 50% and 200% scale
const clampedScale = Math.max(0.5, Math.min(2, newScale));
- settings.store.tabWidthScale = Math.round(clampedScale * 100);
+ pendingScale = Math.round(clampedScale * 100);
+
+ // update CSS variable immediately for visual feedback
+ if (ref.current) {
+ ref.current.style.setProperty("--tab-width-scale", String(pendingScale / 100));
+ }
};
const handleMouseUp = () => {
@@ -292,6 +327,12 @@ export default function ChannelTab(props: ChannelTabsProps & { index: number; })
document.removeEventListener("mouseup", handleMouseUp);
document.body.style.cursor = "";
document.body.style.userSelect = "";
+
+ settings.store.tabWidthScale = pendingScale;
+
+ if (ref.current) {
+ ref.current.style.removeProperty("--tab-width-scale");
+ }
};
document.addEventListener("mousemove", handleMouseMove);
@@ -398,6 +439,8 @@ export default function ChannelTab(props: ChannelTabsProps & { index: number; })
})}
key={index}
ref={ref}
+ onMouseEnter={() => setIsHovered(true)}
+ onMouseLeave={() => setIsHovered(false)}
onAuxClick={e => {
if (e.button === 1 /* middle click */)
closeTab(id);
@@ -412,7 +455,29 @@ export default function ChannelTab(props: ChannelTabsProps & { index: number; })
className={cl("tab-inner")}
data-compact={compact}
>
+ {/* left position badge */}
+ {showTabNumbers && tabNumberPosition === "left" && (
+
+ )}
+
+
+ {/* right position badge */}
+ {showTabNumbers && tabNumberPosition === "right" && (
+
+ )}
diff --git a/src/equicordplugins/channelTabs/components/ChannelTabsContainer.tsx b/src/equicordplugins/channelTabs/components/ChannelTabsContainer.tsx
index f0d7e6502..e7df8227a 100644
--- a/src/equicordplugins/channelTabs/components/ChannelTabsContainer.tsx
+++ b/src/equicordplugins/channelTabs/components/ChannelTabsContainer.tsx
@@ -13,7 +13,7 @@ import { useForceUpdater } from "@utils/react";
import { findComponentByCodeLazy, findStoreLazy } from "@webpack";
import { Button, ContextMenuApi, Flex, FluxDispatcher, useCallback, useEffect, useRef, UserStore, useState, useStateFromStores } from "@webpack/common";
-import { BasicChannelTabsProps, ChannelTabsProps, clearStaleNavigationContext, createTab, handleChannelSwitch, isNavigationFromSource, moveToTab, openedTabs, openStartupTabs, saveTabs, settings, setUpdaterFunction, useGhostTabs } from "../util";
+import { BasicChannelTabsProps, ChannelTabsProps, clearStaleNavigationContext, closeTab, createTab, handleChannelSwitch, isNavigationFromSource, isTabSelected, moveToTab, openedTabs, openStartupTabs, saveTabs, settings, setUpdaterFunction, useGhostTabs } from "../util";
import BookmarkContainer from "./BookmarkContainer";
import ChannelTab, { PreviewTab } from "./ChannelTab";
import { BasicContextMenu } from "./ContextMenus";
@@ -31,8 +31,15 @@ export default function ChannelsTabsContainer(props: BasicChannelTabsProps) {
showBookmarkBar,
widerTabsAndBookmarks,
tabWidthScale,
- enableHotkeys,
- hotkeyCount,
+ enableNumberKeySwitching,
+ numberKeySwitchCount,
+ enableCloseTabShortcut,
+ enableNewTabShortcut,
+ enableTabCycleShortcut,
+ closeTabKeybind,
+ newTabKeybind,
+ cycleTabForwardKeybind,
+ cycleTabBackwardKeybind,
tabBarPosition,
animationHover,
animationSelection,
@@ -55,8 +62,15 @@ export default function ChannelsTabsContainer(props: BasicChannelTabsProps) {
"showBookmarkBar",
"widerTabsAndBookmarks",
"tabWidthScale",
- "enableHotkeys",
- "hotkeyCount",
+ "enableNumberKeySwitching",
+ "numberKeySwitchCount",
+ "enableCloseTabShortcut",
+ "enableNewTabShortcut",
+ "enableTabCycleShortcut",
+ "closeTabKeybind",
+ "newTabKeybind",
+ "cycleTabForwardKeybind",
+ "cycleTabBackwardKeybind",
"tabBarPosition",
"animationHover",
"animationSelection",
@@ -86,6 +100,7 @@ export default function ChannelsTabsContainer(props: BasicChannelTabsProps) {
}, [userId]);
const ref = useRef(null);
+ const tabScrollerRef = useRef(null);
useEffect(() => {
setUpdaterFunction(update);
@@ -115,11 +130,35 @@ export default function ChannelsTabsContainer(props: BasicChannelTabsProps) {
_update();
}, [widerTabsAndBookmarks]);
useEffect(() => {
- if (!enableHotkeys) return;
+ const matchesKeybind = (event: KeyboardEvent, keybindString: string): boolean => {
+ const parts = keybindString.split("+");
+ const hasCtrl = parts.includes("CTRL");
+ const hasShift = parts.includes("SHIFT");
+ const hasAlt = parts.includes("ALT");
+ const mainKey = parts[parts.length - 1].toLowerCase();
+
+ const ctrlPressed = event.ctrlKey || event.metaKey;
+ const shiftPressed = event.shiftKey;
+ const altPressed = event.altKey;
+ const keyPressed = event.key.toLowerCase();
+
+ // special handling for TAB key
+ if (mainKey === "tab") {
+ return hasCtrl === ctrlPressed && hasShift === shiftPressed && hasAlt === altPressed && keyPressed === "tab";
+ }
+
+ // special handling for SPACE
+ if (mainKey === "space") {
+ return hasCtrl === ctrlPressed && hasShift === shiftPressed && hasAlt === altPressed && keyPressed === " ";
+ }
+
+ return hasCtrl === ctrlPressed && hasShift === shiftPressed && hasAlt === altPressed && keyPressed === mainKey;
+ };
const handleKeyDown = (event: KeyboardEvent) => {
const target = event.target as HTMLElement;
+ // skip if typing in input fields
if (
target.tagName === "INPUT" ||
target.tagName === "TEXTAREA" ||
@@ -128,23 +167,80 @@ export default function ChannelsTabsContainer(props: BasicChannelTabsProps) {
return;
}
- const keyNumber = parseInt(event.key, 10);
-
- if (!isNaN(keyNumber) && keyNumber >= 1 && keyNumber <= hotkeyCount) {
- const tabIndex = keyNumber - 1;
- if (openedTabs[tabIndex]) {
- event.preventDefault();
- moveToTab(openedTabs[tabIndex].id);
+ // 1. number key switching (1-9)
+ if (enableNumberKeySwitching) {
+ const keyNumber = parseInt(event.key, 10);
+ if (!isNaN(keyNumber) && keyNumber >= 1 && keyNumber <= numberKeySwitchCount) {
+ const tabIndex = keyNumber - 1;
+ if (openedTabs[tabIndex]) {
+ event.preventDefault();
+ moveToTab(openedTabs[tabIndex].id);
+ return;
+ }
}
}
+
+ // 2. close tab shortcut (default: CTRL+W)
+ if (enableCloseTabShortcut && matchesKeybind(event, closeTabKeybind)) {
+ event.preventDefault();
+ const currentTab = openedTabs.find(t => isTabSelected(t.id));
+ if (currentTab && openedTabs.length > 1) {
+ closeTab(currentTab.id);
+ }
+ return;
+ }
+
+ // 3. new tab shortcut (default: CTRL+T)
+ if (enableNewTabShortcut && matchesKeybind(event, newTabKeybind)) {
+ event.preventDefault();
+ event.stopPropagation(); // prevent discord's quick switcher from seeing this
+ createTab(props, true);
+ return;
+ }
+
+ // 4. cycle tabs forward (default: CTRL+TAB)
+ if (enableTabCycleShortcut && matchesKeybind(event, cycleTabForwardKeybind)) {
+ event.preventDefault();
+ event.stopPropagation(); // prevent discord's guild switcher from seeing this
+ const currentIndex = openedTabs.findIndex(t => isTabSelected(t.id));
+ if (currentIndex !== -1 && openedTabs.length > 1) {
+ const nextIndex = (currentIndex + 1) % openedTabs.length;
+ moveToTab(openedTabs[nextIndex].id);
+ }
+ return;
+ }
+
+ // 5. cycle tabs backward (default: CTRL+SHIFT+TAB)
+ if (enableTabCycleShortcut && matchesKeybind(event, cycleTabBackwardKeybind)) {
+ event.preventDefault();
+ event.stopPropagation(); // prevent Discord's guild switcher from seeing this
+ const currentIndex = openedTabs.findIndex(t => isTabSelected(t.id));
+ if (currentIndex !== -1 && openedTabs.length > 1) {
+ const nextIndex = (currentIndex - 1 + openedTabs.length) % openedTabs.length;
+ moveToTab(openedTabs[nextIndex].id);
+ }
+ return;
+ }
};
- document.addEventListener("keydown", handleKeyDown);
+ document.addEventListener("keydown", handleKeyDown, true);
return () => {
- document.removeEventListener("keydown", handleKeyDown);
+ document.removeEventListener("keydown", handleKeyDown, true);
};
- }, [enableHotkeys, hotkeyCount]);
+ }, [
+ enableNumberKeySwitching,
+ numberKeySwitchCount,
+ enableCloseTabShortcut,
+ closeTabKeybind,
+ enableNewTabShortcut,
+ newTabKeybind,
+ enableTabCycleShortcut,
+ cycleTabForwardKeybind,
+ cycleTabBackwardKeybind,
+ props,
+ openedTabs
+ ]);
useEffect(() => {
if (userId) {
@@ -200,9 +296,11 @@ export default function ChannelsTabsContainer(props: BasicChannelTabsProps) {
>}
- {openedTabs.filter(tab => tab != null).map((tab, i) =>
-
- )}
+
+ {openedTabs.filter(tab => tab != null).map((tab, i) =>
+
+ )}
+