diff --git a/README.md b/README.md
index 27d0ef5..ea22609 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,14 @@
# ROM-Info
-Display ROM info of select ROMs (e.g. NDS, GB/C/A, 3DS)
\ No newline at end of file
+Display ROM info of select ROMs (e.g. NDS, GB/C/A, 3DS)
+
+
+# Install (Windows - PowerShell)
+1. python -m venv .venv
+2. .\\.venv\Scripts\Activate.ps1
+3. pip install -r requirements.txt
+
+
+
+
+Licensed under [CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/)
\ No newline at end of file
diff --git a/logger.py b/logger.py
new file mode 100644
index 0000000..f7a6a4a
--- /dev/null
+++ b/logger.py
@@ -0,0 +1,32 @@
+# -*- coding: utf-8 -*-
+
+from datetime import datetime
+
+import colorama
+
+
+class Logger(object):
+ def __init__(self, module: str, datefmt: str = '%m/%d/%Y %I:%M:%S %p'):
+ colorama.init()
+ self.module = module
+ self.datefmt = datefmt
+
+ def info(self, msg: str, levelname: str = 'INFO'):
+ asctime = datetime.now().strftime(self.datefmt)
+
+ print(f'\033[92m[{asctime}] - {levelname} - {self.module} - {msg}\033[39m')
+
+ def debug(self, msg: str, levelname: str = 'DEBUG'):
+ asctime = datetime.now().strftime(self.datefmt)
+
+ print(f'\033[93m[{asctime}] - {levelname} - {self.module} - {msg}\033[39m')
+
+ def verbose(self, msg: str, levelname: str = 'VERBOSE'):
+ asctime = datetime.now().strftime(self.datefmt)
+
+ print(f'\033[96m[{asctime}] - {levelname} - {self.module} - {msg}\033[39m')
+
+ def error(self, msg: str, levelname: str = 'ERROR'):
+ asctime = datetime.now().strftime(self.datefmt)
+
+ print(f'\033[91m[{asctime}] - {levelname} - {self.module} - {msg}\033[39m')
diff --git a/main.py b/main.py
new file mode 100644
index 0000000..ced411e
--- /dev/null
+++ b/main.py
@@ -0,0 +1,20 @@
+import sys
+
+from logger import Logger
+from rominfo.nintendo_ds import *
+
+logger = Logger('RomInfo')
+
+if __name__ == '__main__':
+ if len(sys.argv) == 2:
+ # NDS(i) - https://dsibrew.org/wiki/DSi_cartridge_header
+ if sys.argv[1].endswith('.nds'):
+ with open(sys.argv[1], mode='rb') as nds:
+ logger.info('Title : {}'.format(nds_get_info('title', nds)))
+ logger.info('Game code : {}'.format(nds_get_info('gamecode', nds)))
+ logger.info('Maker code : {}'.format(nds_get_info('makercode', nds)))
+ logger.info('Unit code : {}'.format(nds_get_info('unitcode', nds)))
+ logger.info('Encryption seed : {}'.format(nds_get_info('encryptionseed', nds)))
+ logger.info('Device capacity : {}'.format(nds_get_info('devicecapacity', nds)))
+ else:
+ logger.error('No ROM specified. App requires one argument.')
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..e9405b3
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1 @@
+colorama==0.4.6
diff --git a/rominfo/nintendo_ds.py b/rominfo/nintendo_ds.py
new file mode 100644
index 0000000..d225bcc
--- /dev/null
+++ b/rominfo/nintendo_ds.py
@@ -0,0 +1,55 @@
+from math import pow
+from typing import BinaryIO
+
+
+def nds_get_info(opt: str, inf: BinaryIO):
+ match opt.lower():
+ case 'title':
+ inf.seek(0x000)
+ return inf.read(12).decode()
+ case 'gamecode':
+ inf.seek(0x00C)
+ return inf.read(4).decode()
+ case 'makercode':
+ inf.seek(0x010)
+ return _maker_lookup(inf.read(2).decode())
+ case 'unitcode':
+ inf.seek(0x012)
+ return _unit_lookup(inf.read(1))
+ case 'encryptionseed':
+ inf.seek(0x013)
+ return str(int.from_bytes(inf.read(1), 'little'))
+ case 'devicecapacity':
+ inf.seek(0x014)
+ return _capacity_lookup(int.from_bytes(inf.read(1), 'little'))
+ case _:
+ return 'Unknown'
+
+
+def _maker_lookup(code: str) -> str:
+ match code:
+ case '01':
+ return 'Nintendo ({})'.format(code)
+ case 'GD':
+ return 'Square-Enix ({})'.format(code)
+ case _:
+ return 'Unknown ({})'.format(code)
+
+
+def _unit_lookup(code: bytes) -> str:
+ if code == b'\x00':
+ return 'NDS (00h)'
+ elif code == b'\x02':
+ return 'DSi Enhanced (02h)'
+ elif code == b'\x03':
+ return 'DSi Exclusive (03h)'
+ else:
+ return 'Unknown ({})'.format(code)
+
+
+def _capacity_lookup(code: int) -> str: # 512Mbit (67108864 bytes) (09h)
+ match code:
+ case 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12:
+ return '{} MB ({})'.format(round(128 * pow(2, code) / 1024), '{}h'.format(str(code).zfill(2)))
+ case _:
+ return 'Unknown ({})'.format(code)