From 8e34105dade67163f7a3714be82371196e4fc99e Mon Sep 17 00:00:00 2001 From: Aryan Jain Date: Fri, 21 Feb 2025 10:27:25 +0530 Subject: [PATCH] refactor: typescript defined addShortcit.ts, model.types.ts and normalizers.plugin.ts --- .../model/{addShortcut.js => addShortcut.ts} | 41 +- .../src/hotkey_binder/model/model.types.ts | 24 ++ .../model/normalize/normalizer.plugin.js | 389 ------------------ .../model/normalize/normalizer.plugin.ts | 264 ++++++++++++ 4 files changed, 309 insertions(+), 409 deletions(-) rename src/simulator/src/hotkey_binder/model/{addShortcut.js => addShortcut.ts} (76%) delete mode 100644 src/simulator/src/hotkey_binder/model/normalize/normalizer.plugin.js create mode 100644 src/simulator/src/hotkey_binder/model/normalize/normalizer.plugin.ts diff --git a/src/simulator/src/hotkey_binder/model/addShortcut.js b/src/simulator/src/hotkey_binder/model/addShortcut.ts similarity index 76% rename from src/simulator/src/hotkey_binder/model/addShortcut.js rename to src/simulator/src/hotkey_binder/model/addShortcut.ts index 6233f71e..16256fc4 100644 --- a/src/simulator/src/hotkey_binder/model/addShortcut.js +++ b/src/simulator/src/hotkey_binder/model/addShortcut.ts @@ -1,25 +1,26 @@ -// import { shortcut } from './Shortcuts.plugin'; -// import createSaveAsImgPrompt from '../../data/saveImage'; -//Assign the callback func for the keymap here import { - // createNewCircuitScopeCall, elementDirection, insertLabel, labelDirection, openHotkey, moveElement, openDocumentation, -} from './actions' -import save from '../../data/save' -import { saveOffline, openOffline } from '../../data/project' -import createSaveAsImgPrompt from '../../data/saveImage' -import { createSubCircuitPrompt } from '../../subcircuit' -import { createCombinationalAnalysisPrompt } from '../../combinationalAnalysis' -import { shortcut } from './shortcuts.plugin' -import logixFunction from '../../data' +} from './actions.js' +import save from '../../data/save.js' +import { saveOffline, openOffline } from '../../data/project.js' +import createSaveAsImgPrompt from '../../data/saveImage.js' +import { createSubCircuitPrompt } from '../../subcircuit.js' +import { createCombinationalAnalysisPrompt } from '../../combinationalAnalysis.js' +import { shortcut } from './shortcuts.plugin.js' +import logixFunction from '../../data.js' +import { ActionType, ShortcutOptions } from './model.types.js' -export const addShortcut = (keys, action) => { - let callback +// Add space after comment as per linter rule +// Assign the callback func for the keymap here + +export default function addShortcut(keys: string, action: ActionType): void { + let callback: () => void = () => console.error('No shortcut found..') + switch (action) { case 'New Circuit': callback = logixFunction.createNewCircuitScope // TODO: directly call rather than using dom click @@ -87,14 +88,14 @@ export const addShortcut = (keys, action) => { case 'Open Documentation': callback = openDocumentation break - default: - callback = () => console.error('No shortcut found..') - break } - shortcut.add(keys, callback, { + + const options: ShortcutOptions = { type: 'keydown', propagate: false, target: document, disable_in_input: true, - }) -} + } + + shortcut.add(keys, callback, options) +} \ No newline at end of file diff --git a/src/simulator/src/hotkey_binder/model/model.types.ts b/src/simulator/src/hotkey_binder/model/model.types.ts index 271cbf01..6c18d3d2 100644 --- a/src/simulator/src/hotkey_binder/model/model.types.ts +++ b/src/simulator/src/hotkey_binder/model/model.types.ts @@ -1,6 +1,30 @@ //This file holds the interfaces required for src/simulator/src/hotkey_binder/model //to be continued for storing interfaces of the parent folder +export type ActionType = + | 'New Circuit' + | 'Save Online' + | 'Save Offline' + | 'Download as Image' + | 'Open Offline' + | 'Insert Sub-circuit' + | 'Combinational Analysis' + | 'Direction Up' + | 'Direction Down' + | 'Direction Left' + | 'Direction Right' + | 'Insert Label' + | 'Label Direction Up' + | 'Label Direction Down' + | 'Label Direction Left' + | 'Label Direction Right' + | 'Move Element Up' + | 'Move Element Down' + | 'Move Element Left' + | 'Move Element Right' + | 'Hotkey Preference' + | 'Open Documentation' + export interface ShortcutOptions { type?: string propagate?: boolean diff --git a/src/simulator/src/hotkey_binder/model/normalize/normalizer.plugin.js b/src/simulator/src/hotkey_binder/model/normalize/normalizer.plugin.js deleted file mode 100644 index cc9a7def..00000000 --- a/src/simulator/src/hotkey_binder/model/normalize/normalizer.plugin.js +++ /dev/null @@ -1,389 +0,0 @@ -/** - * This plugin has been modified to support metakeys - */ - -/* - * Library to normalize key codes across browsers. This works with keydown - * events; keypress events are not fired for all keys, and the codes are - * different for them. It returns an object with the following fields: - * { int code, bool shift, bool alt, bool ctrl }. The normalized keycodes - * obey the following rules: - * - * For alphabetic characters, the ASCII code of the uppercase version - * - * For codes that are identical across all browsers (this includes all - * modifiers, esc, delete, arrows, etc.), the common keycode - * - * For numeric keypad keys, the value returned by numkey(). - * (Usually 96 + the number) - * - * For symbols, the ASCII code of the character that appears when shift - * is not held down, EXCEPT for '" => 222 (conflicts with right-arrow/pagedown), - * .> => 190 (conflicts with Delete) and `~ => 126 (conflicts with Num0). - * - * Basic usage: - * document.onkeydown = function(e) { - * do_something_with(KeyCode.translateEvent(e) - * }; - * - * The naming conventions for functions use 'code' to represent an integer - * keycode, 'key' to represent a key description (specified above), and 'e' - * to represent an event object. - * - * There's also functionality to track and detect which keys are currently - * being held down: install 'key_up' and 'key_down' on their respective event - * handlers, and then check with 'is_down'. - * - * @fileoverview - * @author Jonathan Tang - * @version 0.9 - * @license BSD - */ - -/* -Copyright (c) 2008 Jonathan Tang -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions -are met: -1. Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. -2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. -3. The name of the author may not be used to endorse or promote products - derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR -IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES -OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. -IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, -INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT -NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF -THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -var modifiers = ['ctrl', 'alt', 'shift', 'meta'], - KEY_MAP = {}, - shifted_symbols = { - 58: 59, // : -> ; - 43: 61, // = -> + - 60: 44, // < -> , - 95: 45, // _ -> - - 62: 46, // > -> . - 63: 47, // ? -> / - 96: 192, // ` -> ~ - 124: 92, // | -> \ - 39: 222, // ' -> 222 - 34: 222, // " -> 222 - 33: 49, // ! -> 1 - 64: 50, // @ -> 2 - 35: 51, // # -> 3 - 36: 52, // $ -> 4 - 37: 53, // % -> 5 - 94: 54, // ^ -> 6 - 38: 55, // & -> 7 - 42: 56, // * -> 8 - 40: 57, // ( -> 9 - 41: 58, // ) -> 0 - 123: 91, // { -> [ - 125: 93, // } -> ] - } - -function isLower(ascii) { - return ascii >= 97 && ascii <= 122 -} -function capitalize(str) { - return str.substr(0, 1).toUpperCase() + str.substr(1).toLowerCase() -} - -var is_gecko = navigator.userAgent.indexOf('Gecko') != -1, - is_ie = navigator.userAgent.indexOf('MSIE') != -1, - is_windows = navigator.platform.indexOf('Win') != -1, - is_opera = window.opera && window.opera.version() < 9.5, - is_konqueror = navigator.vendor && navigator.vendor.indexOf('KDE') != -1, - is_icab = navigator.vendor && navigator.vendor.indexOf('iCab') != -1 - -var GECKO_IE_KEYMAP = { - 186: 59, // ;: in IE - 187: 61, // =+ in IE - 188: 44, // ,< - 109: 95, // -_ in Mozilla - 107: 61, // =+ in Mozilla - 189: 95, // -_ in IE - 190: 62, // .> - 191: 47, // /? - 192: 126, // `~ - 219: 91, // {[ - 220: 92, // \| - 221: 93, // }] -} - -var OPERA_KEYMAP = {} - -// Browser detection taken from quirksmode.org -if (is_opera && is_windows) { - KEY_MAP = OPERA_KEYMAP -} else if (is_opera || is_konqueror || is_icab) { - var unshift = [ - 33, 64, 35, 36, 37, 94, 38, 42, 40, 41, 58, 43, 60, 95, 62, 63, 124, 34, - ] - KEY_MAP = OPERA_KEYMAP - for (var i = 0; i < unshift.length; ++i) { - KEY_MAP[unshift[i]] = shifted_symbols[unshift[i]] - } -} else { - // IE and Gecko are close enough that we can use the same map for both, - // and the rest of the world (eg. Opera 9.50) seems to be standardizing - // on them - KEY_MAP = GECKO_IE_KEYMAP -} - -if (is_konqueror) { - KEY_MAP[0] = 45 - KEY_MAP[127] = 46 - KEY_MAP[45] = 95 -} - -var key_names = { - 32: 'SPACE', - 13: 'ENTER', - 9: 'TAB', - 8: 'BACKSPACE', - 16: 'SHIFT', - 17: 'CTRL', - 18: 'ALT', - 20: 'CAPS_LOCK', - 144: 'NUM_LOCK', - 145: 'SCROLL_LOCK', - 37: 'LEFT', - 38: 'UP', - 39: 'RIGHT', - 40: 'DOWN', - 33: 'PAGE_UP', - 34: 'PAGE_DOWN', - 36: 'HOME', - 35: 'END', - 45: 'INSERT', - 46: 'DELETE', - 27: 'ESCAPE', - 19: 'PAUSE', - 222: "'", - 91: 'META', -} -function fn_name(code) { - if (code >= 112 && code <= 123) return 'F' + (code - 111) - return false -} -function num_name(code) { - if (code >= 96 && code < 106) return 'Num' + (code - 96) - switch (code) { - case 106: - return 'Num*' - case 111: - return 'Num/' - case 110: - return 'Num.' - default: - return false - } -} - -var current_keys = { - codes: {}, - ctrl: false, - alt: false, - shift: false, - meta: false, -} - -function update_current_modifiers(key) { - current_keys.ctrl = key.ctrl - current_keys.alt = key.alt - current_keys.shift = key.shift - current_keys.meta = key.meta -} - -function same_modifiers(key1, key2) { - return ( - key1.ctrl === key2.ctrl && - key1.alt === key2.alt && - key1.shift === key2.shift && - key1.meta === key2.meta - ) -} - -if (typeof window.KeyCode != 'undefined') { - var _KeyCode = window.KeyCode -} - -export const KeyCode = { - no_conflict: function () { - window.KeyCode = _KeyCode - return KeyCode - }, - - /** Generates a function key code from a number between 1 and 12 */ - fkey: function (num) { - return 111 + num - }, - - /** - * Generates a numeric keypad code from a number between 0 and 9. - * Also works for (some) arithmetic operators. The mappings are: - * - * *: 106, /: 111, .: 110 - * - * + and - are not supported because the keycodes generated by Mozilla - * conflict with the non-keypad codes. The same applies to all the - * arithmetic keypad keys on Konqueror and early Opera. - */ - numkey: function (num) { - switch (num) { - case '*': - return 106 - case '/': - return 111 - case '.': - return 110 - default: - return 96 + num - } - }, - - /** - * Generates a key code from the ASCII code of (the first character of) a - * string. - */ - key: function (str) { - var c = str.charCodeAt(0) - if (isLower(c)) return c - 32 - return shifted_symbols[c] || c - }, - - /** Checks if two key objects are equal. */ - key_equals: function (key1, key2) { - return key1.code == key2.code && same_modifiers(key1, key2) - }, - - /** Translates a keycode to its normalized value. */ - translate_key_code: function (code) { - return KEY_MAP[code] || code - }, - - /** - * Translates a keyDown event to a normalized key event object. The - * object has the following fields: - * { int code; boolean shift, boolean alt, boolean ctrl } - */ - translate_event: function (e) { - e = e || window.event - var code = e.which || e.keyCode - return { - code: KeyCode.translate_key_code(code), - shift: e.shiftKey, - alt: e.altKey, - ctrl: e.ctrlKey, - meta: e.metaKey, - } - }, - - /** - * Keydown event listener to update internal state of which keys are - * currently pressed. - */ - - key_down: function (e) { - var key = KeyCode.translate_event(e) - current_keys.codes[key.code] = key.code - update_current_modifiers(key) - }, - - /** - * Keyup event listener to update internal state. - */ - key_up: function (e) { - var key = KeyCode.translate_event(e) - delete current_keys.codes[key.code] - update_current_modifiers(key) - }, - - /** - * Returns true if the key spec (as returned by translate_event) is - * currently held down. - */ - is_down: function (key) { - var code = key.code - if (code == KeyCode.CTRL) return current_keys.ctrl - if (code == KeyCode.ALT) return current_keys.alt - if (code == KeyCode.SHIFT) return current_keys.shift - - return ( - current_keys.codes[code] !== undefined && - same_modifiers(key, current_keys) - ) - }, - - /** - * Returns a string representation of a key event suitable for the - * shortcut.js or JQuery HotKeys plugins. Also makes a decent UI display. - */ - hot_key: function (key) { - var pieces = [] - for (var i = 0; i < modifiers.length; ++i) { - var modifier = modifiers[i] - if ( - key[modifier] && - modifier.toUpperCase() != key_names[key.code] - ) { - pieces.push(capitalize(modifier)) - } - } - - var c = key.code - var key_name = - key_names[c] || fn_name(c) || num_name(c) || String.fromCharCode(c) - pieces.push(capitalize(key_name)) - return pieces.join('+') - }, -} - -// Add key constants -for (var code in key_names) { - KeyCode[key_names[code]] = code -} - -// var fields = ['charCode', 'keyCode', 'which', 'type', 'timeStamp', -// 'altKey', 'ctrlKey', 'shiftKey', 'metaKey']; -// var types = ['keydown', 'keypress', 'keyup']; - -// function show_event(type) { -// return function(e) { -// var lines = []; -// for(var i = 0; i < fields.length; ++i) { -// lines.push('' + fields[i] + ': ' + e[fields[i]] + '
'); -// } -// document.getElementByI(type).innerHTML = lines.join('\n'); -// return false; -// } -// }; - -// function show_is_key_down(id, code, ctrl, alt, shift) { -// document.getElementById(id + '_down').innerHTML = KeyCode.is_down({ -// code: code, -// ctrl: ctrl, -// alt: alt, -// shift: shift -// }); -// }; - -// function update_key_downs() { -// show_is_key_down('x', KeyCode.key('x'), false, false, false); -// show_is_key_down('shift_x', KeyCode.key('x'), false, false, true); -// show_is_key_down('shift_c', KeyCode.key('c'), false, false, true); -// show_is_key_down('ctrl_a', KeyCode.key('a'), true, false, false); -// }; diff --git a/src/simulator/src/hotkey_binder/model/normalize/normalizer.plugin.ts b/src/simulator/src/hotkey_binder/model/normalize/normalizer.plugin.ts new file mode 100644 index 00000000..3cfa5d36 --- /dev/null +++ b/src/simulator/src/hotkey_binder/model/normalize/normalizer.plugin.ts @@ -0,0 +1,264 @@ +interface KeyCodeType { + no_conflict: () => KeyCodeType; + fkey: (num: number) => number; + numkey: (num: number | string) => number; + key: (str: string) => number; + key_equals: (key1: KeyEvent, key2: KeyEvent) => boolean; + translate_key_code: (code: number) => number; + translate_event: (e: KeyboardEvent) => KeyEvent; + key_down: (e: KeyboardEvent) => void; + key_up: (e: KeyboardEvent) => void; + is_down: (key: KeyEvent) => boolean; + hot_key: (key: KeyEvent) => string; + [key: string]: any; // For dynamic key constants +} + +interface KeyEvent { + code: number; + shift: boolean; + alt: boolean; + ctrl: boolean; + meta: boolean; +} + +interface CurrentKeys { + codes: { [key: number]: number }; + ctrl: boolean; + alt: boolean; + shift: boolean; + meta: boolean; +} + +const modifiers: string[] = ['ctrl', 'alt', 'shift', 'meta']; +const KEY_MAP: { [key: number]: number } = {}; + +const shifted_symbols: { [key: number]: number } = { + 58: 59, // : -> ; + 43: 61, // = -> + + 60: 44, // < -> , + 95: 45, // _ -> - + 62: 46, // > -> . + 63: 47, // ? -> / + 96: 192, // ` -> ~ + 124: 92, // | -> \ + 39: 222, // ' -> 222 + 34: 222, // " -> 222 + 33: 49, // ! -> 1 + 64: 50, // @ -> 2 + 35: 51, // # -> 3 + 36: 52, // $ -> 4 + 37: 53, // % -> 5 + 94: 54, // ^ -> 6 + 38: 55, // & -> 7 + 42: 56, // * -> 8 + 40: 57, // ( -> 9 + 41: 58, // ) -> 0 + 123: 91, // { -> [ + 125: 93, // } -> ] +}; + +function isLower(ascii: number): boolean { + return ascii >= 97 && ascii <= 122; +} + +function capitalize(str: string): string { + return str.substr(0, 1).toUpperCase() + str.substr(1).toLowerCase(); +} + +const is_gecko = navigator.userAgent.indexOf('Gecko') != -1; +const is_ie = navigator.userAgent.indexOf('MSIE') != -1; +const is_windows = navigator.platform.indexOf('Win') != -1; +const is_opera = window.opera && (window.opera as any).version() < 9.5; +const is_konqueror = navigator.vendor && navigator.vendor.indexOf('KDE') != -1; +const is_icab = navigator.vendor && navigator.vendor.indexOf('iCab') != -1; + +const GECKO_IE_KEYMAP: { [key: number]: number } = { + 186: 59, // ;: in IE + 187: 61, // =+ in IE + 188: 44, // ,< + 109: 95, // -_ in Mozilla + 107: 61, // =+ in Mozilla + 189: 95, // -_ in IE + 190: 62, // .> + 191: 47, // /? + 192: 126, // `~ + 219: 91, // {[ + 220: 92, // \| + 221: 93, // }] +}; + +const OPERA_KEYMAP: { [key: number]: number } = {}; + +// Browser detection and keymap setup +if (is_opera && is_windows) { + Object.assign(KEY_MAP, OPERA_KEYMAP); +} else if (is_opera || is_konqueror || is_icab) { + const unshift = [33, 64, 35, 36, 37, 94, 38, 42, 40, 41, 58, 43, 60, 95, 62, 63, 124, 34]; + Object.assign(KEY_MAP, OPERA_KEYMAP); + for (const code of unshift) { + KEY_MAP[code] = shifted_symbols[code]; + } +} else { + Object.assign(KEY_MAP, GECKO_IE_KEYMAP); +} + +if (is_konqueror) { + KEY_MAP[0] = 45; + KEY_MAP[127] = 46; + KEY_MAP[45] = 95; +} + +const key_names: { [key: number]: string } = { + 32: 'SPACE', + 13: 'ENTER', + 9: 'TAB', + 8: 'BACKSPACE', + 16: 'SHIFT', + 17: 'CTRL', + 18: 'ALT', + 20: 'CAPS_LOCK', + 144: 'NUM_LOCK', + 145: 'SCROLL_LOCK', + 37: 'LEFT', + 38: 'UP', + 39: 'RIGHT', + 40: 'DOWN', + 33: 'PAGE_UP', + 34: 'PAGE_DOWN', + 36: 'HOME', + 35: 'END', + 45: 'INSERT', + 46: 'DELETE', + 27: 'ESCAPE', + 19: 'PAUSE', + 222: "'", + 91: 'META', +}; + +function fn_name(code: number): string | false { + if (code >= 112 && code <= 123) return 'F' + (code - 111); + return false; +} + +function num_name(code: number): string | false { + if (code >= 96 && code < 106) return 'Num' + (code - 96); + switch (code) { + case 106: return 'Num*'; + case 111: return 'Num/'; + case 110: return 'Num.'; + default: return false; + } +} + +const current_keys: CurrentKeys = { + codes: {}, + ctrl: false, + alt: false, + shift: false, + meta: false, +}; + +function update_current_modifiers(key: KeyEvent): void { + current_keys.ctrl = key.ctrl; + current_keys.alt = key.alt; + current_keys.shift = key.shift; + current_keys.meta = key.meta; +} + +function same_modifiers(key1: KeyEvent, key2: KeyEvent): boolean { + return ( + key1.ctrl === key2.ctrl && + key1.alt === key2.alt && + key1.shift === key2.shift && + key1.meta === key2.meta + ); +} + +const _KeyCode = (typeof window !== 'undefined' && (window as any).KeyCode) || undefined; + +export const KeyCode: KeyCodeType = { + no_conflict: function(): KeyCodeType { + if (typeof window !== 'undefined') { + (window as any).KeyCode = _KeyCode; + } + return KeyCode; + }, + + fkey: function(num: number): number { + return 111 + num; + }, + + numkey: function(num: number | string): number { + switch (num) { + case '*': return 106; + case '/': return 111; + case '.': return 110; + default: return 96 + Number(num); + } + }, + + key: function(str: string): number { + const c = str.charCodeAt(0); + if (isLower(c)) return c - 32; + return shifted_symbols[c] || c; + }, + + key_equals: function(key1: KeyEvent, key2: KeyEvent): boolean { + return key1.code == key2.code && same_modifiers(key1, key2); + }, + + translate_key_code: function(code: number): number { + return KEY_MAP[code] || code; + }, + + translate_event: function(e: KeyboardEvent): KeyEvent { + const code = e.which || e.keyCode; + return { + code: KeyCode.translate_key_code(code), + shift: e.shiftKey, + alt: e.altKey, + ctrl: e.ctrlKey, + meta: e.metaKey, + }; + }, + + key_down: function(e: KeyboardEvent): void { + const key = KeyCode.translate_event(e); + current_keys.codes[key.code] = key.code; + update_current_modifiers(key); + }, + + key_up: function(e: KeyboardEvent): void { + const key = KeyCode.translate_event(e); + delete current_keys.codes[key.code]; + update_current_modifiers(key); + }, + + is_down: function(key: KeyEvent): boolean { + const code = key.code; + if (code == KeyCode.CTRL) return current_keys.ctrl; + if (code == KeyCode.ALT) return current_keys.alt; + if (code == KeyCode.SHIFT) return current_keys.shift; + + return current_keys.codes[code] !== undefined && same_modifiers(key, current_keys); + }, + + hot_key: function(key: KeyEvent): string { + const pieces: string[] = []; + for (const modifier of modifiers) { + if (key[modifier as keyof KeyEvent] && modifier.toUpperCase() != key_names[key.code]) { + pieces.push(capitalize(modifier)); + } + } + + const c = key.code; + const key_name = key_names[c] || fn_name(c) || num_name(c) || String.fromCharCode(c); + pieces.push(capitalize(key_name)); + return pieces.join('+'); + }, +}; + +// Add key constants +for (const code in key_names) { + KeyCode[key_names[code]] = Number(code); +} \ No newline at end of file