31 décembre 2025
Important : Darktable rétablit automatiquement les raccourcis standard manquants au démarrage.
Pour éviter que ta configuration personnalisée ne soit perturbée, tu devrais désactiver cette fonction automatique.
Dans Darktable, allez dans Paramètres > Divers .
Désactivez l'option « Restaurer les raccourcis clavier au démarrage » (si elle est disponible/active).
Installation du fichier :
Quitter Darktable
Remplacez le fichier shortcutsrc dans ton dossier de configuration par la nouvelle version.
Toggle Theme
QWERTZ
QWERTY
AZERTY
Filter Keyboard by Context:
All
Global
Lighttable
Darkroom
Shortcut Key
Action (Value)
Context
Actions
const preloadedShortcutsrcContent = « ;
// — CONFIGURATION —
const KEYBOARD_LAYOUTS = {
qwertz: {
fkeys: [
[
[« Escape »],
« SPACER »,
[« F1 », « F1 », « key-fkey »],
[« F2 », « F2 », « key-fkey »],
[« F3 », « F3 », « key-fkey »],
[« F4 », « F4 », « key-fkey »],
« SPACER »,
[« F5 », « F5 », « key-fkey »],
[« F6 », « F6 », « key-fkey »],
[« F7 », « F7 », « key-fkey »],
[« F8 », « F8 », « key-fkey »],
« SPACER »,
[« F9 », « F9 », « key-fkey »],
[« F10 », « F10 », « key-fkey »],
[« F11 », « F11 », « key-fkey »],
[« F12 », « F12 », « key-fkey »],
« SPACER »,
],
],
main: [
[
[« ^ », « asciicircum »],
[« 1 »],
[« 2 »],
[« 3 »],
[« 4 »],
[« 5 »],
[« 6 »],
[« 7 »],
[« 8 »],
[« 9 »],
[« 0 »],
[« ß », « ssharp »],
[« ´ », « acute »],
[« BackSpace », « BackSpace », « key-backspace »],
],
[
[« Tab », « Tab », « key-tab », « modifier »],
[« q »],
[« w »],
[« e »],
[« r »],
[« t »],
[« z »],
[« u »],
[« i »],
[« o »],
[« p »],
[« Ü », « udiaeresis »],
[« + », « plus »],
[« Return », « Return », « key-return »],
],
[
[« Caps », « Caps_Lock », « key-caps », « modifier »],
[« a »],
[« s »],
[« d »],
[« f »],
[« g »],
[« h »],
[« j »],
[« k »],
[« l »],
[« Ö », « odiaeresis »],
[« Ä », « adiaeresis »],
[« # », « numbersign »],
],
[
[« Shift », « shift », « key-shift-l », « modifier »],
[« <", "<"],
["y"],
["x"],
["c"],
["v"],
["b"],
["n"],
["m"],
[",", "comma"],
[".", "period"],
["-", "minus"],
["Shift", "shift", "key-shift-r", "modifier"],
],
[
["Ctrl", "control", "key-ctrl", "modifier"],
["Win", "super", "", "modifier"],
["Alt", "alt", "", "modifier"],
["Space", "space", "key-space"],
["AltGr", "alt", "", "modifier"],
["Ctrl", "control", "key-ctrl", "modifier"],
],
],
special: [
[
["Print", "Print", "key-special"],
["Scroll", "Scroll", "key-special"],
["Pause", "Pause", "key-special"],
["None", "None", "key-special"],
],
[
["Ins", "Insert", "key-special"],
["Home", "Home", "key-special"],
["P_Up", "Page_Up", "key-special"],
],
[
["Delete", "Delete", "key-special"],
["End", "End", "key-special"],
["P_Down", "Page_Down", "key-special"],
],
],
arrows: [
[
["Left", "Left", "key-special"],
["Up", "Up", "key-special"],
["Down", "Down", "key-special"],
["Right", "Right", "key-special"],
],
],
},
qwerty: {
fkeys: [
[
["Escape"],
"SPACER",
["F1", "F1", "key-fkey"],
["F2", "F2", "key-fkey"],
["F3", "F3", "key-fkey"],
["F4", "F4", "key-fkey"],
"SPACER",
["F5", "F5", "key-fkey"],
["F6", "F6", "key-fkey"],
["F7", "F7", "key-fkey"],
["F8", "F8", "key-fkey"],
"SPACER",
["F9", "F9", "key-fkey"],
["F10", "F10", "key-fkey"],
["F11", "F11", "key-fkey"],
["F12", "F12", "key-fkey"],
"SPACER",
],
],
main: [
[
["~", "asciitilde"],
["1"],
["2"],
["3"],
["4"],
["5"],
["6"],
["7"],
["8"],
["9"],
["0"],
["-", "minus"],
["=", "equal"],
["BackSpace", "BackSpace", "key-backspace"],
],
[
["Tab", "Tab", "key-tab", "modifier"],
["q"],
["w"],
["e"],
["r"],
["t"],
["y"],
["u"],
["i"],
["o"],
["p"],
["[", "bracketleft"],
["]", "bracketright"],
["\\", "backslash"],
],
[
["Caps", "Caps_Lock", "key-caps", "modifier"],
["a"],
["s"],
["d"],
["f"],
["g"],
["h"],
["j"],
["k"],
["l"],
[";", "semicolon"],
["'", "quote"],
["Enter", "Return", "key-return"],
],
[
["Shift", "shift", "key-shift-l", "modifier"],
[",", "comma"],
[".", "period"],
["/", "slash"],
["z"],
["x"],
["c"],
["v"],
["b"],
["n"],
["m"],
["Shift", "shift", "key-shift-r", "modifier"],
],
[
["Ctrl", "control", "key-ctrl", "modifier"],
["Win", "super", "", "modifier"],
["Alt", "alt", "", "modifier"],
["Space", "space", "key-space"],
["AltGr", "alt", "", "modifier"],
["Ctrl", "control", "key-ctrl", "modifier"],
],
],
special: [
[
["Print", "Print", "key-special"],
["Scroll", "Scroll", "key-special"],
["Pause", "Pause", "key-special"],
["None", "None", "key-special"],
],
[
["Insert", "Insert", "key-special"],
["Home", "Home", "key-special"],
["P_Up", "Page_Up", "key-special"],
],
[
["Del", "Delete", "key-special"],
["End", "End", "key-special"],
["P_Down", "Page_Down", "key-special"],
],
],
arrows: [
[
["Left", "Left", "key-special"],
["Up", "Up", "key-special"],
["Down", "Down", "key-special"],
["Right", "Right", "key-special"],
],
],
},
azerty: {
fkeys: [
[
["Escape"],
"SPACER",
["F1", "F1", "key-fkey"],
["F2", "F2", "key-fkey"],
["F3", "F3", "key-fkey"],
["F4", "F4", "key-fkey"],
"SPACER",
["F5", "F5", "key-fkey"],
["F6", "F6", "key-fkey"],
["F7", "F7", "key-fkey"],
["F8", "F8", "key-fkey"],
"SPACER",
["F9", "F9", "key-fkey"],
["F10", "F10", "key-fkey"],
["F11", "F11", "key-fkey"],
["F12", "F12", "key-fkey"],
"SPACER",
],
],
main: [
[
["²", "asciicircum"],
["1"],
["2"],
["3"],
["4"],
["5"],
["6"],
["7"],
["8"],
["9"],
["0"],
["°", "degree"],
["+", "plus"],
["BackSpace", "BackSpace", "key-backspace"],
],
[
["Tab", "Tab", "key-tab", "modifier"],
["a"],
["z"],
["e"],
["r"],
["t"],
["y"],
["u"],
["i"],
["o"],
["p"],
["^", "asciicircum"],
["$", "dollar"],
["Return", "Return", "key-return"],
],
[
["Caps", "Caps_Lock", "key-caps", "modifier"],
["q"],
["s"],
["d"],
["f"],
["g"],
["h"],
["j"],
["k"],
["l"],
["m"],
["ù", "grave"],
["*", "asterisk"],
],
[
["Shift", "shift", "key-shift-l", "modifier"],
[" {
if (!str) return « »;
return str
.replace(/&/g, « & »)
.replace(//g, « > »)
.replace(/ »/g, « " »)
.replace(/’/g, « ' »);
};
const escapeAttr = (str) => {
if (!str) return « »;
return str.replace(/ »/g, « " »);
};
const normalizeKey = (key) => {
if (!key) return « »;
return key
.trim()
.replace(/^;+|;+$/g, « »)
.replace(/;{2,}/g, « ; »);
};
const getContext = (value) => {
if (!value || !value.includes(« / »)) {
return « global »;
}
const [prefix, view] = value.split(« / »);
if (prefix === « views ») {
if (view && view.startsWith(« darkroom »)) return « darkroom »;
if (view && view.startsWith(« lighttable »))
return « lighttable »;
return « lighttable »; // Default for ‘views’ is lighttable
}
if ([« actions », « iop », « utility »].includes(prefix)) {
return « darkroom »;
}
return « global »;
};
const parseFileContent = (content) => {
const lines = content.split(« \n »);
const parsedShortcuts = [];
nextId = 0;
lines.forEach((line) => {
line = line.trim();
if (line.startsWith(« # ») || !line) return;
if (line.includes(« = »)) {
const [key, value] = line.split(« = », 2);
if (key) {
const trimmedValue = value.trim();
parsedShortcuts.push({
id: nextId++,
key: key.trim(),
value: trimmedValue,
context: getContext(trimmedValue),
});
}
}
});
return parsedShortcuts;
};
const processAndRenderContent = (content, sourceName) => {
try {
shortcuts = parseFileContent(content);
fileInfo.textContent = `Loaded from « ${sourceName} ». Found ${shortcuts.length} shortcuts.`;
mainContent.classList.remove(« d-none »);
renderAll();
} catch (err) {
console.error(« Error processing file content: », err);
fileInfo.textContent = `Error: Could not process content from « ${sourceName} ». Check console for details.`;
mainContent.classList.add(« d-none »);
}
};
const renderAll = () => {
renderKeyboard();
renderTable();
renderFilterInfo();
};
const getKeyboardMap = () => {
const keyMap = {};
let shortcutsToDisplay = shortcuts;
if (currentKeyboardContext !== « all ») {
shortcutsToDisplay = shortcuts.filter(
(s) => s.context === currentKeyboardContext,
);
}
shortcutsToDisplay.forEach((entry) => {
const parts = entry.key
.toLowerCase()
.split(« ; »)
.map((k) => k.trim());
parts.forEach((part) => {
if (!part) return;
if (!keyMap[part]) keyMap[part] = [];
keyMap[part].push(
`[${entry.context}] ${escapeHTML(entry.key)} = ${escapeHTML(entry.value)}`,
);
});
});
return keyMap;
};
// — RENDER FUNCTIONS —
const renderKeyboard = () => {
const keyMap = getKeyboardMap();
keyboardContainer.innerHTML = « »;
const createKeyElement = (keyInfo) => {
if (!keyInfo)
return ‘
‘;
if (keyInfo === « SPACER »)
return ‘
‘;
const [
displayText,
dataKey = displayText.toLowerCase(),
cssClass = « »,
modifierClass = « »,
] = keyInfo;
const keyId = dataKey.toLowerCase();
if (!keyId) return « »;
const isAssigned = keyMap[keyId];
const isSelected = currentFilterKeys.includes(keyId);
const tooltip = isAssigned
? `
${keyMap[keyId].join(« \n »)} `
: « »;
return `
${displayText}${tooltip}
`;
};
Object.entries(KEYBOARD_LAYOUTS[currentLayout]).forEach(
([blockName, layout]) => {
const blockContainer = document.createElement(« div »);
blockContainer.classList.add(
« keyboard-block »,
`keyboard-${blockName}`,
);
layout.forEach((row) => {
const rowContainer = document.createElement(« div »);
rowContainer.classList.add(« key-row »);
row.forEach((keyInfo) => {
rowContainer.innerHTML +=
createKeyElement(keyInfo);
});
blockContainer.appendChild(rowContainer);
});
keyboardContainer.appendChild(blockContainer);
},
);
};
const renderTable = () => {
let displayData = […shortcuts];
if (currentSearchTerm) {
const lowerCaseSearchTerm = currentSearchTerm.toLowerCase();
displayData = displayData.filter(
(entry) =>
entry.key
.toLowerCase()
.includes(lowerCaseSearchTerm) ||
entry.value
.toLowerCase()
.includes(lowerCaseSearchTerm) ||
entry.context
.toLowerCase()
.includes(lowerCaseSearchTerm),
);
}
if (currentFilterKeys.length > 0) {
displayData = displayData.filter((entry) => {
const entryKeys = entry.key
.toLowerCase()
.split(« ; »)
.map((k) => k.trim());
return currentFilterKeys.every((filterKey) =>
entryKeys.includes(filterKey),
);
});
}
switch (currentSortBy) {
case « key »:
displayData.sort((a, b) => a.key.localeCompare(b.key));
break;
case « value »:
displayData.sort((a, b) =>
a.value.localeCompare(b.value),
);
break;
}
tableBody.innerHTML =
displayData.length === 0 &&
!document.querySelector(« .add-new-row »)
? ‘
No shortcuts found for the current filter. ‘
: displayData
.map(
(entry) =>
`
${entry.context} Update Delete `,
)
.join(« »);
};
const renderFilterInfo = () => {
if (currentFilterKeys.length > 0) {
filterInfo.innerHTML = `Filter active:
${currentFilterKeys.join( » + « )} Clear Filter `;
} else {
filterInfo.innerHTML = « »;
}
};
// — EVENT HANDLERS —
fileInput.addEventListener(« change », (event) => {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
processAndRenderContent(e.target.result, file.name);
};
reader.readAsText(file);
});
sortSelect.addEventListener(« change », (e) => {
currentSortBy = e.target.value;
renderTable();
});
searchInput.addEventListener(« input », (e) => {
currentSearchTerm = e.target.value;
renderTable();
});
downloadButton.addEventListener(« click », () => {
const fileContent = shortcuts
.map((entry) => `${entry.key}=${entry.value}`)
.join(« \n »);
const blob = new Blob([fileContent], {
type: « text/plain;charset=utf-8 »,
});
const a = document.createElement(« a »);
a.href = URL.createObjectURL(blob);
a.download = « shortcutsrc »;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
});
addShortcutButton.addEventListener(« click », () => {
if (document.querySelector(« .add-new-row »)) return;
const newRow = document.createElement(« tr »);
newRow.classList.add(« add-new-row »);
newRow.innerHTML = `
– Save Cancel `;
tableBody.prepend(newRow);
});
keyboardContainer.addEventListener(« click », (e) => {
const keyElement = e.target.closest(« .filter-key »);
if (!keyElement) return;
const filterKey = keyElement.dataset.key;
if (e.shiftKey) {
const index = currentFilterKeys.indexOf(filterKey);
if (index > -1) {
currentFilterKeys.splice(index, 1);
} else {
currentFilterKeys.push(filterKey);
}
} else {
currentFilterKeys =
currentFilterKeys.length === 1 &&
currentFilterKeys[0] === filterKey
? []
: [filterKey];
}
renderAll();
});
contextFilterButtons.addEventListener(« click », (e) => {
const button = e.target.closest(« button »);
if (!button) return;
currentKeyboardContext = button.dataset.context;
contextFilterButtons
.querySelectorAll(« button »)
.forEach((btn) => {
btn.classList.remove(
« btn-primary »,
« btn-outline-secondary »,
);
if (btn === button) {
btn.classList.add(« btn-primary »);
} else {
btn.classList.add(« btn-outline-secondary »);
}
});
renderKeyboard();
});
filterInfo.addEventListener(« click », (e) => {
if (e.target.id === « clear-filter-btn ») {
currentFilterKeys = [];
renderAll();
}
});
tableBody.addEventListener(« click », (e) => {
const row = e.target.closest(« tr »);
if (!row) return;
const handleUpdate = (isNew) => {
const keyInput = row.querySelector(« td:nth-child(1) input »);
const valueInput = row.querySelector(
« td:nth-child(2) input »,
);
const newKey = normalizeKey(keyInput.value);
const newValue = valueInput.value.trim();
if (!newKey || !newValue) {
alert(« Key and Value cannot be empty. »);
return;
}
const newContext = getContext(newValue);
const entryId = isNew ? -1 : parseInt(row.dataset.id, 10);
const conflict = shortcuts.find(
(s) =>
s.id !== entryId &&
s.key === newKey &&
(s.context === newContext ||
s.context === « global » ||
newContext === « global »),
);
if (conflict) {
alert(
`Conflict found! The key ‘${newKey}’ is already assigned for the context ‘${newContext}’ (Action: ${conflict.value}). Global shortcuts cannot be overwritten in a specific context and vice versa with the same key.`,
);
return;
}
if (isNew) {
shortcuts.push({
id: nextId++,
key: newKey,
value: newValue,
context: newContext,
});
} else {
const entry = shortcuts.find((s) => s.id === entryId);
if (entry) {
entry.key = newKey;
entry.value = newValue;
entry.context = newContext;
}
}
renderAll();
};
if (e.target.classList.contains(« save-new-btn »))
handleUpdate(true);
if (e.target.classList.contains(« update-btn »))
handleUpdate(false);
if (e.target.classList.contains(« cancel-new-btn »)) row.remove();
if (e.target.classList.contains(« delete-btn »)) {
const entryId = parseInt(row.dataset.id, 10);
const entryIndex = shortcuts.findIndex(
(s) => s.id === entryId,
);
if (
entryIndex > -1 &&
confirm(
`Really delete shortcut « ${shortcuts[entryIndex].key} »?`,
)
) {
shortcuts.splice(entryIndex, 1);
renderAll();
}
}
});
const loadFromUrl = async () => {
const urlParams = new URLSearchParams(window.location.search);
const srcUrl = urlParams.get(« src »);
if (srcUrl) {
const fileInputCard = fileInput.closest(« .card »);
if (fileInputCard) {
fileInputCard.classList.add(« d-none »);
}
try {
fileInfo.textContent = `Loading from URL: ${srcUrl}`;
const response = await fetch(srcUrl);
if (!response.ok) {
throw new Error(
`HTTP error! status: ${response.status}`,
);
}
const content = await response.text();
processAndRenderContent(content, srcUrl);
} catch (err) {
console.error(
« Error fetching or processing file from URL: »,
err,
);
fileInfo.textContent = `Error: Could not load file from URL. ${err.message}`;
mainContent.classList.add(« d-none »);
if (fileInputCard) {
fileInputCard.classList.remove(« d-none »);
}
}
}
};
document.body.addEventListener(« dragover », (event) => {
event.preventDefault();
document.body.classList.add(« drag-over »);
});
document.body.addEventListener(« dragleave », () => {
document.body.classList.remove(« drag-over »);
});
document.body.addEventListener(« drop », (event) => {
event.preventDefault();
document.body.classList.remove(« drag-over »);
const file = event.dataTransfer.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
processAndRenderContent(e.target.result, file.name);
};
reader.readAsText(file);
});
// — INITIALIZATION —
document.addEventListener(« DOMContentLoaded », () => {
if (
preloadedShortcutsrcContent &&
preloadedShortcutsrcContent.trim() !== « »
) {
const fileInputCard = fileInput.closest(« .card »);
if (fileInputCard) {
fileInputCard.classList.add(« d-none »);
}
processAndRenderContent(
preloadedShortcutsrcContent,
« pre-loaded variable »,
);
} else {
loadFromUrl();
}
// Standard-Theme auf « dark » setzen
document.body.classList.add(« dark-mode »);
localStorage.setItem(« theme », « dark »);
// QWERTZ als Standard-Tastaturbelegung
keyboardLayoutSelect.value = « qwertz »;
currentLayout = « qwertz »;
renderAll();
});
// — THEME —
document
.getElementById(« theme-toggle »)
.addEventListener(« click », () => {
document.body.classList.toggle(« dark-mode »);
localStorage.setItem(
« theme »,
document.body.classList.contains(« dark-mode »)
? « dark »
: « light »,
);
});
if (localStorage.getItem(« theme ») === « dark ») {
document.body.classList.add(« dark-mode »);
}
// — KEYBOARD LAYOUT —
keyboardLayoutSelect.addEventListener(« change », (e) => {
currentLayout = e.target.value;
renderAll();
});