├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── change_request.md │ ├── feature_request.md │ └── question.md ├── pull_request_template.md └── workflows │ └── test-catnap.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── catnap.nim.cfg ├── config.nims ├── config ├── config.toml └── distros.toml ├── docs ├── catnap.1 └── catnap.5 ├── image ├── demo1.png ├── demo2.png ├── demo3.png ├── distros.png ├── logo │ ├── catnap.psd │ └── catnap.svg ├── margin.png └── no_margin.png ├── scripts ├── basic.flf ├── git-commit-id.sh ├── test-commandline-args.sh └── test_config.toml └── src ├── catnap.nim ├── catnaplib ├── drawing │ └── render.nim ├── generation │ ├── stats.nim │ └── utils.nim ├── global │ ├── config.nim │ └── definitions.nim ├── platform │ ├── caching.nim │ ├── fetch.nim │ └── probe.nim └── terminal │ ├── colors.nim │ └── logging.nim └── extern ├── headers └── getDisk.h └── libraries └── parsetoml.nim /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[Bug]: " 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### What version of Catnap are you running? 11 | Paste the output of `catnap -v` here. 12 | 13 | ### Describe the bug: 14 | A clear and concise description of what the bug is. 15 | 16 | ### Screenshots: 17 | If applicable, add screenshots to help explain your problem. 18 | 19 | ### Device: 20 | - OS: [e.g. Linux] 21 | - WM: [e.g. KDE Plasma 6] 22 | - TERMINAL: [e.g. Kitty] 23 | 24 | ### Additional context: 25 | Add any other context about the problem here. 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/change_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Change request 3 | about: Suggest an idea for this project 4 | title: "[Change]:" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Is your change request related to a problem? Please describe: 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | ### Describe what you want to change: 14 | A clear and concise description of what you want changed. 15 | 16 | ### Additional context: 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[Feature]:" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Is your feature request related to a problem? Please describe: 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | ### Describe the solution you'd like: 14 | A clear and concise description of what you want to happen. 15 | 16 | ### Additional context: 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Ask a question about the project 4 | title: "[Question]:" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Ask your Question: 11 | Ask a question about the project here. 12 | 13 | ### Additional context: 14 | Add any other context or screenshots about the question here. 15 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### Is your pull request linked to an existing issue? 2 | If so, put it here. 3 | 4 | ### What is your pull request about?: 5 | A clear and concise description of what you are adding or fixing. 6 | 7 | ### Any other disclosures/notices/things to add? 8 | Add any other context/images about the pull request here. 9 | -------------------------------------------------------------------------------- /.github/workflows/test-catnap.yml: -------------------------------------------------------------------------------- 1 | name: test-catnap 2 | on: 3 | push: 4 | pull_request: 5 | jobs: 6 | test-catnap: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: 'Checkout Repository' 10 | uses: actions/checkout@v4 11 | - name: 'Install Nim' 12 | uses: iffy/install-nim@v5 13 | - name: 'Setup Nim' 14 | uses: jiro4989/setup-nim-action@v1 15 | with: 16 | nim-version: 'stable' 17 | repo-token: ${{ secrets.GITHUB_TOKEN }} 18 | - name: 'Build Catnap' 19 | run: nim debug 20 | - name: 'Install Figlet' 21 | run: sudo apt-get install figlet 22 | - name: 'Run Tests' 23 | run: cd scripts && ./test-commandline-args.sh 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /bin 2 | catnap 3 | *.exe 4 | *.gz 5 | src/catnaplib/global/currentcommit.nim 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | Create a new [issue](https://github.com/iinsertNameHere/catnap/issues) using the correct issue template or introduce a new feature/fix a bug and submit a pull request. 3 | 4 | # Project structure 5 | ```graphql 6 | . 7 | ├── config/ 8 | │ ├── config.toml # Main config 9 | │ └── distros.toml # Distro art config 10 | ├── docs/ # Contains man docs 11 | ├── image/ # Contains all images for README.md 12 | ├── src/ 13 | │ ├── catnaplib/ 14 | │ │ ├── drawing/ # Files for rendering output 15 | │ │ ├── generation/ # Files for generating output objects 16 | │ │ ├── global/ # Files used globally 17 | │ │ ├── platform/ # Files related to fetching system info 18 | │ │ └── terminal/ # Files related to terminal stuff (Colors, Logging) 19 | │ ├── extern/ 20 | │ │ ├── headers/ # Contains extern c++ headers (hpp) 21 | │ │ └── libraries/ # Contains extern libs 22 | │ └── catnap.nim # Entry src file 23 | ├── scripts/ # Test Scripts etc. 24 | └── config.nims # nim install, nim debug , ... 25 | ``` 26 | 27 | # How to add a new distro 28 | 29 | 1. Add the distro's logo in the default `distros.toml` in the `config/` folder. Please arrange the distro in alphabetical order. 30 | 2. In `src/catnaplib/global/definitions.nim`, go to the `PKGMANAGERS` section. 31 | 3. According to the name of the distro in `config/distros.toml`, put a new line like this: 32 | ```nim 33 | "name of distro": "name of package manager", 34 | ``` 35 | 4. If your distro's package manager is already in the `PKGCOUNTCOMMANDS` section, skip the next step. 36 | 5. Put your distro's package manager in the `PKGCOUNTCOMMANDS` section like this: 37 | ```nim 38 | "name of package manager": "command to get number of packages", 39 | ``` 40 | 6. Submit a pull request to the Catnap repo. 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 iinsertNameHere 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 | 7 |

Catnap🌿 Systemfetch

8 |

9 | View Demo 10 | · 11 | Documentation 12 | · 13 | Contributing 14 |

15 |

16 | 17 | 18 |

19 |
20 |
21 | 22 | ## 🌿 What is Catnap 23 | I created `Catnap🌿` (originally known as `Catnip`) as a playful, simple system-information **concatenation** tool using `nim👑`. It is quite **customizable** and has possibilities to alter the names and colors of the statistics. In the future, I also intend to add more distribution logos. Feel free to contribute to the project at any time. 24 | 25 | > #### ⏱️ Execution Time 26 | > *Around **0.006** seconds on my laptop 27 | 28 | #### 📊 Displayed Statistics 29 |
30 | View Statistics 31 | 48 |
49 | 50 | #### ❤️ Shoutout to: 51 | - [NimParsers/parsetoml](https://github.com/NimParsers/parsetoml) for the toml parsing 52 | - [ssleert/Nitch](https://github.com/ssleert/nitch) for the inspiration 53 | - [All contributors](#thanks-to-all-contributors-) 54 | 55 | **Feel free to submit [issues](https://github.com/iinsertNameHere/catnap/issues) if you have any improvement ideas! ❤** 56 | 57 |
58 | 59 | ## 📷 Demo Images 60 |
61 | Demo Images ✨ 62 | 63 | Default: 64 |
65 | 66 |
67 | FigletLogos (
Nitch mode): 68 |
69 | 70 |
71 | Image Mode: 72 |
73 | 74 |
75 | Distro Showcase: 76 |
77 | 78 | 79 |
80 |
81 | 82 |
83 | 84 | ## 📒 Documentation 85 | You can read the full documentation [here](https://catnap-fetch.xyz/) 86 |
87 | It covers installation, usage and configuration! 88 | 89 | > If you think something is missing, feel free to open a Issue in the [catnap-docs](https://github.com/iinsertNameHere/catnap-docs) repository. 90 | 91 |
92 | 93 | ## 🧪 Testing 94 | To test catnap, run the following command in the catnap repo: 95 | ```bash 96 | $ cd tests/ 97 | $ ./commandline_args.sh 98 | ``` 99 | 100 |
101 | 102 | ## 🐛 Bugs 103 | If you've found a bug, please follow the steps below: 104 | 1. Make sure you're on the latest commit, as bugs that are in your current version might've been fixed in later commits. 105 | 2. Run `catnap -v` and copy the output into your clipboard. 106 | 3. Create an issue and remember to click on the bug report template! 107 | 4. Fill in the details and submit the report. 108 | 109 |
110 | 111 | ## 🔨 Contributing 112 | For more info on how to contribute and how to add a new distro, see [CONTRIBUTING.md](CONTRIBUTING.md). 113 | 114 |
115 | 116 | ## ❤️ Thanks to all contributors! 117 | 118 |

119 | 120 | 121 | 122 |

123 | 124 |
125 |
126 | 127 | 128 |

129 | 130 |

131 | -------------------------------------------------------------------------------- /catnap.nim.cfg: -------------------------------------------------------------------------------- 1 | --path=./src/extern/libraries -------------------------------------------------------------------------------- /config.nims: -------------------------------------------------------------------------------- 1 | import strformat 2 | import strutils 3 | import os 4 | 5 | proc compile(release: bool) = 6 | var args: seq[string] 7 | args.add(&"--cincludes:{thisDir()}/src/extern/headers") 8 | args.add(&"--path:{thisDir()}/src/extern/libraries") 9 | args.add(&"--passC:-f") 10 | args.add(&"--mm:arc") 11 | args.add(&"--threads:on") 12 | args.add(&"--panics:on") 13 | if release: 14 | args.add(&"--checks:off") 15 | args.add(&"--verbosity:0") 16 | args.add(&"-d:danger") 17 | args.add(&"--opt:speed") 18 | args.add(&"-d:strip") 19 | args.add(&"--outdir:{thisDir()}/bin") 20 | args.add(&"{thisDir()}/src/catnap.nim") 21 | 22 | exec("nim c " & args.join(" ")) 23 | 24 | proc configure() = 25 | var configpath = "" 26 | 27 | # Use XDG_CONFIG_HOME only if it is defined. Else use ~/.confg 28 | let XDG_CONFIG_HOME = getEnv("XDG_CONFIG_HOME") 29 | if XDG_CONFIG_HOME == "": 30 | configpath = getEnv("HOME") & "/.config/catnap/" 31 | else: 32 | configpath = XDG_CONFIG_HOME & "/catnap/" 33 | 34 | echo "Creating " & configpath 35 | if dirExists(configpath): 36 | echo "Configuration directory already exists, skipping..." 37 | else: 38 | mkdir(configpath) 39 | 40 | echo "Creating " & configpath & "config.toml" 41 | if fileExists(configpath & "config.toml"): 42 | echo "Configuration file already exists, skipping..." 43 | else: 44 | cpFile(thisDir() & "/config/config.toml", configpath & "config.toml") 45 | 46 | echo "Creating " & configpath & "distros.toml" 47 | if fileExists(configpath & "distros.toml"): 48 | echo "Distro art file already exists, skipping..." 49 | else: 50 | cpFile(thisDir() & "/config/distros.toml", configpath & "distros.toml") 51 | 52 | task clean, "Cleans existing build": 53 | echo "\e[36;1mCleaning\e[0;0m existing build" 54 | rmFile("{thisDir()}/bin/catnap") 55 | 56 | task release, "Builds the project in release mode": 57 | cleanTask() 58 | echo "\e[36;1mBuilding\e[0;0m in release mode" 59 | exec &"./scripts/git-commit-id.sh" 60 | compile(true) 61 | 62 | task debug, "Builds the project in debug mode": 63 | cleanTask() 64 | echo "\e[36;1mBuilding\e[0;0m in debug mode" 65 | exec &"./scripts/git-commit-id.sh" 66 | compile(false) 67 | 68 | task install_cfg, "Installs the config files": 69 | echo "\e[36;1mInstalling\e[0;0m config files" 70 | configure() 71 | 72 | task install_bin, "Installs the bin file and man page:": 73 | echo "\e[36;1mInstalling\e[0;0m bin file" 74 | echo &"Copying {thisDir()}/bin/catnap to /usr/local/bin" 75 | if defined(linux): 76 | exec &"sudo install -Dm755 {thisDir()}/bin/catnap /usr/local/bin/catnap" 77 | else: 78 | exec &"sudo mkdir -p /usr/local/bin && sudo install -m755 {thisDir()}/bin/catnap /usr/local/bin/catnap" 79 | 80 | let 81 | man_1_path = "/usr/share/man/man1/catnap.1.gz" 82 | local_1_path = &"{thisDir()}/docs/catnap.1" 83 | 84 | man_5_path = "/usr/share/man/man5/catnap.5.gz" 85 | local_5_path = &"{thisDir()}/docs/catnap.5" 86 | 87 | # Install man page only if it 88 | echo &"\e[36;1mInstalling\e[0;0m man page" 89 | # Create .gz file 90 | exec &"gzip -kf {local_1_path}" 91 | exec &"gzip -kf {local_5_path}" 92 | 93 | # If man page dose not exist or there is a new version, install the new man page 94 | if not fileExists(man_1_path) or readFile(local_1_path & ".gz") != readFile(man_1_path): 95 | echo &"Copying {local_1_path} to /usr/share/man/man1" 96 | if defined(linux): 97 | exec &"sudo install -Dm755 {local_1_path}.gz /usr/share/man/man1/catnap.1.gz" 98 | else: 99 | exec &"sudo mkdir -p /usr/local/share/man/man1 && sudo install -m755 {local_1_path}.gz /usr/local/share/man/man1/catnap.1.gz" 100 | else: 101 | echo &"Copying {local_1_path} to /usr/share/man/man1 - SKIPPED" 102 | 103 | if not fileExists(man_5_path) or readFile(local_5_path & ".gz") != readFile(man_5_path): 104 | echo &"Copying {local_5_path} to /usr/share/man/man5" 105 | if defined(linux): 106 | exec &"sudo install -Dm755 {local_5_path}.gz /usr/share/man/man5/catnap.5.gz" 107 | else: 108 | exec &"sudo mkdir -p /usr/local/share/man/man5 && sudo install -m755 {local_5_path}.gz /usr/local/share/man/man5/catnap.5.gz" 109 | else: 110 | echo &"Copying {local_5_path} to /usr/share/man/man5 - SKIPPED" 111 | 112 | task uninstall, "Uninstalls the bin file and man page:": 113 | echo "\e[36;1mUninstalling\e[0;0m bin file" 114 | exec &"sudo rm /usr/local/bin/catnap" 115 | echo "\e[36;1mUninstalling\e[0;0m man page" 116 | exec &"sudo rm /usr/share/man/man1/catnap.1.gz" 117 | exec &"sudo rm /usr/share/man/man5/catnap.5.gz" 118 | 119 | task install, "'release', 'install_linux' and 'install_cfg'": 120 | releaseTask() 121 | install_cfgTask() 122 | install_binTask() 123 | 124 | task setup, "'release' and 'install_cfg'": 125 | releaseTask() 126 | install_cfgTask() 127 | -------------------------------------------------------------------------------- /config/config.toml: -------------------------------------------------------------------------------- 1 | # This is the configuration file for catnap. 2 | # Here you can configure the stat colors, 3 | # stat names, stat icons and more! 4 | # 5 | # https://github.com/iinsertNameHere/catnap 6 | 7 | ############################################## 8 | ## FetchInfo stats Config ## 9 | ############################################## 10 | [stats] 11 | username = {icon = " ", name = "user", color = "(RD)"} 12 | hostname = {icon = " ", name = "hostname", color = "(YW)"} 13 | uptime = {icon = " ", name = "uptime", color = "(BE)"} 14 | distro = {icon = " ", name = "distro", color = "(GN)"} 15 | kernel = {icon = " ", name = "kernel", color = "(MA)"} 16 | desktop = {icon = " ", name = "desktop", color = "(CN)"} 17 | terminal = {icon = " ", name = "term", color = "(RD)"} 18 | shell = {icon = " ", name = "shell", color = "(MA)"} 19 | # packages = {icon = " ", name = "packages", color = "(RD)"} # WARNING: Resource Intensive 20 | # weather = {icon = " ", name = "weather", color = "(BE)"} # Requires curl and an emoji font. | WARNING: Resource Intensive 21 | # gpu = {icon = "󱔐 ", name = "gpu", color = "(MA)"} # WARNING: Resource Intensive 22 | # cpu = {icon = " ", name = "cpu", color = "(RD)"} 23 | # battery = {icon = " ", name = "battery", color = "(GN)"} 24 | # memory = {icon = " ", name = "memory", color = "(YW)"} 25 | # The following stats do not work on MacOS and the BSDs. 26 | # disk_0 = {icon = " ", name = "disk", color = "(GN)"} 27 | sep_color = "SEPARATOR" 28 | colors = {icon = " ", name = "colors", color = "!DT!", symbol = ""} 29 | 30 | ############################################## 31 | ## Misc Config ## 32 | ############################################## 33 | [misc] 34 | borderstyle = "line" 35 | layout = "Inline" # [Inline, ArtOnTop, StatsOnTop] 36 | stats_margin_top = 0 37 | location = "" # Used for fetching weather; leave empty to use the location of your IP; check https://wttr.in/:help for supported location types 38 | 39 | [misc.figletLogos] # Requires Figlet. 40 | enable = false 41 | color = "(YW)" 42 | font = "slant" 43 | margin = [0, 0, 0,] 44 | 45 | [misc.imageMode] # Requires Viu. 46 | enable = false 47 | path = "/path/to/some/image.png" # Please only use absolute paths. 48 | scale = 18 49 | margin = [2, 2, 3,] 50 | 51 | -------------------------------------------------------------------------------- /config/distros.toml: -------------------------------------------------------------------------------- 1 | # This is the distro logos file for catnap. 2 | # Here you can configure all distro logos 3 | # or add a new one. 4 | # 5 | # https://github.com/iinsertNameHere/catnap 6 | 7 | ############################################## 8 | ## Distro ASCII-Art Config ## 9 | ############################################## 10 | 11 | ### A ### 12 | [alpine] 13 | margin = [3, 2, 2,] 14 | art = [ 15 | "{BE} /\\ /\\ ", 16 | "{BE} // \\ \\ ", 17 | "{BE} // \\ \\ ", 18 | "{BE} /// \\ \\ ", 19 | "{BE}/// \\ \\", 20 | ] 21 | 22 | [amogos] 23 | margin = [1, 2, 3] 24 | art = [ 25 | "{RD} -///:. ", 26 | "{RD} smhhhhmh\\`", 27 | "{RD} :NA{BE}mogO{RD}SNs ", 28 | "{RD} hNNmmmmNNN ", 29 | "{RD} NNNNNNNNNN ", 30 | "{RD} :NNNNNNNNNN ", 31 | "{RD} mNNssussNNN ", 32 | "{RD} sNn: sNNo ", 33 | "{RD}+ooo+ sNNo ", 34 | "{RD} +oooo\\`", 35 | ] 36 | 37 | [android] 38 | margin = [2, 2, 2,] 39 | art = [ 40 | "{GN} ;, ,; ", 41 | "{GN} ';,.-----.,;' ", 42 | "{GN} ,' ', ", 43 | "{GN} / O O \\ ", 44 | "{GN}| |", 45 | "{GN}'-----------------'", 46 | ] 47 | 48 | [arch] 49 | margin = [2, 2, 3,] 50 | art = [ 51 | "{BE} /\\ ", 52 | "{BE} / \\ ", 53 | "{BE} /\\ \\ ", 54 | "{BE} / \\ ", 55 | "{BE} / ,, \\ ", 56 | "{BE} / | | -\\ ", 57 | "{BE}/_-'' ''-_\\", 58 | ] 59 | 60 | [arch_old] 61 | margin = [1, 2, 3,] 62 | art = [ 63 | "(BE) -+*##*+- ", 64 | "(BE) -+#######*={BK}: ", 65 | "(BE) -+*******+++={BK}+*+ ", 66 | "(BE) -++++++++=====- {BK}**+-", 67 | "(BE) =++==+ =====-{BK}-==", 68 | "(BE)+++++ +++-{BK}---", 69 | "(BE)++++ {BK}+++(BE)+++{BK}= ", 70 | "(BE)**** {BK}******+ (BE)** ", 71 | "(BE)*** * ", 72 | ] 73 | 74 | [archbang] 75 | margin = [3, 2, 3,] 76 | art = [ 77 | "{CN} /\\ ", 78 | "{CN} / _\\ ", 79 | "{CN} / \\ / ", 80 | "{CN} / // \\ ", 81 | "{CN} / // \\ ", 82 | "{CN} / ___()___ \\ ", 83 | "{CN} /.\\` `/.\\ ", 84 | ] 85 | 86 | [archcraft] 87 | margin = [2, 2, 3,] 88 | art = [ 89 | "{GN} . ", 90 | "{GN} / \\ ", 91 | "{GN} /\\ \\ ", 92 | "{GN} / \\ ", 93 | "{GN} / , ^ , \\ ", 94 | "{GN} / < > -\\ ", 95 | "{GN}/_-'' ''-_\\", 96 | ] 97 | 98 | [arco] 99 | alias = "arcolinux" 100 | margin = [1, 2, 2,] 101 | art = [ 102 | "{BE} A ", 103 | "{BE} ooo ", 104 | "{BE} ooooo ", 105 | "{BE} ooooooo ", 106 | "{BE} ooooooooo ", 107 | "{BE} ooooo ooooo ", 108 | "{BE} ooooo ooooo ", 109 | "{BE} ooooo ooooo ", 110 | "{BE} ooooo {GN}oooooooooo ", 111 | "{BE} ooooo {GN}oooooooo ", 112 | "{BE}ooooo {GN}oooooo", 113 | ] 114 | 115 | [artix] 116 | margin = [2,2,2,] 117 | art = [ 118 | "{BE} /\\ ", 119 | "{BE} / \\ ", 120 | "{BE} /`'.,\\ ", 121 | "{BE} / ', ", 122 | "{BE} / ,`\\ ", 123 | "{BE} / ,.'`. \\ ", 124 | "{BE}/.,'` `'.\\", 125 | ] 126 | 127 | ### B #### 128 | [bedrock] 129 | margin = [4, 3, 3,] 130 | art = [ 131 | "{WE}__ ", 132 | "{WE}\\ \\___ ", 133 | "{WE} \\ _ \\ ", 134 | "{WE} \\___/ ", 135 | ] 136 | 137 | [buildroot] 138 | margin = [3, 3, 3,] 139 | art = [ 140 | "{YW} ___ ", 141 | "{YW} / \\` \\ ", 142 | "{YW}| : :| ", 143 | "{YW} -. _:__.- ", 144 | "{YW} \\` ---- \\`" 145 | ] 146 | 147 | ### C #### 148 | [cachy] 149 | alias = "cachyos" 150 | margin = [2, 2, 2,] 151 | art = [ 152 | "{CN} /''''''''''''/ ", 153 | "{CN} /''''''''''''/ ", 154 | "{CN} /''''''/ ", 155 | "{CN}/''''''/ ", 156 | "{CN}\\......\\ ", 157 | "{CN} \\......\\ ", 158 | "{CN} \\.............../", 159 | "{CN} \\............./ ", 160 | ] 161 | 162 | [centos] 163 | margin = [2, 3, 2] 164 | art = [ 165 | "{GN} ____{YW}^{MA}____ ", 166 | "{GN} |\\ {YW}|{MA} /| ", 167 | "{GN} | \\ {YW}|{MA} / | ", 168 | "{MA}<---- {BE}----> ", 169 | "{BE} | / {GN}|{YW} \\ | ", 170 | "{BE} |/__{GN}|{YW}__\\| ", 171 | "{GN} v ", 172 | ] 173 | 174 | [crux] 175 | margin = [2, 2, 3,] 176 | art = [ 177 | "{BE} ___ ", 178 | "{BE} (.· | ", 179 | "{BE} ({YW}<>{BE} | ", 180 | "{BE} / {WE}__{BE} \\ ", 181 | "{BE} ( {WE}/ \\{BE} /|", 182 | "{YW}_/\\{WE}(__){YW}/_)", 183 | "{YW}\\/{BE}-____{YW}\\/ ", 184 | ] 185 | 186 | [crystal] 187 | margin = [2, 2, 2,] 188 | art = [ 189 | "(MA) -//. ", 190 | "(MA) -//. ", 191 | "(MA) -//. . ", 192 | "(MA) -//. '//- ", 193 | "(MA) /+: :+/", 194 | "(MA) .//' .//. ", 195 | "(MA) . .//. ", 196 | "(MA) .//. ", 197 | "(MA) .//. ", 198 | ] 199 | 200 | ### D ### 201 | [dahlia] 202 | alias = "dahliaos" 203 | margin = [1, 4, 3,] 204 | art = [ 205 | "{RD} _ ", 206 | "{RD} ___/ \\___ ", 207 | "{RD} | _-_ | ", 208 | "{RD} | / \\ | ", 209 | "{RD}/ | | \\ ", 210 | "{RD}\\ | | / ", 211 | "{RD} | \\ _ _ / | ", 212 | "{RD} |___ - ___| ", 213 | "{RD} \\_/ ", 214 | ] 215 | 216 | [debian] 217 | margin = [1, 2, 3,] 218 | art = [ 219 | "(RD) , - = - , ", 220 | "(RD) = ,, = ", 221 | "(RD)= :° ; = ", 222 | "(RD)= :. = ", 223 | "(RD)^= ° == ° ", 224 | "(RD)°=. ", 225 | "(RD) ^==.. ", 226 | "(RD) ^^°==.. ", 227 | "(RD) ^^°= ", 228 | ] 229 | 230 | [deepin] 231 | margin = [2, 2, 2,] 232 | art = [ 233 | "{BE} ____________ ", 234 | "{BE} / / / ___\\ ", 235 | "{BE} / | / __/ ___\\ ", 236 | "{BE} / \\/ / / \\ ", 237 | "{BE}| \\ \\ |", 238 | "{BE} \\_______/ _/ / ", 239 | "{BE} \\________/ / ", 240 | "{BE} \\____________/ ", 241 | ] 242 | 243 | [devuan] 244 | margin = [2, 2, 2,] 245 | art = [ 246 | "..:::. ", 247 | " ..-==- ", 248 | " .+#:", 249 | " =@@", 250 | " :+%@#:", 251 | ".:=+#@@%*: ", 252 | "@@@@#=: ", 253 | ] 254 | 255 | [dragonfly] 256 | alias = "dragonflybsd" 257 | margin = [2, 3, 2,] 258 | art = [ 259 | " ,{RD}_{WE}, ", 260 | "('-_{RD}|{WE}_-') ", 261 | " >--{RD}|{WE}--< ", 262 | "(_-'{RD}|{WE}'-_) ", 263 | " {RD}| ", 264 | " {RD}| ", 265 | " {RD}| ", 266 | ] 267 | 268 | ### E ### 269 | [elementary] 270 | alias = "elementaryos" 271 | margin = [2, 2, 3,] 272 | art = [ 273 | "{WE} _______ ", 274 | "{WE} / ____ \\ ", 275 | "{WE}/ | / /\\", 276 | "{WE}|__\\ / / |", 277 | "{WE}\\ /__/ /", 278 | "{WE} \\_______/ ", 279 | ] 280 | 281 | [endeavour] 282 | alias = "endeavouros" 283 | margin = [2, 2, 3,] 284 | art = [ 285 | "{MA} /\\ ", 286 | "{RD} /{MA}/ \\{CN}\\ ", 287 | "{RD} /{MA}/ \\{CN}\\ ", 288 | "{RD} / {MA}/ _) {CN})", 289 | "{RD} /_{MA}/___-- {CN}__- ", 290 | "{CN}/____-- ", 291 | ] 292 | 293 | [evolution] 294 | alias = "evolutionos" 295 | margin = [2, 3, 3,] 296 | art = [ 297 | "{GN} $2,coddoc' ", 298 | "{GN} 'cddddddddddc' ", 299 | "{GN} 'ddd$1OWWXXXXXXK$2ddo. ", 300 | "{GN}.dddd$1OMX$2ddddddddddd. ", 301 | "{GN}odddd$1OMX$2k$100O$2k$1OO$2ddddo", 302 | "{GN}.dddd$1OMX$2kOOOxOkdddd. ", 303 | "{GN} .ddd$1OWW$2X$1XXXXXK$2ddd' ", 304 | "{GN} 'dddddddddddd' ", 305 | "{GN} 'cddddd, " 306 | ] 307 | 308 | ### F ### 309 | [fedora] 310 | margin = [1, 2, 3,] 311 | art = [ 312 | "(WE) _____ ", 313 | "(WE) / __)(BE)\\ ", 314 | "(WE) | / (BE)\\ \\", 315 | "(BE) __(WE)_| |_(BE)_/ /", 316 | "(BE) / (WE)(_ _)(BE)_/ ", 317 | "(BE)/ / (WE)| | ", 318 | "(BE)\\ \\(WE)__/ | ", 319 | "(BE) \\(WE)(_____/ ", 320 | 321 | ] 322 | 323 | [freebsd] 324 | margin = [2, 4, 2,] 325 | art = [ 326 | "{RD}/\\,-'''''-,/\\ ", 327 | "{RD}\\_) (_/ ", 328 | "{RD}| | ", 329 | "{RD}| | ", 330 | " {RD}; ; ", 331 | " {RD}'-_____-' ", 332 | ] 333 | 334 | ### G ### 335 | [garuda] 336 | margin = [3, 2, 3,] 337 | art = [ 338 | "(MA) .----. ", 339 | "(MA) .' , '. ", 340 | "(MA) .' '-----|", 341 | "(MA)'. -----, ", 342 | "(MA) '.____.' ", 343 | ] 344 | 345 | [gentoo] 346 | margin = [2, 2, 3,] 347 | art = [ 348 | "{MA} _-----_ ", 349 | "{MA}( \\ ", 350 | "{MA}\\ 0 \\ ", 351 | "{WE} \\ )", 352 | "{WE} / _/ ", 353 | "{WE}( _- ", 354 | "{WE}\\____- ", 355 | ] 356 | 357 | [guix] 358 | margin = [3, 2, 3,] 359 | art = [ 360 | "{YW}|.__ __.| ", 361 | "{YW}|__ \\ / __| ", 362 | "{YW} \\ \\ / / ", 363 | "{YW} \\ \\ / / ", 364 | "{YW} \\ \\ / / ", 365 | "{YW} \\ \\/ / ", 366 | "{YW} \\__/ ", 367 | ] 368 | 369 | ### H ### 370 | [hyperbola] 371 | margin = [2, 2, 3] 372 | art = [ 373 | " |`__.`/ ", 374 | " \\____/ ", 375 | " .--. ", 376 | " / \\ ", 377 | " / ___ \\ ", 378 | " / .` `.\\ ", 379 | "/.` `.\\ ", 380 | ] 381 | 382 | ### I ### 383 | [iglunix] 384 | margin = [0, 2, 2,] 385 | art = [ 386 | " | ", 387 | " | | ", 388 | " | ", 389 | " | ________ ", 390 | " | /\\ | \\ ", 391 | " / \\ | \\ | ", 392 | " / \\ \\ | ", 393 | " / \\________\\ ", 394 | " \\ / / ", 395 | " \\ / / ", 396 | " \\ / / ", 397 | " \\/________/ ", 398 | ] 399 | 400 | [instant] 401 | alias = "instantos" 402 | margin = [3, 2, 2,] 403 | art = [ 404 | "{WE} ,-''-, ", 405 | "{WE}: .''. : ", 406 | "{WE}: ',,' : ", 407 | "{WE} '-____:__ ", 408 | "{WE} : \\`.", 409 | "{WE} \\`._.'", 410 | ] 411 | 412 | ### J ### 413 | 414 | ### K ### 415 | [kali] 416 | margin = [0, 1, 2] 417 | art = [ 418 | "{BE} ⢀ ⡀ ", 419 | "{BE} ⢀⡀⢀⡀⢣⡐⣄ ", 420 | "{BE} ⢤⣬⣿⣿⣿⣿⣿⣶⣿⣮⣷⣄ ", 421 | "{BE} ⢴⣿⣿⠛⠉ ⠈⠉⠻⣿⣿⣿⣷⣄ ", 422 | "{BE}⢠⣿⣿⡇ ⢿⣿⣦⣿⣦ ", 423 | "{BE} ⡸⣿⣷⡀ ⠙⠻⣿⣷⡀ ", 424 | "{BE} ⠸⠻⣿⣦⡀ ⠈⢻⣿⠇", 425 | "{BE} ⠉⠛⠷⣦⣄⡀ ⠈⠁ ", 426 | "{BE} ⠉⠛⢷⣄ ", 427 | "{BE} ⠈⢿ ", 428 | ] 429 | 430 | [neon] 431 | alias = "kdeneon" 432 | margin = [3, 3, 3,] 433 | art = [ 434 | "{GN} --- _ ", 435 | "{GN} / --- \\ ", 436 | "{GN}| | | | ", 437 | "{GN} \\ --- _/ ", 438 | "{GN} --- ", 439 | ] 440 | 441 | ### L ### 442 | [lite] 443 | alias = "linuxlite" 444 | margin = [3, 3, 4,] 445 | art = [ 446 | "{YW} /\\ ", 447 | "{YW} / \\", 448 | "{YW} / {WE}/ {YW}/", 449 | "{YW}> {WE}/ {YW}/ ", 450 | "{YW}\\ {WE}\\ {YW}\\ ", 451 | " {YW}\\_{WE}\\{YW}_\\", 452 | "{WE} \\ ", 453 | ] 454 | 455 | 456 | ### M ### 457 | [macos] 458 | margin = [2, 4, 2,] 459 | art = [ 460 | "{GN} .:' ", 461 | "{GN} _ :'_ ", 462 | "{YW} .'\\`_\\`-'_\\`. ", 463 | "{RD}:________.-' ", 464 | "{RD}:________: ", 465 | "{MA} :_______\\`-; ", 466 | "{BE} \\`._.-._.' ", 467 | ] 468 | 469 | [mageria] 470 | margin = [2, 4, 3,] 471 | art = [ 472 | "{BE} * ", 473 | "{BE} * ", 474 | "{BE} ** ", 475 | "{BK} /\\__/\\ ", 476 | "{BK}/ \\ ", 477 | "{BK}\\ / ", 478 | "{BK} \\____/ ", 479 | ] 480 | 481 | [manjaro] 482 | margin = [2, 2, 4,] 483 | art = [ 484 | " !DT! ", 485 | " !DT! ", 486 | " !DT! ", 487 | " !DT! !DT! ", 488 | " !DT! !DT! ", 489 | " !DT! !DT! ", 490 | ] 491 | 492 | [mint] 493 | alias = "linuxmint" 494 | margin = [2, 2, 4,] 495 | art = [ 496 | "{CN} ___________ ", 497 | "{CN}|_ \\", 498 | "{CN} | (WE)| _____ {CN}|", 499 | "{CN} | (WE)| | | | {CN}|", 500 | "{CN} | (WE)| | | | {CN}|", 501 | "{CN} | (WE)\\_____/ {CN}|", 502 | "{CN} \\_________/", 503 | ] 504 | 505 | [mx] 506 | margin = [2, 2, 2,] 507 | art = [ 508 | "{WE} \\\\ / ", 509 | "{WE} \\\\/ ", 510 | "{WE} \\\\ ", 511 | "{WE} /\\/ \\\\ ", 512 | "{WE} / \\ /\\ ", 513 | "{WE} / \\/ \\ ", 514 | "{WE}/__________\\ ", 515 | ] 516 | 517 | 518 | ### N ### 519 | [netbsd] 520 | margin = [2, 2, 2,] 521 | art = [ 522 | "{WE}\\\\{YW}\\`-______,----__ ", 523 | "{WE} \\\\ {YW}__,---\\`_", 524 | "{WE} \\\\ {YW}\\`.____ ", 525 | "{WE} \\\\{YW}-______,----\\`-", 526 | "{WE} \\\\ ", 527 | "{WE} \\\\ ", 528 | "{WE} \\\\ ", 529 | ] 530 | 531 | [nobara] 532 | margin = [2, 2, 2,] 533 | art = [ 534 | "{WE} _._. _..,._ ", 535 | "{WE}|##############. ", 536 | "{WE}|################. ", 537 | "{WE}|#####/ . \\#####. ", 538 | "{WE}|####| ### > ### ", 539 | "{WE}|##### \\`\\`\\`|##### ", 540 | "{WE}|######==_ |##### ", 541 | "{WE}|######\"##| |##### ", 542 | "{WE} \\`\"\"\\`\\` ' \\`\\##/ ", 543 | ] 544 | 545 | [none] 546 | margin = [0, 0, 0,] 547 | art = [""] 548 | 549 | [nixos] 550 | margin = [1, 2, 3,] 551 | art = [ 552 | "{BE} ⣀⡀ {CN}⣀⣀⡀ ⢀⣀ ", 553 | "{BE} ⠈⢿⣷⡀ {CN}⠈⢿⣷⣄⣾⡿⠃ ", 554 | "{BE} ⢀⣶⣶⣾⣿⣿⣶⣶⣮{CN}⣻⣿⣿⠁ {BE}⢠⡀ ", 555 | "{CN} ⣩⣭⡍ ⢻⣿⣆{BE}⢠⣿⡗ ", 556 | "{CN}⢠⣤⣤⣤⣴⣿⠟ ⠹{BE}⣳⣿⣿⣤⣤⡄", 557 | "{CN}⠘⠛⠛⣿⣿⢯{BE}⣦ ⣰⣿⠟⠛⠛⠛⠃", 558 | "{CN} ⢼⣿⠋{BE}⠹⣿⣧{CN}⢀⣀⣀⣀⣜⣛⣋⣀⣀⡀ ", 559 | "{CN} ⠈⠃ {BE}⢀⣾⣿⣷{CN}⡻⠿⠿⢿⣿⡿⠿⠿⠁ ", 560 | "{BE} ⢠⣾⡿⠙⢿⣷⡀ {CN}⠈⢿⣷⡄ ", 561 | "{BE} ⠉⠁ ⠈⠉⠉ {CN}⠈⠉ ", 562 | ] 563 | 564 | ### O ### 565 | [openbsd] 566 | margin = [2, 2, 3,] 567 | art = [ 568 | "{YW} _____ ", 569 | "{YW} \\- -/ ", 570 | "{YW} \\_/ \\ ", 571 | "{YW} | {WE}O O{YW} |", 572 | "{YW} |_ < ) 3 )", 573 | "{YW} / \\ / ", 574 | " {YW} /-_____-\\ ", 575 | ] 576 | 577 | [opensuse] 578 | margin = [0, 0, 2,] 579 | art = [ 580 | "!DT! .;ldkO0000Okdl;. ", 581 | "!DT! .;d00xl:^''''''^:ok00d;. ", 582 | "!DT! .d00l' 'o00d. ", 583 | "!DT! .d0Kd' (GN)Okxol:;,. !DT!:O0d. ", 584 | "!DT! .OK(GN)KKK0kOKKKKKKKKKKOxo:, !DT!lK0. ", 585 | "!DT! ,0K(GN)KKKKKKKKKKKKKKKOP^!DT!,,,(GN)^dx: !DT!;OO, ", 586 | "!DT!.OK(GN)KKKKKKKKKKKKKKKk'!DT!.oOPPb.(GN)'Ok. !DT!cKO.", 587 | "!DT!:KK(GN)KKKKKKKKKKKKKKK: !DT!kKx..dd (GN)lKd !DT!'OK:", 588 | "!DT!dKK(GN)KKKKKKKKKOxOKKKd !DT!^OKKKO' (GN)kKKc !DT!dKd", 589 | "!DT!dKK(GN)KKKKKKKKKK;.;oOKx,..!DT!^(GN)..;kKKKO. !DT!dKd", 590 | "!DT!:KK(GN)KKKKKKKKKK0o;...^cdxxOK0O/^^' !DT!.OK:", 591 | "!DT! kKK(GN)KKKKKKKKKKKKKOx;,,......,;od !DT!lKk ", 592 | "!DT! '0K(GN)KKKKKKKKKKKKKKKKKKKK00KKOo^ !DT!c00' ", 593 | "!DT! 'kK(GN)KKOxddxkOO00000Okxoc;'' !DT!.dKk' ", 594 | "!DT! l0Ko. .c00l' ", 595 | "!DT! 'l0Kk:. .;xK0l' ", 596 | "!DT! 'lkK0xl:;,,,,;:ldO0kl' ", 597 | "!DT! '^:ldxkkkkxdl:^' " 598 | ] 599 | 600 | ### P ### 601 | [parabola] 602 | margin = [2, 2, 2,] 603 | art = [ 604 | " __ __ __ _ ", 605 | "`_//_//_/ / `. ", 606 | " / .`", 607 | " / .` ", 608 | " /.` ", 609 | " /` ", 610 | ] 611 | 612 | [pop] 613 | alias = "popos, pop_os" 614 | margin = [1, 1, 2,] 615 | art = [ 616 | "{CN} ______ ", 617 | "{CN} \\ _ \\ __ ", 618 | "{CN} \\ \\ \\ \\ / / ", 619 | "{CN} \\ \\_\\ \\ / / ", 620 | "{CN} \\ ___\\ /_/ ", 621 | "{CN} \\ \\ _ ", 622 | "{CN} __\\_\\__(_)_ ", 623 | "{CN} (___________) ", 624 | ] 625 | 626 | [postmarketos] 627 | margin = [2, 2, 2,] 628 | art = [ 629 | "{GN} /\\ ", 630 | "{GN} / \\ ", 631 | "{GN} / \\ ", 632 | "{GN} \\__ \\ ", 633 | "{GN} /\\__ \\ _\\ ", 634 | "{GN} / / \\/ __ ", 635 | "{GN} / / ____/ \\ ", 636 | "{GN} / \\ \\ \\ ", 637 | "{GN}/_____/ /________\\", 638 | ] 639 | 640 | [pureos] 641 | margin = [3, 2, 2,] 642 | art = [ 643 | " _____________ ", 644 | "| _________ |", 645 | "| | | |", 646 | "| | | |", 647 | "| |_________| |", 648 | "|_____________|", 649 | ] 650 | 651 | ### Q ### 652 | 653 | ### R ### 654 | [raspbian] 655 | alias = "raspberrypios" 656 | margin = [2, 2, 3,] 657 | art = [ 658 | "{GN} __ __ ", 659 | "{GN} (_\\)(/_) ", 660 | "{RD} (_(__)_) ", 661 | "{RD}(_(_)(_)_) ", 662 | "{RD} (_(__)_) ", 663 | "{RD} (__) ", 664 | ] 665 | 666 | [reborn] 667 | alias = "rebornos" 668 | margin = [2, 3, 3,] 669 | art = [ 670 | "{BE} _______ ", 671 | "{BE} /\\_____/\\ ", 672 | "{BE} / /\\___/\\ \\ ", 673 | "{BE}/_/_/ \\_\\_\\", 674 | "{BE}\\ \\ \\___/ / /", 675 | "{BE} \\ \\/___\\/ / ", 676 | "{BE} \\/_____\\/ ", 677 | ] 678 | 679 | [rocky] 680 | margin = [1, 2, 3] 681 | art = [ 682 | "{GN} `-/+++++++++/-.` ", 683 | "{GN} `-+++++++++++++++++-` ", 684 | "{GN}.+++++++++++++++++++++. ", 685 | "{GN}-+++++++++++++++++++++++.", 686 | "{GN}+++++++++++++++/-/+++++++", 687 | "{GN}+++++++++++++/. ./+++++", 688 | "{GN}+++++++++++:. ./+++", 689 | "{GN}+++++++++:` `:/:` .:/", 690 | "{GN}-++++++:` .:+++++:` ", 691 | "{GN} .+++-` ./+++++++++:` ", 692 | "{GN} `-` ./+++++++++++- ", 693 | "{GN} -+++++++++:-.` ", 694 | ] 695 | 696 | ### S ### 697 | [slackware] 698 | margin = [2, 2, 3,] 699 | art = [ 700 | "{BE} ________ ", 701 | "{BE} / ______| ", 702 | "{BE} | |______ ", 703 | "{BE} \\______ \\ ", 704 | "{BE} ______| | ", 705 | "{BE}| |________/ ", 706 | "{BE}|____________", 707 | ] 708 | 709 | [solus] 710 | margin = [3, 2, 2,] 711 | art = [ 712 | "{BE} /| ", 713 | "{BE} / |\\ ", 714 | "{BE} / | \\ _ ", 715 | "{BE} /___|__\\_\\ ", 716 | "{BE} \\ / ", 717 | "{BE} \\-------´ ", 718 | ] 719 | 720 | [sourcemage] 721 | margin = [0, 2, 3] 722 | art = [ 723 | " :ymNMNho. ", 724 | ".+sdmNMMMMMMMMMMy` ", 725 | ".-::/yMMMMMMMMMMMm- ", 726 | " sMMMMMMMMMMMm/ ", 727 | " /NMMMMMMMMMMMMMm: ", 728 | " .MMMMMMMMMMMMMMMMM: ", 729 | " `MMMMMMMMMMMMMMMMMN. ", 730 | " NMMMMMMMMMMMMMMMMMd ", 731 | " mMMMMMMMMMMMMMMMMMMo ", 732 | " hhMMMMMMMMMMMMMMMMMM. ", 733 | " .`/MMMMMMMMMMMMMMMMMs ", 734 | " :mMMMMMMMMMMMMMMMN` ", 735 | " `sMMMMMMMMMMMMMMM+ ", 736 | " /NMMMMMMMMMMMMMN` ", 737 | " oMMMMMMMMMMMMM+ ", 738 | " ./sd.-hMMMMMMMMmmN` ", 739 | " ./+oyyyh- `MMMMMMMMMmNh ", 740 | " sMMMMMMMMMmmo", 741 | " `NMMMMMMMMMd:", 742 | " -dMMMMMMMMMo", 743 | " -shmNMMms.", 744 | ] 745 | 746 | ### T ### 747 | [tux] 748 | alias = "default" 749 | margin = [2, 2, 3,] 750 | art = [ 751 | "(BK) .--. ", 752 | "(BK) |!DT!o(BK)_!DT!o (BK)| ", 753 | "(BK) |(YW):_/ (BK)| ", 754 | "(BK) /!DT!/ \\ (BK)\\ ", 755 | "(BK) (!DT!| | (BK)) ", 756 | "(YW) /'!DT!|_ _/(YW)'\\ ", 757 | "(YW) \\___)(BK)=(YW)(___/ ", 758 | ] 759 | 760 | ### U ### 761 | [ubuntu] 762 | margin = [0, 2, 4,] 763 | art = [ 764 | "{RD} .-. ", 765 | "{RD} .-'````( ) ", 766 | "{RD} ,`\\ \\ `-`. ", 767 | "{RD} / \\ '````-. ` ", 768 | "{RD} .-. , `___:", 769 | "{RD} ( ) : ___ ", 770 | "{RD} `-` ` , :", 771 | "{RD} \\ / ,....-` , ", 772 | "{RD} `./ / .-.` ", 773 | "{RD} `-....-( ) ", 774 | "{RD} `-` ", 775 | ] 776 | 777 | ### V ### 778 | [vanilla] 779 | alias = "vanillaos" 780 | margin = [2, 2, 2,] 781 | art = [ 782 | "{YW} .#. ", 783 | "{YW} :#=#: ", 784 | "{YW}.#=\":.:##=##:.:\"=#. ", 785 | "{YW}\\`:#=#####=####=##:\\` ", 786 | "{YW} \\`:####=\\` \\`=####:\\`", 787 | "{YW} .:##,. .,##:. ", 788 | "{YW} :##=##:-:##=##: ", 789 | "{YW} .#=##:\\` \\`:##=#. ", 790 | "{YW} \\`\\` \\`\\` ", 791 | ] 792 | 793 | [venom] 794 | margin = [1, 2, 3,] 795 | art = [ 796 | "{WE} ++** ", 797 | "{WE} *===**====+* ", 798 | "{WE} *====* +===+ ", 799 | "{WE} *==*+===* *===* ", 800 | "{WE}*===* *===+ *===*", 801 | "{WE}*===* +===+ *===*", 802 | "{WE}*===* +===* *===*", 803 | "{WE} *===* *===+*==* ", 804 | "{WE} +===+ *===+=* ", 805 | "{WE} *+====**===* ", 806 | "{WE} **++ ", 807 | ] 808 | 809 | [void] 810 | margin = [2, 2, 4,] 811 | art = [ 812 | "(GN) _______ ", 813 | "(GN) _ \\______ - ", 814 | "(GN)| \\ ___ \\ |", 815 | "(GN)| | / \\ | |", 816 | "(GN)| | \\___/ | |", 817 | "(GN)| \\______ \\_|", 818 | "(GN) -_______\\ ", 819 | ] 820 | 821 | [voyager] 822 | margin = [2, 2, 2,] 823 | art = [ 824 | "{RD} _____ ____ ", 825 | "{RD}| | | |", 826 | "{RD}| | | |", 827 | "{RD}| | | |", 828 | "{RD}| | |____|", 829 | "{RD}| | _____ ", 830 | "{RD}| || |", 831 | "{RD}|_____||_____|", 832 | 833 | ] 834 | 835 | ### W ### 836 | 837 | ### X ### 838 | [xero] 839 | alias = "xerolinux" 840 | margin = [3, 2, 2,] 841 | art = [ 842 | "{WE} /\\ ", 843 | "{WE}______ / \\______", 844 | "{WE} /\\ /\\ ", 845 | "{WE} / || \\ ", 846 | "{WE}____/__/__\\__\\___", 847 | "{WE} / __| |__-\\ ", 848 | "{WE} /_-'' ''-_\\ ", 849 | ] 850 | 851 | ### Y ### 852 | 853 | ### Z ### 854 | [zorin] 855 | margin = [1, 1, 2] 856 | art = [ 857 | "(BE) ⣰⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣆ ", 858 | "(BE) ⠐⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠂ ", 859 | "(BE) ", 860 | "(BE) ⣴⣿⣿⣿⣿⣿⣿⣿⡿⠟⠋⠁ ⣀⣤⣶⣦ ", 861 | "(BE)⢼⣿⣿⣿⣿⣿⠿⠋⠁ ⢀⣠⣶⣿⣿⣿⣿⣿⡇", 862 | "(BE) ⠹⣿⠛⠉ ⣀⣴⣾⣿⣿⣿⣿⣿⣿⣿⠟ ", 863 | "(BE) ", 864 | "(BE) ⠠⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⣤⠄ ", 865 | "(BE) ⠹⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠏ ", 866 | ] 867 | -------------------------------------------------------------------------------- /docs/catnap.1: -------------------------------------------------------------------------------- 1 | .TH catnap 1 "2024-05-11" "1.0" "User Commands" 2 | .SH NAME 3 | Catnap \- A highly customizable systemfetch written in nim 4 | .SH SYNOPSIS 5 | .B catnap 6 | .B -d 7 | .RI [\| DistroId \|] 8 | .br 9 | .B catnap 10 | .B -g 11 | .RI [\| StatName \|] 12 | .br 13 | .B catnap 14 | .B -l 15 | .RI [\| Layout \|] 16 | .SH DESCRIPTION 17 | Catnap is a simple system-information concatenation tool written with nim. It has the ability to alter the names and colors of the statistics via its configuration file. 18 | .SH OPTIONS 19 | .TP 20 | \fB\-c\fR, \fB\-\-config\fR=File 21 | Uses a custom location for the config file 22 | .TP 23 | \fB\-h\fR, \fB\-\-help\fR 24 | Show the help list 25 | .TP 26 | \fB\-v\fR, \fB\-\-version\fR 27 | Show Catnap's version 28 | .TP 29 | \fB\-d\fR, \fB\-\-distroid\fR=DistroId 30 | Set which DistroId to use 31 | .TP 32 | \fB\-g\fR, \fB\-\-grep\fR=StatName 33 | Get the specified stats value 34 | .TP 35 | \fB\-m\fR, \fB\-\-margin\fR=Margin 36 | Overwrite margin value for the displayed logo 37 | .TP 38 | \fB\-l\fR, \fB\-\-layout\fR=Layout 39 | Overwrite layout config value 40 | .TP 41 | \fB\-fe\fR, \fB\-\-figletLogos.enable\fR=On/Off 42 | Overwrite figletLogos mode 43 | .TP 44 | \fB\-ff\fR, \fB\-\-figletLogos.font\fR=Font 45 | Overwrite figletLogos font 46 | .TP 47 | \fB\-fm\fR, \fB\-\-figletLogos.margin\fR=Margin 48 | Overwrite figletLogos margin 49 | .SH FILES 50 | .TP 51 | \fB\.config/catnap/config.toml\fR 52 | Configuration file 53 | .TP 54 | \fB\.config/catnap/distros.toml\fR 55 | Distribution logos file 56 | .SH BUGS 57 | .TP 58 | Report all bugs to https://github.com/iinsertNameHere/catnap/issues 59 | .SH SEE ALSO 60 | .TP 61 | \fBfiglet\fP(6) 62 | .TP 63 | \fBcatnap\fP(5) 64 | -------------------------------------------------------------------------------- /docs/catnap.5: -------------------------------------------------------------------------------- 1 | .TH catnap 5 "2024-05-11" "1.0" "User Commands" 2 | .SH NAME 3 | config.toml and distros.toml \- Catnap configuration and art files 4 | .SH SYNOPSIS 5 | ~/.config/catnap/config.toml ~/.config/catnap/distros.toml 6 | .SH DESCRIPTION 7 | You can change the names, colors, and icons for the various stats inside the stats section of the config.toml file. 8 | .TP 9 | To create a new DistroArt object inside the distros.toml file, add a new section to the file. 10 | .TP 11 | .SH CONFIGURATION 12 | .TP 13 | If you don't want to display a stat, you can just comment out the line. 14 | .TP 15 | You can change the order in which the stats are displayed by just changing the order in the config. 16 | .TP 17 | Separators are defined by creating a new key with the sep_ prefix. The value of the key can be anything. 18 | .TP 19 | To display multiple disks, just create a new stat is named disk_[index]. To check what the index of the disk you want to add is, just run catnap -g disks. 20 | .TP 21 | .SH LAYOUT 22 | .TP 23 | In the layout you can define how the logo and stats will be arranged. 24 | .TP 25 | Use Inline to place the logo and stats next to each other. 26 | .TP 27 | Use ArtOnTop to place the logo on top of the stats. 28 | .TP 29 | Use StatsOnTop to place the stats on top of the logo 30 | .TP 31 | .SH FIGLET LOGOS 32 | .TP 33 | \fBenable\fR 34 | Set enable to true/false to enable or disable figlet generated logos. 35 | .TP 36 | \fBcolor\fR 37 | Use color to set the color the figlet logos should have. 38 | .TP 39 | \fBfont\fR 40 | Use font to set what font figlet should use. 41 | .TP 42 | \fBmargin\fR 43 | Use margin to define the margins of the figlet logos. 44 | .TP 45 | .SH IMAGE MODE 46 | \fBenable\fR 47 | Set enable to true/false to enable or disable image mode. 48 | .TP 49 | \fBpath\fR 50 | Use path to define what image file to display. 51 | .TP 52 | \fBscale\fR 53 | Use scale to set the scale of the image. 54 | .TP 55 | \fBmargin\fR 56 | Use margin to define the logo margins. 57 | .TP 58 | .SH DISTRO ART 59 | .TP 60 | \fBmargin\fR 61 | The margin key is used to define the top, left and right margins of the art. 62 | .TP 63 | \fBart\fR 64 | The art key is used to define the ascii-art for your distro. 65 | .TP 66 | \fBalias\fR 67 | The alias key can be used to define alternate names that should also refer to the DistroArt Object. 68 | .TP 69 | .SH COLORS 70 | Catnap's color system uses a ColorId, which is made up of the colors first and last letter, enclosed in characters that indicate the type of color. 71 | .TP 72 | \fBColor Types:\fR 73 | .TP 74 | Foreground Normal -> (#) 75 | .TP 76 | Foreground Bright -> {#} 77 | .TP 78 | Background Normal -> [#] 79 | .TP 80 | Background Bright -> <#> 81 | .TP 82 | "#" is a color ID. 83 | .TP 84 | \fBColor IDs:\fR 85 | .TP 86 | BLACK -> Background 87 | .TP 88 | RED -> RD 89 | .TP 90 | GREEN -> GN 91 | .TP 92 | YELLOW -> YW 93 | .TP 94 | BLUE -> BE 95 | .TP 96 | MAGENTA -> MA 97 | .TP 98 | CYAN -> CN 99 | .TP 100 | WHITE -> WE 101 | .TP 102 | .SH MORE INFORMATION 103 | For more information on configuration, go to https://github.com/iinsertNameHere/catnap 104 | .SH SEE ALSO 105 | .TP 106 | \fBfiglet\fP(6) 107 | .TP 108 | \fBcatnap\fP(1) 109 | -------------------------------------------------------------------------------- /image/demo1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iinsertNameHere/catnap/268e207ab39d217b6768229e371c9520688a3e68/image/demo1.png -------------------------------------------------------------------------------- /image/demo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iinsertNameHere/catnap/268e207ab39d217b6768229e371c9520688a3e68/image/demo2.png -------------------------------------------------------------------------------- /image/demo3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iinsertNameHere/catnap/268e207ab39d217b6768229e371c9520688a3e68/image/demo3.png -------------------------------------------------------------------------------- /image/distros.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iinsertNameHere/catnap/268e207ab39d217b6768229e371c9520688a3e68/image/distros.png -------------------------------------------------------------------------------- /image/logo/catnap.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iinsertNameHere/catnap/268e207ab39d217b6768229e371c9520688a3e68/image/logo/catnap.psd -------------------------------------------------------------------------------- /image/logo/catnap.svg: -------------------------------------------------------------------------------- 1 | 2 | 9140009-ai 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /image/margin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iinsertNameHere/catnap/268e207ab39d217b6768229e371c9520688a3e68/image/margin.png -------------------------------------------------------------------------------- /image/no_margin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iinsertNameHere/catnap/268e207ab39d217b6768229e371c9520688a3e68/image/no_margin.png -------------------------------------------------------------------------------- /scripts/basic.flf: -------------------------------------------------------------------------------- 1 | flf2a$ 8 8 17 -1 2 2 | basic.flf by Craig O'Flaherty 3 | August 17, 1994 4 | $$@ 5 | $$@ 6 | $$@ 7 | $$@ 8 | $$@ 9 | $$@ 10 | $$@ 11 | $$@@ 12 | db$@ 13 | 88$@ 14 | YP$@ 15 | $@ 16 | db$@ 17 | YP$@ 18 | $@ 19 | $@@ 20 | .o. .o.$@ 21 | `8' `8'$@ 22 | $@ 23 | $@ 24 | $@ 25 | $@ 26 | $@ 27 | $@@ 28 | $@ 29 | db db $@ 30 | C88888D$@ 31 | 88 88 $@ 32 | C88888D$@ 33 | YP YP $@ 34 | $@ 35 | $@@ 36 | A $@ 37 | .d8888.$@ 38 | 88'8 YP$@ 39 | `8b8. $@ 40 | `V8b.$@ 41 | db 8 8D$@ 42 | `8888Y'$@ 43 | V $@@ 44 | db dD$@ 45 | YP d8'$@ 46 | d8' $@ 47 | d8' $@ 48 | d8' db$@ 49 | d8' YP$@ 50 | $@ 51 | $@@ 52 | .d888b. $@ 53 | 8P 8D $@ 54 | `Vb d8' $@ 55 | d88C dD$@ 56 | C8' d8D $@ 57 | `888P Yb$@ 58 | $@ 59 | $@@ 60 | Cb$@ 61 | `D$@ 62 | '$@ 63 | $@ 64 | $@ 65 | $@ 66 | $@ 67 | $@@ 68 | dD$@ 69 | d8' $@ 70 | d8 $@ 71 | C88 $@ 72 | V8 $@ 73 | V8. $@ 74 | VD$@ 75 | $@@ 76 | Cb $@ 77 | `8b $@ 78 | 8b $@ 79 | 88D$@ 80 | 8P $@ 81 | .8P $@ 82 | CP $@ 83 | $@@ 84 | $@ 85 | 8. A .8$@ 86 | `8.8.8'$@ 87 | 888 $@ 88 | .d'8`b.$@ 89 | 8' V `8$@ 90 | $@ 91 | $@@ 92 | $@ 93 | db $@ 94 | 88 $@ 95 | C8888D$@ 96 | 88 $@ 97 | VP $@ 98 | $@ 99 | $@@ 100 | $@ 101 | $@ 102 | $@ 103 | $@ 104 | db$@ 105 | V8$@ 106 | P$@ 107 | $@@ 108 | $@ 109 | $@ 110 | $@ 111 | C8888D$@ 112 | $@ 113 | $@ 114 | $@ 115 | $@@ 116 | $@ 117 | $@ 118 | $@ 119 | $@ 120 | db$@ 121 | VP$@ 122 | $@ 123 | $@@ 124 | dD$@ 125 | d8'$@ 126 | d8' $@ 127 | d8' $@ 128 | d8' $@ 129 | C8' $@ 130 | $@ 131 | $@@ 132 | .d88b. $@ 133 | .8P 88.$@ 134 | 88 d'88$@ 135 | 88 d' 88$@ 136 | `88 d8'$@ 137 | `Y88P' $@ 138 | $@ 139 | $@@ 140 | db$@ 141 | o88$@ 142 | 88$@ 143 | 88$@ 144 | 88$@ 145 | VP$@ 146 | $@ 147 | $@@ 148 | .d888b.$@ 149 | VP `8D$@ 150 | odD'$@ 151 | .88' $@ 152 | j88. $@ 153 | 888888D$@ 154 | $@ 155 | $@@ 156 | d8888b.$@ 157 | VP `8D$@ 158 | oooY'$@ 159 | ~~~b.$@ 160 | db 8D$@ 161 | Y8888P'$@ 162 | $@ 163 | $@@ 164 | j88D $@ 165 | j8~88 $@ 166 | j8' 88 $@ 167 | V88888D$@ 168 | 88 $@ 169 | VP $@ 170 | $@ 171 | $@@ 172 | ooooo$@ 173 | 8P~~~~$@ 174 | dP $@ 175 | V8888b.$@ 176 | `8D$@ 177 | 88oobY'$@ 178 | $@ 179 | $@@ 180 | dD $@ 181 | d8' $@ 182 | d8' $@ 183 | d8888b.$@ 184 | 88' `8D$@ 185 | `8888P $@ 186 | $@ 187 | $@@ 188 | d88888D$@ 189 | VP d8'$@ 190 | d8' $@ 191 | d8' $@ 192 | d8' $@ 193 | d8' $@ 194 | $@ 195 | $@@ 196 | .d888b.$@ 197 | 88 8D$@ 198 | `VoooY'$@ 199 | .d~~~b.$@ 200 | 88 8D$@ 201 | `Y888P'$@ 202 | $@ 203 | $@@ 204 | .d888b.$@ 205 | 88' `8D$@ 206 | `V8o88'$@ 207 | d8' $@ 208 | d8' $@ 209 | d8' $@ 210 | $@ 211 | $@@ 212 | $@ 213 | db$@ 214 | VP$@ 215 | $@ 216 | db$@ 217 | VP$@ 218 | $@ 219 | $@@ 220 | $@ 221 | db$@ 222 | VP$@ 223 | $@ 224 | db$@ 225 | V8$@ 226 | P$@ 227 | $@@ 228 | $@ 229 | .dP$@ 230 | .d8 $@ 231 | ,P $@ 232 | `b $@ 233 | `Vb $@ 234 | `Vb$@ 235 | $@@ 236 | $@ 237 | C8888D$@ 238 | $@ 239 | C8888D$@ 240 | $@ 241 | $@ 242 | $@ 243 | $@@ 244 | $@ 245 | Vb $@ 246 | `Vb $@ 247 | `V.$@ 248 | .d'$@ 249 | .dP $@ 250 | dP $@ 251 | $@@ 252 | .d888b.$@ 253 | VP `8D$@ 254 | odD'$@ 255 | 8P' $@ 256 | oo $@ 257 | VP $@ 258 | $@ 259 | $@@ 260 | .o888b.$@ 261 | d8' Y8$@ 262 | 8P db dP$@ 263 | 8b V8o8P$@ 264 | Y8. d$@ 265 | `Y888P'$@ 266 | $@ 267 | $@@ 268 | .d8b. $@ 269 | d8' `8b$@ 270 | 88ooo88$@ 271 | 88~~~88$@ 272 | 88 88$@ 273 | YP YP$@ 274 | $@ 275 | $@@ 276 | d8888b.$@ 277 | 88 `8D$@ 278 | 88oooY'$@ 279 | 88~~~b.$@ 280 | 88 8D$@ 281 | Y8888P'$@ 282 | $@ 283 | $@@ 284 | .o88b.$@ 285 | d8P Y8$@ 286 | 8P $@ 287 | 8b $@ 288 | Y8b d8$@ 289 | `Y88P'$@ 290 | $@ 291 | $@@ 292 | d8888b.$@ 293 | 88 `8D$@ 294 | 88 88$@ 295 | 88 88$@ 296 | 88 .8D$@ 297 | Y8888D'$@ 298 | $@ 299 | $@@ 300 | d88888b$@ 301 | 88' $@ 302 | 88ooooo$@ 303 | 88~~~~~$@ 304 | 88. $@ 305 | Y88888P$@ 306 | $@ 307 | $@@ 308 | d88888b$@ 309 | 88' $@ 310 | 88ooo $@ 311 | 88~~~ $@ 312 | 88 $@ 313 | YP $@ 314 | $@ 315 | $@@ 316 | d888b $@ 317 | 88' Y8b$@ 318 | 88 $@ 319 | 88 ooo$@ 320 | 88. ~8~$@ 321 | Y888P $@ 322 | $@ 323 | $@@ 324 | db db$@ 325 | 88 88$@ 326 | 88ooo88$@ 327 | 88~~~88$@ 328 | 88 88$@ 329 | YP YP$@ 330 | $@ 331 | $@@ 332 | d888888b$@ 333 | `88' $@ 334 | 88 $@ 335 | 88 $@ 336 | .88. $@ 337 | Y888888P$@ 338 | $@ 339 | $@@ 340 | d88b$@ 341 | `8P'$@ 342 | 88 $@ 343 | 88 $@ 344 | db. 88 $@ 345 | Y8888P $@ 346 | $@ 347 | $@@ 348 | db dD$@ 349 | 88 ,8P'$@ 350 | 88,8P $@ 351 | 88`8b $@ 352 | 88 `88.$@ 353 | YP YD$@ 354 | $@ 355 | $@@ 356 | db $@ 357 | 88 $@ 358 | 88 $@ 359 | 88 $@ 360 | 88booo.$@ 361 | Y88888P$@ 362 | $@ 363 | $@@ 364 | .88b d88.$@ 365 | 88'YbdP`88$@ 366 | 88 88 88$@ 367 | 88 88 88$@ 368 | 88 88 88$@ 369 | YP YP YP$@ 370 | $@ 371 | $@@ 372 | d8b db$@ 373 | 888o 88$@ 374 | 88V8o 88$@ 375 | 88 V8o88$@ 376 | 88 V888$@ 377 | VP V8P$@ 378 | $@ 379 | $@@ 380 | .d88b. $@ 381 | .8P Y8.$@ 382 | 88 88$@ 383 | 88 88$@ 384 | `8b d8'$@ 385 | `Y88P' $@ 386 | $@ 387 | $@@ 388 | d8888b.$@ 389 | 88 `8D$@ 390 | 88oodD'$@ 391 | 88~~~ $@ 392 | 88 $@ 393 | 88 $@ 394 | $@ 395 | $@@ 396 | .d88b. $@ 397 | .8P Y8.$@ 398 | 88 88$@ 399 | 88 88$@ 400 | `8P d8'$@ 401 | `Y88'Y8$@ 402 | $@ 403 | $@@ 404 | d8888b.$@ 405 | 88 `8D$@ 406 | 88oobY'$@ 407 | 88`8b $@ 408 | 88 `88.$@ 409 | 88 YD$@ 410 | $@ 411 | $@@ 412 | .d8888.$@ 413 | 88' YP$@ 414 | `8bo. $@ 415 | `Y8b.$@ 416 | db 8D$@ 417 | `8888Y'$@ 418 | $@ 419 | $@@ 420 | d888888b$@ 421 | `~~88~~'$@ 422 | 88 $@ 423 | 88 $@ 424 | 88 $@ 425 | YP $@ 426 | $@ 427 | $@@ 428 | db db$@ 429 | 88 88$@ 430 | 88 88$@ 431 | 88 88$@ 432 | 88b d88$@ 433 | ~Y8888P'$@ 434 | $@ 435 | $@@ 436 | db db$@ 437 | 88 88$@ 438 | Y8 8P$@ 439 | `8b d8'$@ 440 | `8bd8' $@ 441 | YP $@ 442 | $@ 443 | $@@ 444 | db d8b db$@ 445 | 88 I8I 88$@ 446 | 88 I8I 88$@ 447 | Y8 I8I 88$@ 448 | `8b d8'8b d8'$@ 449 | `8b8' `8d8' $@ 450 | $@ 451 | $@@ 452 | db db$@ 453 | `8b d8'$@ 454 | `8bd8' $@ 455 | .dPYb. $@ 456 | .8P Y8.$@ 457 | YP YP$@ 458 | $@ 459 | $@@ 460 | db db$@ 461 | `8b d8'$@ 462 | `8bd8' $@ 463 | 88 $@ 464 | 88 $@ 465 | YP $@ 466 | $@ 467 | $@@ 468 | d88888D$@ 469 | YP d8'$@ 470 | d8' $@ 471 | d8' $@ 472 | d8' db$@ 473 | d88888P$@ 474 | $@ 475 | $@@ 476 | d88D$@ 477 | 88 $@ 478 | 88 $@ 479 | 88 $@ 480 | 88 $@ 481 | L88D$@ 482 | $@ 483 | $@@ 484 | Cb $@ 485 | `8b $@ 486 | `8b $@ 487 | `8b $@ 488 | `8b $@ 489 | `8D$@ 490 | $@ 491 | $@@ 492 | C88D$@ 493 | 88$@ 494 | 88$@ 495 | 88$@ 496 | 88$@ 497 | C888$@ 498 | $@ 499 | $@@ 500 | db $@ 501 | .dPVb. $@ 502 | dP' `Vb$@ 503 | $@ 504 | $@ 505 | $@ 506 | $@ 507 | $@@ 508 | $@ 509 | $@ 510 | $@ 511 | $@ 512 | $@ 513 | C88888D$@ 514 | $@ 515 | $@@ 516 | dD$@ 517 | C'$@ 518 | `$@ 519 | $@ 520 | $@ 521 | $@ 522 | $@ 523 | $@@ 524 | .d8b. $@ 525 | d8' `8b$@ 526 | 88ooo88$@ 527 | 88~~~88$@ 528 | 88 88$@ 529 | YP YP$@ 530 | $@ 531 | $@@ 532 | d8888b.$@ 533 | 88 `8D$@ 534 | 88oooY'$@ 535 | 88~~~b.$@ 536 | 88 8D$@ 537 | Y8888P'$@ 538 | $@ 539 | $@@ 540 | .o88b.$@ 541 | d8P Y8$@ 542 | 8P $@ 543 | 8b $@ 544 | Y8b d8$@ 545 | `Y88P'$@ 546 | $@ 547 | $@@ 548 | d8888b.$@ 549 | 88 `8D$@ 550 | 88 88$@ 551 | 88 88$@ 552 | 88 .8D$@ 553 | Y8888D'$@ 554 | $@ 555 | $@@ 556 | d88888b$@ 557 | 88' $@ 558 | 88ooooo$@ 559 | 88~~~~~$@ 560 | 88. $@ 561 | Y88888P$@ 562 | $@ 563 | $@@ 564 | d88888b$@ 565 | 88' $@ 566 | 88ooo $@ 567 | 88~~~ $@ 568 | 88 $@ 569 | YP $@ 570 | $@ 571 | $@@ 572 | d888b $@ 573 | 88' Y8b$@ 574 | 88 $@ 575 | 88 ooo$@ 576 | 88. ~8~$@ 577 | Y888P $@ 578 | $@ 579 | $@@ 580 | db db$@ 581 | 88 88$@ 582 | 88ooo88$@ 583 | 88~~~88$@ 584 | 88 88$@ 585 | YP YP$@ 586 | $@ 587 | $@@ 588 | d888888b$@ 589 | `88' $@ 590 | 88 $@ 591 | 88 $@ 592 | .88. $@ 593 | Y888888P$@ 594 | $@ 595 | $@@ 596 | d88b$@ 597 | `8P'$@ 598 | 88 $@ 599 | 88 $@ 600 | db. 88 $@ 601 | Y8888P $@ 602 | $@ 603 | $@@ 604 | db dD$@ 605 | 88 ,8P'$@ 606 | 88,8P $@ 607 | 88`8b $@ 608 | 88 `88.$@ 609 | YP YD$@ 610 | $@ 611 | $@@ 612 | db $@ 613 | 88 $@ 614 | 88 $@ 615 | 88 $@ 616 | 88booo.$@ 617 | Y88888P$@ 618 | $@ 619 | $@@ 620 | .88b d88.$@ 621 | 88'YbdP`88$@ 622 | 88 88 88$@ 623 | 88 88 88$@ 624 | 88 88 88$@ 625 | YP YP YP$@ 626 | $@ 627 | $@@ 628 | d8b db$@ 629 | 888o 88$@ 630 | 88V8o 88$@ 631 | 88 V8o88$@ 632 | 88 V888$@ 633 | VP V8P$@ 634 | $@ 635 | $@@ 636 | .d88b. $@ 637 | .8P Y8.$@ 638 | 88 88$@ 639 | 88 88$@ 640 | `8b d8'$@ 641 | `Y88P' $@ 642 | $@ 643 | $@@ 644 | d8888b.$@ 645 | 88 `8D$@ 646 | 88oodD'$@ 647 | 88~~~ $@ 648 | 88 $@ 649 | 88 $@ 650 | $@ 651 | $@@ 652 | .d88b. $@ 653 | .8P Y8.$@ 654 | 88 88$@ 655 | 88 88$@ 656 | `8P d8'$@ 657 | `Y88'Y8$@ 658 | $@ 659 | $@@ 660 | d8888b.$@ 661 | 88 `8D$@ 662 | 88oobY'$@ 663 | 88`8b $@ 664 | 88 `88.$@ 665 | 88 YD$@ 666 | $@ 667 | $@@ 668 | .d8888.$@ 669 | 88' YP$@ 670 | `8bo. $@ 671 | `Y8b.$@ 672 | db 8D$@ 673 | `8888Y'$@ 674 | $@ 675 | $@@ 676 | d888888b$@ 677 | `~~88~~'$@ 678 | 88 $@ 679 | 88 $@ 680 | 88 $@ 681 | YP $@ 682 | $@ 683 | $@@ 684 | db db$@ 685 | 88 88$@ 686 | 88 88$@ 687 | 88 88$@ 688 | 88b d88$@ 689 | ~Y8888P'$@ 690 | $@ 691 | $@@ 692 | db db$@ 693 | 88 88$@ 694 | Y8 8P$@ 695 | `8b d8'$@ 696 | `8bd8' $@ 697 | YP $@ 698 | $@ 699 | $@@ 700 | db d8b db$@ 701 | 88 I8I 88$@ 702 | 88 I8I 88$@ 703 | Y8 I8I 88$@ 704 | `8b d8'8b d8'$@ 705 | `8b8' `8d8' $@ 706 | $@ 707 | $@@ 708 | db db$@ 709 | `8b d8'$@ 710 | `8bd8' $@ 711 | .dPYb. $@ 712 | .8P Y8.$@ 713 | YP YP$@ 714 | $@ 715 | $@@ 716 | db db$@ 717 | `8b d8'$@ 718 | `8bd8' $@ 719 | 88 $@ 720 | 88 $@ 721 | YP $@ 722 | $@ 723 | $@@ 724 | d88888D$@ 725 | YP d8'$@ 726 | d8' $@ 727 | d8' $@ 728 | d8' db$@ 729 | d88888P$@ 730 | $@ 731 | $@@ 732 | .8P$@ 733 | 8' $@ 734 | .dP $@ 735 | C88 $@ 736 | `Yb $@ 737 | 8. $@ 738 | `8b$@ 739 | $@@ 740 | 8$@ 741 | 8$@ 742 | 8$@ 743 | $@ 744 | 8$@ 745 | 8$@ 746 | 8$@ 747 | $@@ 748 | V8. $@ 749 | `8 $@ 750 | Vb. $@ 751 | 88D$@ 752 | dP' $@ 753 | .8 $@ 754 | C8' $@ 755 | $@@ 756 | .oo. .$@ 757 | P' `VP'$@ 758 | $@ 759 | $@ 760 | $@ 761 | $@ 762 | $@ 763 | $@@ 764 | @ 765 | @ 766 | @ 767 | @ 768 | @ 769 | @ 770 | @ 771 | @@ 772 | @ 773 | @ 774 | @ 775 | @ 776 | @ 777 | @ 778 | @ 779 | @@ 780 | @ 781 | @ 782 | @ 783 | @ 784 | @ 785 | @ 786 | @ 787 | @@ 788 | @ 789 | @ 790 | @ 791 | @ 792 | @ 793 | @ 794 | @ 795 | @@ 796 | @ 797 | @ 798 | @ 799 | @ 800 | @ 801 | @ 802 | @ 803 | @@ 804 | @ 805 | @ 806 | @ 807 | @ 808 | @ 809 | @ 810 | @ 811 | @@ 812 | @ 813 | @ 814 | @ 815 | @ 816 | @ 817 | @ 818 | @ 819 | @@ 820 | -------------------------------------------------------------------------------- /scripts/git-commit-id.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | echo "const CURRENTCOMMIT* = static: \"$(git rev-parse HEAD)"\"\ > src/catnaplib/global/currentcommit.nim 3 | -------------------------------------------------------------------------------- /scripts/test-commandline-args.sh: -------------------------------------------------------------------------------- 1 | function VerifyTest() { 2 | if [ $? -ne 0 ]; then 3 | exit 1 4 | fi 5 | } 6 | 7 | #### TESTS #### 8 | 9 | # Test Normal run 10 | echo "[!] Testing: Normal Run" 11 | ./../bin/catnap -c ./test_config.toml -a ../config/distros.toml -n 12 | VerifyTest 13 | 14 | # Test help 15 | echo "[!] Testing: Help" 16 | ./../bin/catnap -c ./test_config.toml -a ../config/distros.toml -h -n 17 | VerifyTest 18 | 19 | # Test version 20 | echo "[!] Testing: Version" 21 | ./../bin/catnap -c ./test_config.toml -a ../config/distros.toml -v -n 22 | VerifyTest 23 | 24 | # Test distroid 25 | echo "[!] Testing: DistroId" 26 | ./../bin/catnap -c ./test_config.toml -a ../config/distros.toml -d arch -n 27 | VerifyTest 28 | 29 | # Test grep 30 | echo "[!] Testing: Grep" 31 | ./../bin/catnap -c ./test_config.toml -a ../config/distros.toml -g kernel -n 32 | VerifyTest 33 | 34 | # Test margin 35 | echo "[!] Testing: Margin" 36 | ./../bin/catnap -c ./test_config.toml -a ../config/distros.toml -m 1,2,3 -n 37 | VerifyTest 38 | 39 | # Test layout 40 | echo "[!] Testing: Layout" 41 | ./../bin/catnap -c ./test_config.toml -a ../config/distros.toml -l ArtOnTop -n 42 | VerifyTest 43 | 44 | # Test figletlogos mode 45 | echo "[!] Testing: FigletLogos" 46 | ./../bin/catnap -c ./test_config.toml -a ../config/distros.toml -fe on -n 47 | VerifyTest 48 | 49 | # Test figletlogos margin 50 | echo "[!] Testing: FigletLogos Margin" 51 | ./../bin/catnap -c ./test_config.toml -a ../config/distros.toml -fe on -fm 1,2,3 -n 52 | VerifyTest 53 | 54 | # Test figletlogos font with example figlet font file 55 | echo "[!] Testing: FigletLogos Font" 56 | ./../bin/catnap -c ./test_config.toml -a ../config/distros.toml -fe on -ff basic.flf -n 57 | VerifyTest 58 | 59 | # Test default Config 60 | echo "[!] Testing: Default config" 61 | ./../bin/catnap -c ../config/config.toml -a ../config/distros.toml -n 62 | VerifyTest 63 | 64 | -------------------------------------------------------------------------------- /scripts/test_config.toml: -------------------------------------------------------------------------------- 1 | # THIS IS THE CONFIG FILE USED TO TEST CATNAP! 2 | # PLEASE MAKE SURE, EVERY STAT IS ACTIVATE! 3 | # ALWAYS ADD NEWLY ADDED STATS HERE! 4 | 5 | [stats] 6 | username = {icon = ">", name = "user", color = "(RD)"} 7 | hostname = {icon = ">", name = "hname", color = "(YW)"} 8 | uptime = {icon = ">", name = "uptime", color = "(BE)"} 9 | distro = {icon = ">", name = "distro", color = "(GN)"} 10 | kernel = {icon = ">", name = "kernel", color = "(MA)"} 11 | desktop = {icon = ">", name = "desktp", color = "(CN)"} 12 | terminal = {icon = ">", name = "term", color = "(RD)"} 13 | shell = {icon = ">", name = "shell", color = "(MA)"} 14 | packages = {icon = ">", name = "packages", color = "(GN)"} 15 | weather = {icon = ">", name = "weather", color = "(BE)"} 16 | gpu = {icon = ">", name = "gpu", color = "(MA)"} 17 | cpu = {icon = ">", name = "cpu", color = "(RD)"} 18 | disk_0 = {icon = ">", name = "disk", color = "(GN)"} 19 | memory = {icon = ">", name = "memory", color = "(YW)"} 20 | sep_color = "SEPARATOR" 21 | colors = {icon = "> ", name = "colors", color = "!DT!", symbol = "#"} 22 | 23 | ############################################## 24 | 25 | [misc] 26 | borderstyle = "doubleline" 27 | layout = "Inline" 28 | stats_margin_top = 0 29 | location = "San Francisco" 30 | 31 | [misc.figletLogos] 32 | enable = false 33 | color = "" 34 | font = "slant" 35 | margin = [0, 0, 0,] 36 | 37 | [misc.imageMode] 38 | enable = false 39 | path = "" 40 | scale = 0 41 | margin = [0, 0, 0,] 42 | -------------------------------------------------------------------------------- /src/catnap.nim: -------------------------------------------------------------------------------- 1 | import "catnaplib/platform/fetch" 2 | import "catnaplib/drawing/render" 3 | from "catnaplib/global/definitions" import CONFIGPATH, DISTROSPATH, Config, STATNAMES, CACHEPATH, TEMPPATH 4 | import "catnaplib/global/config" 5 | import "catnaplib/terminal/logging" 6 | import parsetoml 7 | import os 8 | from unicode import toLower 9 | import strutils 10 | import strformat 11 | import std/wordwrap 12 | import "catnaplib/platform/probe" 13 | from "catnaplib/global/currentcommit" import CURRENTCOMMIT 14 | 15 | # Debug code for execution time 16 | when not defined release: 17 | import times 18 | let t0 = epochTime() 19 | 20 | # Help text 21 | proc printHelp(cfg: Config) = 22 | let mounts_len = probe.getMounts().len 23 | var disk_statnames: seq[string] 24 | var count = 0 25 | while count < mounts_len: 26 | disk_statnames.add("disk_" & $count) 27 | count += 1 28 | 29 | echo "Usage:" 30 | echo " catnap [options] [arguments]" 31 | echo "" 32 | echo "Options:" 33 | echo " -h --help Show help list" 34 | echo " -v --version Shows info about the version" 35 | echo " -d --distroid Set which DistroId to use" 36 | echo " -g --grep Get the stats value" 37 | echo " -n --no-cache Clears the cache before execution" 38 | echo " -c --config Uses a custom location for the config file" 39 | echo " -a --art Uses a custom location for the distros file" 40 | echo "" 41 | echo " -m --margin Overwrite margin value for the displayed logo (Example: 1,2,3)" 42 | echo " -l --layout Overwrite layout config value [Inline,ArtOnTop,StatsOnTop]" 43 | echo "" 44 | echo " -fe --figlet-enable Overwrite figletLogos mode" 45 | echo " -fm --figlet-margin Overwrite figletLogos margin (Example: 1,2,3)" 46 | echo " -ff --figlet-font Overwrite figletLogos font" 47 | echo "" 48 | echo "StatNames:" 49 | echo " " & (STATNAMES & @["disks"] & disk_statnames).join(", ").wrapWords(80).replace("\n", "\n ") 50 | echo "" 51 | echo "DistroIds:" 52 | echo " " & cfg.getAllDistros().join(", ").wrapWords(80).replace("\n", "\n ") 53 | echo "" 54 | quit() 55 | 56 | # Handle commandline args 57 | var distroid = "nil" 58 | var statname = "nil" 59 | var figletLogos_enabled = "nil" 60 | var figletLogos_margin: seq[int] 61 | var figletLogos_font = "nil" 62 | var layout = "nil" 63 | var margin: seq[int] 64 | var cfgPath = CONFIGPATH 65 | var dstPath = DISTROSPATH 66 | var help = false 67 | var error = false 68 | 69 | if paramCount() > 0: 70 | var idx = 1 71 | while paramCount() > (idx - 1): 72 | var param = paramStr(idx) 73 | 74 | # Version Argument 75 | if param == "-v" or param == "--version": 76 | echo "Commit " & CURRENTCOMMIT 77 | quit() 78 | 79 | # Config Argument 80 | elif param == "-c" or param == "--config": 81 | if paramCount() - idx < 1: 82 | logError(&"'{param}' - No Value was specified!", false) 83 | error = true 84 | idx += 1 85 | continue 86 | idx += 1 87 | cfgPath = paramStr(idx) 88 | 89 | # Art Argument 90 | elif param == "-a" or param == "--art": 91 | if paramCount() - idx < 1: 92 | logError(&"'{param}' - No Value was specified!", false) 93 | error = true 94 | idx += 1 95 | continue 96 | idx += 1 97 | dstPath = paramStr(idx) 98 | 99 | # Help Argument 100 | elif param == "-h" or param == "--help": 101 | help = true 102 | 103 | # DistroId Argument 104 | elif param == "-d" or param == "--distroid": 105 | if paramCount() - idx < 1: 106 | logError(&"'{param}' - No Value was specified!", false) 107 | error = true 108 | idx += 1 109 | continue 110 | elif distroid != "nil": 111 | logError(&"{param} - Can only be used once!", false) 112 | error = true 113 | idx += 1 114 | continue 115 | elif statname != "nil": 116 | logError(&"{param} - Can't be used together with: -g/--grep", false) 117 | error = true 118 | idx += 1 119 | continue 120 | idx += 1 121 | distroid = paramStr(idx).toLower() 122 | 123 | # No Cache Argument 124 | elif param == "-n" or param == "--no-cache": 125 | if dirExists(CACHEPATH): removeDir(CACHEPATH) 126 | 127 | # Grep Argument 128 | elif param == "-g" or param == "--grep": 129 | if paramCount() - idx < 1: 130 | logError(&"'{param}' - No Value was specified!", false) 131 | error = true 132 | idx += 1 133 | continue 134 | elif statname != "nil": 135 | logError(&"{param} - Can only be used once!", false) 136 | error = true 137 | idx += 1 138 | continue 139 | elif distroid != "nil": 140 | logError(&"{param} - Can't be used together with: -d/--distroid", false) 141 | error = true 142 | idx += 1 143 | continue 144 | idx += 1 145 | statname = paramStr(idx).toLower() 146 | 147 | # Margin Argument 148 | elif param == "-m" or param == "--margin": 149 | if paramCount() - idx < 1: 150 | logError(&"{param} - No Value was specified!", false) 151 | error = true 152 | idx += 1 153 | continue 154 | elif statname != "nil": 155 | logError(&"'{param}' - Can't be used together with: -g/--grep", false) 156 | error = true 157 | idx += 1 158 | continue 159 | elif figletLogos_margin.len > 0: 160 | logError(&"'{param}' - Can only be used once!", false) 161 | error = true 162 | idx += 1 163 | continue 164 | 165 | idx += 1 166 | let margin_list = paramStr(idx).split(",") 167 | if margin_list.len < 3: 168 | logError(&"'{param}' - Value dose not match format!", false) 169 | error = true 170 | idx += 1 171 | continue 172 | 173 | for idx in countup(0, 2): 174 | let num = margin_list[idx].strip() 175 | var parsed_num: int 176 | 177 | try: 178 | parsed_num = parseInt(num) 179 | except: 180 | logError(&"'{param}' - Value[{idx}] is not a number!", false) 181 | error = true 182 | break 183 | 184 | margin.add(parsed_num) 185 | 186 | # Layout Argument 187 | elif param == "-l" or param == "--layout": 188 | if paramCount() - idx < 1: 189 | logError(&"'{param}' - No Value was specified!", false) 190 | error = true 191 | idx += 1 192 | continue 193 | elif statname != "nil": 194 | logError(&"{param} - Can't be used together with: -g/--grep", false) 195 | error = true 196 | idx += 1 197 | continue 198 | elif layout != "nil": 199 | logError(&"{param} - Can only be used once!", false) 200 | error = true 201 | idx += 1 202 | continue 203 | 204 | idx += 1 205 | layout = paramStr(idx) 206 | 207 | # FigletLogos enabled Argument 208 | elif param == "-fe" or param == "--figlet-enabled": 209 | if paramCount() - idx < 1: 210 | logError(&"'{param}' - No Value was specified!", false) 211 | error = true 212 | idx += 1 213 | continue 214 | elif statname != "nil": 215 | logError(&"{param} - Can't be used together with: -g/--grep", false) 216 | error = true 217 | idx += 1 218 | continue 219 | elif figletLogos_enabled != "nil": 220 | logError(&"{param} - Can only be used once!", false) 221 | error = true 222 | idx += 1 223 | continue 224 | 225 | idx += 1 226 | figletLogos_enabled = paramStr(idx).toLower() 227 | if figletLogos_enabled != "on" and figletLogos_enabled != "off": 228 | logError(&"{param} - Value is not 'on' or 'off'!", false) 229 | error = true 230 | idx += 1 231 | continue 232 | 233 | # FigletLogos margin Argument 234 | elif param == "-fm" or param == "--figlet-margin": 235 | if paramCount() - idx < 1: 236 | logError(&"{param} - No Value was specified!", false) 237 | error = true 238 | idx += 1 239 | continue 240 | elif statname != "nil": 241 | logError(&"'{param}' - Can't be used together with: -g/--grep", false) 242 | error = true 243 | idx += 1 244 | continue 245 | elif figletLogos_margin.len > 0: 246 | logError(&"'{param}' - Can only be used once!", false) 247 | error = true 248 | idx += 1 249 | continue 250 | 251 | idx += 1 252 | let margin_list = paramStr(idx).split(",") 253 | if margin_list.len < 3: 254 | logError(&"'{param}' - Value dose not match format!", false) 255 | error = true 256 | idx += 1 257 | continue 258 | 259 | for idx in countup(0, 2): 260 | let num = margin_list[idx].strip() 261 | var parsed_num: int 262 | 263 | try: 264 | parsed_num = parseInt(num) 265 | except: 266 | logError(&"'{param}' - Value[{idx}] is not a number!", false) 267 | error = true 268 | break 269 | 270 | figletLogos_margin.add(parsed_num) 271 | 272 | 273 | # FigletLogos font Argument 274 | elif param == "-ff" or param == "--figlet-font": 275 | if paramCount() - idx < 1: 276 | logError(&"'{param}' - No Value was specified!", false) 277 | error = true 278 | idx += 1 279 | continue 280 | elif statname != "nil": 281 | logError(&"'{param}' - Can't be used together with: -g/--grep", false) 282 | error = true 283 | idx += 1 284 | continue 285 | elif figletLogos_font != "nil": 286 | logError(&"'{param}' - Can only be used once!", false) 287 | error = true 288 | idx += 1 289 | continue 290 | 291 | idx += 1 292 | figletLogos_font = paramStr(idx) 293 | 294 | # Unknown Argument 295 | else: 296 | logError(&"Unknown option '{param}'!", false) 297 | error = true 298 | idx += 1 299 | continue 300 | 301 | idx += 1 302 | 303 | 304 | # Create tmp folder 305 | if not dirExists(CACHEPATH): 306 | createDir(CACHEPATH) 307 | 308 | if not dirExists(TEMPPATH): 309 | createDir(TEMPPATH) 310 | 311 | # Getting config 312 | var cfg = LoadConfig(cfgPath, dstPath) 313 | 314 | # Handle argument errors and help 315 | if help: printHelp(cfg) 316 | if error: quit(1) 317 | elif help: quit(0) 318 | 319 | if statname == "nil": 320 | # Handle margin overwrite 321 | if margin.len == 3: 322 | for key in cfg.distroart.keys: 323 | cfg.distroart[key].margin = [margin[0], margin[1], margin[2]] 324 | 325 | # Handle layout overwrite 326 | if layout != "nil": 327 | cfg.misc["layout"] = parseString(&"val = '{layout}'")["val"] 328 | 329 | # Handle figletLogos overwrites 330 | if figletLogos_enabled != "nil": 331 | let onoff = if figletLogos_enabled == "on": "true" else: "false" 332 | cfg.misc["figletLogos"]["enable"] = parseString(&"val = {onoff}")["val"] 333 | if figletLogos_margin.len == 3: 334 | let fmargin = &"[{figletLogos_margin[0]},{figletLogos_margin[1]},{figletLogos_margin[2]},]" 335 | cfg.misc["figletLogos"]["margin"] = parseString(&"val = {fmargin}")["val"] 336 | if figletLogos_font != "nil": 337 | cfg.misc["figletLogos"]["font"] = parseString(&"val = '{figletLogos_font}'")["val"] 338 | 339 | # Get system info 340 | let fetchinfo = fetchSystemInfo(cfg, distroid) 341 | 342 | # Render system info 343 | echo "" 344 | Render(cfg, fetchinfo) 345 | echo "" 346 | 347 | else: 348 | if statname == "disks": 349 | var count = 0 350 | for p in probe.getMounts(): 351 | echo "disk_" & $count & ": " & p 352 | count += 1 353 | quit() 354 | else: 355 | let fetchinfo = fetchSystemInfo(cfg) 356 | 357 | if not fetchinfo.list.contains(statname): 358 | logError(&"Unknown StatName '{statname}'!") 359 | 360 | echo fetchinfo.list[statname]() 361 | 362 | # Debug code for execution time 363 | when not defined release: 364 | let time = (epochTime() - t0).formatFloat(format = ffDecimal, precision = 3) 365 | echo &"Execution finished in {time}s" 366 | -------------------------------------------------------------------------------- /src/catnaplib/drawing/render.nim: -------------------------------------------------------------------------------- 1 | import strformat 2 | import parsetoml 3 | import osproc 4 | import terminal 5 | import math 6 | 7 | import "../terminal/logging" 8 | from "../global/definitions" import FetchInfo, Stats, Stat, Config, STATNAMES 9 | import "../terminal/colors" 10 | import "../generation/utils" 11 | import "../generation/stats" 12 | 13 | proc getStat(stats: TomlValueRef, key: string): TomlValueRef = 14 | # Returns the value of stats[key] if it exists, else returns nil 15 | if stats.contains(key): 16 | return stats[key] 17 | return nil 18 | 19 | proc Render*(config: Config, fetchinfo: FetchInfo) = 20 | # Function that Renders a FetchInfo object to the console 21 | 22 | # Define Margins 23 | let 24 | margin_top = fetchinfo.logo.margin[0] 25 | margin_left = fetchinfo.logo.margin[1] 26 | margin_right = fetchinfo.logo.margin[2] 27 | 28 | # Load Config 29 | let layout = config.misc["layout"].getStr() 30 | 31 | # Build distro_art buffer 32 | var distro_art: seq[string] 33 | 34 | # Fill distro_art buffer with fetchinfo.logo.art 35 | for idx in countup(0, fetchinfo.logo.art.len - 1): 36 | distro_art.add(" ".repeat(margin_left) & Colorize(fetchinfo.logo.art[idx]) & colors.Default & " ".repeat(margin_right)) 37 | 38 | # Add margin_top lines ontop of the distro_art 39 | if margin_top > 0: 40 | var l = distro_art[0].reallen - 1 41 | for _ in countup(1, margin_top): 42 | distro_art = " ".repeat(l) & distro_art 43 | 44 | # Build stat_block buffer 45 | var stats: Stats = newStats() 46 | 47 | for stat_name in STATNAMES & fetchinfo.disk_statnames: 48 | stats.setStat(stat_name, config.stats.getStat(stat_name)) 49 | 50 | # Get ordered statnames list 51 | var keys: seq[string] 52 | for k in config.stats.getTable().keys: 53 | keys.add(k) 54 | 55 | var delta: seq[string] 56 | for stat in STATNAMES: 57 | if stat notin keys: 58 | delta.add(stat) 59 | 60 | let ORDERED_STATNAMES = keys & delta 61 | 62 | var stats_margin_top = 0 63 | if config.misc.contains("stats_margin_top"): 64 | stats_margin_top = config.misc["stats_margin_top"].getInt() 65 | 66 | # Build the stat_block buffer 67 | var stats_block = buildStatBlock(ORDERED_STATNAMES, stats, config, fetchinfo, stats_margin_top) 68 | 69 | # Merge buffers and output 70 | case layout: 71 | of "Inline": # Handle Inline Layout 72 | if not config.misc["imageMode"]["enable"].getBool(): 73 | # ASCII mode 74 | let lendiv = stats_block.len - distro_art.len 75 | if lendiv < 0: 76 | for _ in countup(1, lendiv - lendiv*2): 77 | stats_block.add(" ") 78 | elif lendiv > 0: 79 | for _ in countup(1, lendiv): 80 | distro_art.add(" ".repeat(distro_art[0].reallen - 1)) 81 | 82 | for idx in countup(0, distro_art.len - 1): 83 | echo distro_art[idx] & stats_block[idx] 84 | else: 85 | # Image mode 86 | let 87 | scale = config.misc["imageMode"]["scale"].getInt() 88 | path = config.misc["imageMode"]["path"].getStr() 89 | tmargin = config.misc["imageMode"]["margin"] 90 | margin = [tmargin[0].getInt(), tmargin[1].getInt(), tmargin[2].getInt()] 91 | height = round(scale / 2).int + margin[0] 92 | cmd = &"viu '{path}' -w {scale} -x {margin[1]}" 93 | 94 | for _ in countup(0, margin[0]): 95 | echo "" 96 | 97 | # Display image 98 | if execCmd(cmd) != 0: 99 | logError(&"\"{cmd}\" - Non ZERO exit code!") 100 | 101 | cursorUp(height) 102 | 103 | # Display Stats 104 | for idx in countup(0, stats_block.len - 1): 105 | echo " ".repeat(margin[1] + scale + margin[2]) & stats_block[idx] 106 | 107 | cursorDown(height - stats_block.len) 108 | 109 | 110 | of "ArtOnTop": # Handle ArtOnTop Layout 111 | if not config.misc["imageMode"]["enable"].getBool(): 112 | # ASCII mode 113 | for idx in countup(0, distro_art.len - 1): 114 | echo distro_art[idx] 115 | for idx in countup(0, stats_block.len - 1): 116 | echo stats_block[idx] 117 | else: 118 | # Image mode 119 | let 120 | scale = config.misc["imageMode"]["scale"].getInt() 121 | path = config.misc["imageMode"]["path"].getStr() 122 | tmargin = config.misc["imageMode"]["margin"] 123 | margin = [tmargin[0].getInt(), tmargin[1].getInt(), tmargin[2].getInt()] 124 | 125 | cmd = &"viu '{path}' -w {scale} -x {margin[1]}" 126 | 127 | # Display image 128 | if execCmd(cmd) != 0: 129 | logError(&"\"{cmd}\" - Non ZERO exit code!") 130 | 131 | for _ in countup(0, margin[0]): 132 | echo "" 133 | 134 | # Display Stats 135 | for idx in countup(0, stats_block.len - 1): 136 | echo stats_block[idx] 137 | 138 | of "StatsOnTop": # Handle StatsOnTop Layout 139 | if not config.misc["imageMode"]["enable"].getBool(): 140 | # ASCII mode 141 | for idx in countup(0, stats_block.len - 1): 142 | echo stats_block[idx] 143 | for idx in countup(0, distro_art.len - 1): 144 | echo distro_art[idx] 145 | else: 146 | # Image mode 147 | let 148 | scale = config.misc["imageMode"]["scale"].getInt() 149 | path = config.misc["imageMode"]["path"].getStr() 150 | tmargin = config.misc["imageMode"]["margin"] 151 | margin = [tmargin[0].getInt(), tmargin[1].getInt(), tmargin[2].getInt()] 152 | 153 | cmd = &"viu '{path}' -w {scale} -x {margin[1]}" 154 | 155 | # Display Stats 156 | for idx in countup(0, stats_block.len - 1): 157 | echo stats_block[idx] 158 | 159 | for _ in countup(0, margin[0]): 160 | echo "" 161 | 162 | # Display image 163 | if execCmd(cmd) != 0: 164 | logError(&"\"{cmd}\" - Non ZERO exit code!") 165 | 166 | else: # Invalid Layout 167 | logError(&"{config.configFile}:misc:layout - Invalid value") 168 | quit(1) 169 | -------------------------------------------------------------------------------- /src/catnaplib/generation/stats.nim: -------------------------------------------------------------------------------- 1 | from "../global/definitions" import Stats, Stat, Color, STATNAMES 2 | 3 | import parsetoml 4 | import unicode 5 | import tables 6 | 7 | proc newStat*(icon: string, name: string, color: Color): Stat = 8 | # Create a new Stat object 9 | result.icon = icon 10 | result.name = name 11 | result.color = color 12 | 13 | proc newStats*(): Stats = 14 | # Create a new Stanamets object 15 | result.maxlen = 0 16 | for name in STATNAMES: 17 | result.list[name] = newStat("", "", "") 18 | 19 | proc setStat*(stats: var Stats, stat_name: string, rawstat: TomlValueRef) = 20 | # Function that generates a Stat object an parses it to the related stats field 21 | if rawstat != nil: # Set to empty stat 22 | # Merge icon with stat name and color 23 | let l = uint(unicode.runeLen(rawstat["icon"].getStr()) + unicode.runeLen(rawstat["name"].getStr()) + 1) 24 | if l > stats.maxlen: 25 | stats.maxlen = l 26 | stats.list[stat_name] = newStat(rawstat["icon"].getStr(), rawstat["name"].getStr(), rawstat["color"].getStr()) 27 | if stat_name == "colors": stats.color_symbol = rawstat["symbol"].getStr() 28 | -------------------------------------------------------------------------------- /src/catnaplib/generation/utils.nim: -------------------------------------------------------------------------------- 1 | import unicode 2 | import tables 3 | import strutils 4 | import strformat 5 | import parsetoml 6 | 7 | from "../global/definitions" import Stats, Stat, Config, FetchInfo, STATNAMES 8 | from "stats" import newStat 9 | import "../terminal/colors" 10 | import "../terminal/logging" 11 | 12 | proc repeat*(s: string, i: int): string = 13 | # Repeats a string 's', 'i' times 14 | for _ in countup(0, i): 15 | result &= s 16 | 17 | proc reallen*(s: string): int = 18 | # Get the length of a string without ansi color codes 19 | result = Uncolorize(s).runeLen 20 | 21 | proc buildStatBlock*(stat_names: seq[string], stats: Stats, config: Config, fi: FetchInfo, margin_top: int): seq[string] = 22 | # Build output lines from Stats object and FetchInfo object 23 | 24 | var sb: seq[string] 25 | 26 | for idx in countup(1, margin_top): 27 | sb.add("") 28 | 29 | var borderstyle = "" 30 | if config.misc.contains("borderstyle"): 31 | borderstyle = config.misc["borderstyle"].getStr() 32 | 33 | proc addStat(stat: Stat, value: string) = 34 | # Function to add a stat/value pair to the result 35 | var line = stat.icon & " " & stat.name 36 | while uint(line.runeLen) < stats.maxlen: 37 | line &= " " 38 | case borderstyle: 39 | of "line": 40 | sb.add("│ " & stat.color.Colorize() & line & colors.Default & " │ " & value) 41 | of "dashed": 42 | sb.add("┊ " & stat.color.Colorize() & line & colors.Default & " ┊ " & value) 43 | of "dotted": 44 | sb.add("┇ " & stat.color.Colorize() & line & colors.Default & " ┇ " & value) 45 | of "noborder": 46 | sb.add(" " & stat.color.Colorize() & line & colors.Default & " " & value) 47 | of "doubleline": 48 | sb.add("║ " & stat.color.Colorize() & line & colors.Default & " ║ " & value) 49 | else: # Invalid borderstyle 50 | logError(&"{config.configFile}:misc:borderstyle - Invalid style") 51 | quit(1) 52 | 53 | let colorval = Colorize( # Construct color showcase 54 | " (WE)"&stats.color_symbol& 55 | " (RD)"&stats.color_symbol& 56 | " (YW)"&stats.color_symbol& 57 | " (GN)"&stats.color_symbol& 58 | " (CN)"&stats.color_symbol& 59 | " (BE)"&stats.color_symbol& 60 | " (MA)"&stats.color_symbol& 61 | " (BK)"&stats.color_symbol& 62 | "!DT!" 63 | ) 64 | 65 | let NIL_STAT = newStat("", "", "") # Define empty Stat object 66 | 67 | # Construct the stats section with all stats that are not NIL 68 | case borderstyle: 69 | of "line": 70 | sb.add("╭" & "─".repeat(int(stats.maxlen + 1)) & "╮") 71 | of "dashed": 72 | sb.add("╭" & "┄".repeat(int(stats.maxlen + 1)) & "╮") 73 | of "dotted": 74 | sb.add("•" & "•".repeat(int(stats.maxlen + 1)) & "•") 75 | of "noborder": 76 | sb.add(" " & " ".repeat(int(stats.maxlen + 1)) & " ") 77 | of "doubleline": 78 | sb.add("╔" & "═".repeat(int(stats.maxlen + 1)) & "╗") 79 | var fetchinfolist_keys: seq[string] 80 | for k in fi.list.keys: 81 | fetchinfolist_keys.add(k) 82 | 83 | for stat in stat_names: 84 | if stat == "colors": continue 85 | 86 | if stat.split('_')[0] == "sep": 87 | case borderstyle: 88 | of "line": 89 | sb.add("├" & "─".repeat(int(stats.maxlen + 1)) & "┤") 90 | of "dashed": 91 | sb.add("┊" & "┄".repeat(int(stats.maxlen + 1)) & "┊") 92 | of "dotted": 93 | sb.add("┇" & "•".repeat(int(stats.maxlen + 1)) & "┇") 94 | of "noborder": 95 | sb.add(" " & " ".repeat(int(stats.maxlen + 1)) & " ") 96 | of "doubleline": 97 | sb.add("╠" & "═".repeat(int(stats.maxlen + 1)) & "╣") 98 | continue 99 | 100 | if stat notin fetchinfolist_keys: 101 | logError(&"Unknown StatName '{stat}'!") 102 | 103 | if stats.list[stat] != NIL_STAT: 104 | addStat(stats.list[stat], fi.list[stat]()) 105 | 106 | # Color stat 107 | if stats.list["colors"] != NIL_STAT: 108 | addStat(stats.list["colors"], colorval) 109 | case borderstyle: 110 | of "line": 111 | sb.add("╰" & "─".repeat(int(stats.maxlen + 1)) & "╯") 112 | of "dashed": 113 | sb.add("╰" & "┄".repeat(int(stats.maxlen + 1)) & "╯") 114 | of "dotted": 115 | sb.add("•" & "•".repeat(int(stats.maxlen + 1)) & "•") 116 | of "noborder": 117 | sb.add(" " & " ".repeat(int(stats.maxlen + 1)) & " ") 118 | of "doubleline": 119 | sb.add("╚" & "═".repeat(int(stats.maxlen + 1)) & "╝") 120 | return sb 121 | -------------------------------------------------------------------------------- /src/catnaplib/global/config.nim: -------------------------------------------------------------------------------- 1 | import "../terminal/logging" 2 | from "definitions" import Config, STATNAMES, STATKEYS, Logo, DISTROSPATH 3 | from os import fileExists, getEnv, existsEnv 4 | import strformat 5 | import strutils 6 | import parsetoml 7 | 8 | # Chars that a alias can contain 9 | const ALLOWED_NAME_CHARS = {'A' .. 'Z', 'a' .. 'z', '0' .. '9', '_'} 10 | const VALID_TERMS = @["mlterm","yaft-256color","foot","foot-extra","st-256color","xterm","xterm-256color", "alacritty"] 11 | 12 | proc isImageTerm(): bool = 13 | # Returns true if terminal supports image mode 14 | var term = "" 15 | if getEnv("TERM_PROGRAM") != "": 16 | term = getEnv("TERM_PROGRAM") 17 | else: 18 | term = getEnv("TERM") 19 | return (term in VALID_TERMS or "iTerm" in term or "WezTerm" in term or "mintty" in term or "kitty" in term) 20 | 21 | proc LoadConfig*(cfgPath: string, dstPath: string): Config = 22 | # Lads a config file and validates it 23 | 24 | # Validate the config file 25 | if not fileExists(cfgPath): 26 | logError(&"{cfgPath} - file not found!") 27 | 28 | # Validate the art file 29 | if not fileExists(dstPath): 30 | logError(&"{dstPath} - file not found!") 31 | 32 | let tcfg = parsetoml.parseFile(cfgPath) 33 | let tdistros = parsetoml.parseFile(dstPath) 34 | 35 | # Error out if stats missing 36 | if not tcfg.contains("stats"): 37 | logError(&"{cfgPath} - missing 'stats'!") 38 | 39 | for statname in STATNAMES: 40 | if tcfg["stats"].contains(statname): 41 | for statkey in STATKEYS: 42 | if not tcfg["stats"][statname].contains(statkey): 43 | logError(&"{cfgPath}:stats:{statname} - missing '{statkey}'!") 44 | if tcfg["stats"].contains("colors"): 45 | for statkey in STATKEYS & @["symbol"]: 46 | if not tcfg["stats"]["colors"].contains(statkey): 47 | logError(&"{cfgPath}:stats:colors - missing '{statkey}'!") 48 | 49 | if not tcfg.contains("misc"): 50 | logError(&"{cfgPath} - missing 'stats'!") 51 | if not tcfg["misc"].contains("layout"): 52 | logError(&"{cfgPath}:misc - missing 'layout'!") 53 | if not tcfg["misc"].contains("figletLogos"): 54 | logError(&"{cfgPath}:misc - missing 'figletLogos'!") 55 | if not tcfg["misc"]["figletLogos"].contains("enable"): 56 | logError(&"{cfgPath}:misc:figletLogos - missing 'enable'!") 57 | if not tcfg["misc"]["figletLogos"].contains("color"): 58 | logError(&"{cfgPath}:misc:figletLogos - missing 'color'!") 59 | if not tcfg["misc"]["figletLogos"].contains("font"): 60 | logError(&"{cfgPath}:misc:figletLogos - missing 'font'!") 61 | if not tcfg["misc"]["figletLogos"].contains("margin"): 62 | logError(&"{cfgPath}:misc:figletLogos - missing 'margin'!") 63 | if not tcfg["misc"].contains("imageMode"): 64 | logError(&"{cfgPath}:misc - missing 'imageMode'!") 65 | if not tcfg["misc"]["imageMode"].contains("enable"): 66 | logError(&"{cfgPath}:misc:imageMode - missing 'enable'!") 67 | if not tcfg["misc"]["imageMode"].contains("path"): 68 | logError(&"{cfgPath}:misc:imageMode - missing 'path'!") 69 | if not tcfg["misc"]["imageMode"].contains("scale"): 70 | logError(&"{cfgPath}:misc:imageMode - missing 'scale'!") 71 | if not tcfg["misc"]["imageMode"].contains("margin"): 72 | logError(&"{cfgPath}:misc:imageMode - missing 'font'!") 73 | 74 | if tcfg["misc"]["figletLogos"]["enable"].getBool() and tcfg["misc"]["imageMode"]["enable"].getBool(): 75 | logError(&"{cfgPath}:misc - 'figletLogos' and 'imageMode' can't be used together!") 76 | 77 | # Check if terminal supports image mode 78 | if tcfg["misc"]["imageMode"]["enable"].getBool() and not isImageTerm(): 79 | tcfg["misc"]["imageMode"]["enable"] = parsetoml.parseString(&"val = false")["val"] 80 | 81 | # Fill out the result object 82 | result.configFile = cfgPath 83 | result.distrosFile = dstPath 84 | 85 | for distro in tdistros.getTable().keys: 86 | # Validate distroart objects 87 | if not tdistros[distro].contains("margin"): 88 | logError(&"{dstPath}:{distro} - missing 'margin'!") 89 | 90 | var tmargin = tdistros[distro]["margin"].getElems 91 | if tmargin.len() < 3: 92 | var delta = 3 - tmargin.len() 93 | var s = (if delta > 1: "s" else: "") 94 | logError(&"{dstPath}:{distro}:margin - missing {delta} value{s}!") 95 | 96 | if tmargin.len() > 3: 97 | var delta = tmargin.len() - 3 98 | var s = (if delta > 1: "s" else: "") 99 | logError(&"{dstPath}:{distro}:margin - overflows by {delta} value{s}!") 100 | 101 | if not tdistros[distro].contains("art"): 102 | logError(&"{dstPath}:{distro} - missing 'art'!") 103 | 104 | var tart = tdistros[distro]["art"].getElems 105 | if tart.len() < 1: 106 | logError(&"{dstPath}:{distro}:art - is empty!") 107 | 108 | # Generate Logo Objects 109 | var newLogo: Logo 110 | newLogo.margin = [tmargin[0].getInt(), tmargin[1].getInt(), tmargin[2].getInt()] 111 | for line in tart: 112 | newLogo.art.add(line.getStr()) 113 | 114 | # Inflate distroart table with alias if exists 115 | if tdistros[distro].contains("alias"): 116 | let raw_alias_list = tdistros[distro]["alias"].getStr().split(",") 117 | var alias_list: seq[string] 118 | for alias in raw_alias_list: 119 | alias_list.add(alias.strip()) 120 | 121 | newLogo.isAlias = true 122 | 123 | for name in alias_list: 124 | if result.distroart.hasKey(name) or name == distro: 125 | logError(&"{dstPath}:{distro} - alias '{name}' is already taken!") 126 | 127 | for c in name: # Check if name is a valid alias 128 | if not (c in ALLOWED_NAME_CHARS): 129 | logError(&"{dstPath}:{distro} - '{name}' is not a valid alias!") 130 | 131 | result.distroart[name] = newLogo # Add alias to result 132 | 133 | newLogo.isAlias = false 134 | 135 | result.distroart[distro] = newLogo # Add distroart obj to result 136 | 137 | if not result.distroart.contains("default"): # Validate that default alias exists 138 | logError(&"{dstPath} - missing 'default'!") 139 | 140 | result.stats = tcfg["stats"] 141 | result.misc = tcfg["misc"] 142 | 143 | proc getAllDistros*(cfg: Config): seq[string] = 144 | # Function that returns all keys of distroart Table 145 | let distroart = cfg.distroart 146 | for k in distroart.keys: 147 | if not distroart[k].isAlias: 148 | result.add(k) 149 | -------------------------------------------------------------------------------- /src/catnaplib/global/definitions.nim: -------------------------------------------------------------------------------- 1 | import parsetoml 2 | import os 3 | import tables 4 | 5 | # Define all stats here 6 | type 7 | Color* = string 8 | 9 | ColorSet* = object 10 | Black*: Color 11 | Red*: Color 12 | Green*: Color 13 | Yellow*: Color 14 | Blue*: Color 15 | Magenta*: Color 16 | Cyan*: Color 17 | White*: Color 18 | 19 | DistroId* = object 20 | id*: string 21 | like*: string 22 | 23 | Margin = array[3, int] 24 | 25 | Logo* = object 26 | margin*: Margin 27 | art*: seq[string] 28 | isAlias*: bool 29 | 30 | Stat* = object 31 | icon*: string 32 | name*: string 33 | color*: Color 34 | 35 | Stats* = object 36 | maxlen*: uint 37 | list*: Table[string, Stat] 38 | color_symbol*: string 39 | 40 | FetchInfo* = object 41 | list*: Table[string, proc(): string] 42 | disk_statnames*: seq[string] 43 | distroId*: DistroId 44 | logo*: Logo 45 | 46 | Config* = object 47 | configFile*: string 48 | distrosFile*: string 49 | stats*: TomlValueRef 50 | distroart*: OrderedTable[string, Logo] 51 | misc*: TomlValueRef 52 | 53 | const 54 | 55 | # Stats 56 | STATNAMES* = static: @["username", "hostname", "uptime", "distro", 57 | "kernel", "desktop", "shell", "memory", "battery", "terminal", 58 | "cpu", "gpu", "packages", "weather", "colors"] 59 | STATKEYS* = static: @["icon", "name", "color"] 60 | 61 | # Pkg Manager 62 | PKGMANAGERS* = static: { 63 | "gentoo": "emerge", 64 | "fedora": "dnf", 65 | "mageria": "dnf", 66 | "nobara": "dnf", 67 | "redhat": "yum", 68 | "centos": "yum", 69 | "amogos": "apt", 70 | "ubuntu": "apt", 71 | "debian": "apt", 72 | "deepin": "apt", 73 | "devuan": "apt", 74 | "neon": "apt", 75 | "voyager": "apt", 76 | "elementary": "apt", 77 | "kali": "apt", 78 | "lite": "apt", 79 | "mint": "apt", 80 | "mx": "apt", 81 | "pop": "apt", 82 | "pureos": "apt", 83 | "android": "apt", 84 | "raspbian": "apt", 85 | "zorin": "apt", 86 | "opensuse": "zypper", 87 | "opensuse-tumbleweed": "zypper", 88 | "rocky": "zypper", 89 | "arch": "pacman", 90 | "archbang": "pacman", 91 | "archcraft": "pacman", 92 | "arco": "pacman", 93 | "artix": "pacman", 94 | "cachy": "pacman", 95 | "crystal": "pacman", 96 | "instant": "pacman", 97 | "manjaro": "pacman", 98 | "endavour": "pacman", 99 | "hyperbola": "pacman", 100 | "parabola": "pacman", 101 | "reborn": "pacman", 102 | "xero": "pacman", 103 | "alpine": "apk", 104 | "postmarketos": "apk", 105 | "evolution": "xbps", 106 | "void": "xbps", 107 | "nixos": "nix", 108 | "crux": "pkgutils", 109 | "guix": "guix", 110 | "slackware": "slpkg", 111 | "solus": "eopkg", 112 | "sourcemage": "sorcery", 113 | "vanilla": "apx", 114 | "venom": "scratchpkg", 115 | "freebsd": "pkg", 116 | "dragonfly": "pkg", 117 | "netbsd": "pkgsrc", 118 | "openbsd": "pkgsrc", 119 | "macos": "homebrew", 120 | }.toOrderedTable 121 | PKGCOUNTCOMMANDS* = static: { 122 | "apx": "apx list -i | wc -l", 123 | "eopkg": "eopkg list-installed | wc -l", 124 | "scratchpkg": "scratch installed | wc -l", 125 | "sorcery": "gaze installed | wc -l", 126 | "slpkg": "ls /var/log/packages | wc -l", 127 | "guix": "guix package --list-installed | wc -l", 128 | "pkgutils": "pkginfo -i | wc -l", 129 | "nix": "nix-env --query --installed | wc -l", 130 | "xbps": "xbps-query -l | wc -l", 131 | "emerge": "equery list '*' | wc -l", 132 | "dnf": "dnf list installed | wc -l", 133 | "yum": "yum list installed | wc -l", 134 | "apt": "dpkg-query -l | grep '^ii' | wc -l", 135 | "zypper": "rpm -qa --last | wc --l", 136 | "pacman": "pacman -Q | wc -l", 137 | "apk": "apk list --installed | wc -l", 138 | "pkg": "pkg info", 139 | "pkgsrc": "pkg_info", 140 | "homebrew": "brew list | wc -l", 141 | }.toOrderedTable 142 | 143 | # Files / Dirs 144 | CONFIGPATH* = static: joinPath(getConfigDir(), "catnap/config.toml") 145 | DISTROSPATH* = static: joinPath(getConfigDir(), "catnap/distros.toml") 146 | 147 | 148 | proc getCachePath*(): string = 149 | result = getEnv("XDG_CACHE_HOME") 150 | if result != "": 151 | result = joinPath(result, "catnap") 152 | else: 153 | result = getEnv("HOME") 154 | if result != "": 155 | result = joinPath(result, ".cache/catnap") 156 | else: 157 | result = "/tmp/catnap" 158 | 159 | let CACHEPATH* = getCachePath() 160 | let TEMPPATH* = "/tmp/catnap" 161 | 162 | proc toCachePath*(p: string): string = 163 | # Converts a path [p] into a cahce path 164 | return joinPath(CACHEPATH, p) 165 | 166 | proc toTmpPath*(p: string): string = 167 | # Converts a path [p] into a temp path 168 | return joinPath(TEMPPATH, p) 169 | -------------------------------------------------------------------------------- /src/catnaplib/platform/caching.nim: -------------------------------------------------------------------------------- 1 | import times 2 | import os 3 | import strformat 4 | import strutils 5 | 6 | const datetimeFormat = static: "YYYY-MM-dd HH:mm:ss" 7 | let INFINITEDURATION* = initDuration( 8 | nanoseconds = 4294967295, 9 | microseconds = 4294967295, 10 | milliseconds = 4294967295, 11 | seconds = 4294967295, 12 | minutes = 4294967295, 13 | hours = 4294967295, 14 | days = 4294967295, 15 | weeks = 4294967295) 16 | 17 | proc writeCache*(filename: string, content: string, dur: Duration) = 18 | if fileExists(filename): removeFile(filename) 19 | 20 | var expiration: string 21 | if dur == INFINITEDURATION: 22 | expiration = "NEVER" 23 | else: 24 | expiration = $(now() + dur).format(datetimeFormat) 25 | 26 | var filecontent = &"[CONTENT]\n{content}\n[EXPIRATION]\n{expiration}" 27 | writeFile(filename, filecontent) 28 | 29 | proc readCache*(filename: string, default: string = ""): string = 30 | if not fileExists(filename): return default 31 | 32 | var filecontent = readFile(filename).strip() 33 | let lines = filecontent.split('\n') 34 | 35 | var content: seq[string] 36 | var expiration: DateTime = now() 37 | var partindex = 0 38 | for line in lines: 39 | if line == "[CONTENT]" and partindex == 0: 40 | partindex += 1 41 | continue 42 | elif line == "[EXPIRATION]" and partindex == 1: 43 | partindex += 1 44 | continue 45 | 46 | if partindex == 1: 47 | content.add(line) 48 | elif partindex == 2: 49 | if line == "NEVER": expiration = (now() + 1.hours) 50 | else: expiration = parse(line, datetimeFormat) 51 | break 52 | 53 | if now() >= expiration: return default 54 | return content.join("\n") 55 | -------------------------------------------------------------------------------- /src/catnaplib/platform/fetch.nim: -------------------------------------------------------------------------------- 1 | import osproc 2 | import strformat 3 | import strutils 4 | import sugar 5 | 6 | from "../global/definitions" import FetchInfo, Config, toTmpPath 7 | import "../terminal/logging" 8 | import parsetoml 9 | import "probe" 10 | 11 | proc fetchSystemInfo*(config: Config, distroId: string = "nil"): FetchInfo = 12 | result.distroId = probe.getDistroId() 13 | result.list["username"] = proc(): string = return probe.getUser() 14 | result.list["hostname"] = proc(): string = return probe.getHostname() 15 | result.list["distro"] = proc(): string = return probe.getDistro() 16 | result.list["uptime"] = proc(): string = return probe.getUptime() 17 | result.list["kernel"] = proc(): string = return probe.getKernel() 18 | result.list["desktop"] = proc(): string = return probe.getDesktop() 19 | result.list["terminal"] = proc(): string = return probe.getTerminal() 20 | result.list["shell"] = proc(): string = return probe.getShell() 21 | result.list["memory"] = proc(): string = return probe.getMemory() 22 | result.list["battery"] = proc(): string = return probe.getBattery() 23 | result.list["cpu"] = proc(): string = return probe.getCpu() 24 | result.list["gpu"] = proc(): string = return probe.getGpu() 25 | result.list["packages"] = proc(): string = return probe.getPackages() 26 | result.list["weather"] = proc(): string = return probe.getWeather(config) 27 | if defined(linux): 28 | # Add a disk stat for all mounts 29 | let mounts: seq[string] = probe.getMounts() 30 | 31 | if mounts.len > 1: 32 | var index = 0 33 | for mount in mounts: 34 | let name = "disk_" & $index 35 | 36 | # Capture mount var to prevent value changes 37 | var cap: proc(): string 38 | capture mount: 39 | # Create fetch proc 40 | cap = proc(): string = return probe.getDisk(mount) 41 | 42 | result.list[name] = cap 43 | result.disk_statnames.add(name) 44 | index += 1 45 | else: 46 | result.list["disk_0"] = proc(): string = return probe.getDisk(mounts[0]) 47 | result.disk_statnames.add("disk_0") 48 | else: 49 | #TODO: add macos support 50 | result.list["disk_0"] = proc(): string = "" 51 | result.disk_statnames.add("disk_0") 52 | 53 | var distroId = (if distroId != "nil": distroId else: result.distroId.id) 54 | let figletLogos = config.misc["figletLogos"] 55 | 56 | if not figletLogos["enable"].getBool(): # Get logo from config file 57 | if not config.distroart.contains(distroId): 58 | distroId = result.distroId.like 59 | if not config.distroart.contains(distroId): 60 | distroId = "default" 61 | 62 | result.logo = config.distroart[distroId] 63 | 64 | else: # Generate logo using figlet 65 | let figletFont = figletLogos["font"] 66 | let tmpFile = "figlet_art.txt".toTmpPath 67 | 68 | if execCmd(&"figlet -f {figletFont} '{distroId}' > {tmpFile}") != 0: 69 | logError("Failed to execute 'figlet'!") 70 | let artLines = readFile(tmpFile).split('\n') 71 | let tmargin = figletLogos["margin"] 72 | result.logo.margin = [tmargin[0].getInt(), tmargin[1].getInt(), tmargin[2].getInt()] 73 | for line in artLines: 74 | if line != "": 75 | result.logo.art.add(figletLogos["color"].getStr() & line) 76 | -------------------------------------------------------------------------------- /src/catnaplib/platform/probe.nim: -------------------------------------------------------------------------------- 1 | import os 2 | import strformat 3 | import math 4 | import strutils 5 | from parsecfg import loadConfig, getSectionValue 6 | import posix_utils 7 | import times 8 | import tables 9 | import osproc 10 | import re 11 | from unicode import toLower 12 | from "../global/definitions" import DistroId, PKGMANAGERS, PKGCOUNTCOMMANDS, toCachePath, toTmpPath, Config 13 | import "../terminal/logging" 14 | import parsetoml 15 | import "caching" 16 | import algorithm 17 | 18 | proc getDistro*(): string = 19 | let cacheFile = "distroname".toCachePath 20 | result = readCache(cacheFile) 21 | if result != "": return 22 | 23 | when defined(linux) or defined(bsd): 24 | # Returns the name of the running linux distro 25 | result = "/etc/os-release".loadConfig.getSectionValue("", "PRETTY_NAME") & " " & uname().machine 26 | elif defined(macosx): 27 | result = "MacOS X" & " " & uname().machine 28 | else: 29 | result = "Unknown" 30 | 31 | writeCache(cacheFile, result, INFINITEDURATION) 32 | 33 | proc getDistroId*(): DistroId = 34 | let cacheFile = "distroid".toCachePath 35 | let raw = readCache(cacheFile) 36 | if raw != "": 37 | let raw_vals = raw.split(':') 38 | if raw_vals.len == 2: 39 | result.id = raw_vals[0] 40 | result.like = raw_vals[1] 41 | 42 | if defined(linux) or defined(bsd): 43 | if fileExists("/boot/issue.txt"): # Check if raspbian else get distroid from /etc/os-release 44 | result.id = "raspbian" 45 | result.like = "debian" 46 | else: 47 | result.id = "/etc/os-release".loadConfig.getSectionValue("", "ID").toLower() 48 | result.like = "/etc/os-release".loadConfig.getSectionValue("", "ID_LIKE").toLower() 49 | elif defined(macosx): 50 | result.id = "macos" 51 | result.like = "macos" 52 | else: 53 | result.id = "Unknown" 54 | result.like = "Unknown" 55 | writeCache(cacheFile, &"{result.id}|{result.like}", INFINITEDURATION) 56 | 57 | proc getUptime*(): string = 58 | # Returns the system uptime as a string (DAYS, HOURS, MINUTES) 59 | 60 | # Uptime in sec 61 | var utu: int 62 | if defined(linux) or defined(bsd): 63 | let uptime = "/proc/uptime".open.readLine.split(".")[0] 64 | utu = uptime.parseInt 65 | else: 66 | let 67 | boottime = execProcess("sysctl -n kern.boottime").split(" ")[3].split(",")[0] 68 | now = epochTime() 69 | utu = toInt(now) - parseInt(boottime) 70 | 71 | let 72 | uth = utu div 3600 mod 24 # hours 73 | utm = utu mod 3600 div 60 # minutes 74 | utd = utu div 3600 div 24 # days 75 | 76 | if utd == 0 and uth != 0: 77 | result = &"{uth}h {utm}m" # return hours and mins 78 | elif uth == 0 and utd == 0: 79 | result = &"{utm}m" # return only mins 80 | else: 81 | result = &"{utd}d {uth}h {utm}m" # return days, hours and mins 82 | 83 | proc getHostname*(): string = 84 | # Returns the system hostname 85 | result = uname().nodename 86 | 87 | proc getUser*(): string = 88 | # Returns the current username 89 | result = getEnv("USER") 90 | 91 | proc getKernel*(): string = 92 | # Returns the active kernel version 93 | result = uname().release 94 | 95 | proc getParentPid(pid: int): int = 96 | if defined(linux) or defined(bsd): 97 | let statusFilePath = "/proc/" & $pid & "/status" 98 | let statusLines = readFile(statusFilePath).split("\n") 99 | for rawline in statusLines: 100 | let stat = rawline.split(":") 101 | if stat[0] == "PPid": # Filter CurrentProcessInfo for Parent pid 102 | let pPid = parseInt(stat[1].strip()) 103 | return pPid 104 | elif defined(macosx): 105 | let pPid = execProcess("ps -o ppid -p " & $pid).split("\n")[1] 106 | return parseInt(pPid) 107 | else: 108 | return -1 109 | 110 | proc getProcessName(pid: int): string = 111 | if defined(linux) or defined(bsd): 112 | let statusLines = readFile("/proc/" & $pid & "/status").split("\n") 113 | for rawLine in statusLines: 114 | let stat = rawLine.split(":") 115 | if stat[0] == "Name": # Filter ParentProcessInfo for Parent Name 116 | return stat[1].strip() 117 | elif defined(macosx): 118 | let cmd = execProcess("ps -o comm -p " & $pid).split("\n")[1] 119 | return cmd.split("/")[^1] # Strip away command path 120 | else: 121 | return "Unknown" 122 | 123 | proc getTerminal*(): string = 124 | # Returns the currently running terminal emulator 125 | if defined(linux) or defined(bsd): 126 | result = getCurrentProcessID().getParentPid().getParentPid().getProcessName() 127 | if result == "login" or result == "sshd": 128 | result = "tty" 129 | elif defined(macosx): 130 | result = getEnv("TERM_PROGRAM") 131 | else: 132 | result = "Unknown" 133 | 134 | proc getShell*(): string = 135 | # Returns the system shell 136 | result = getCurrentProcessID().getParentPid().getProcessName() 137 | 138 | proc getDesktop*(): string = 139 | # Returns the running desktop env 140 | result = getEnv("XDG_CURRENT_DESKTOP") # Check Current Desktop (Method 1) 141 | 142 | if result == "": # Check Current Desktop (Method 2) 143 | result = getEnv("XDG_SESSION_DESKTOP") 144 | 145 | if result == "": # Check Current Desktop (Method 3) 146 | result = getEnv("DESKTOP_SESSION") 147 | 148 | if result == "": # Check if in tty mode (Method 1) 149 | if getEnv("XDG_SESSION_TYPE") == "tty": 150 | result = "Headless" 151 | 152 | if result == "": # Check if in tty mode (Method 2) 153 | if getTerminal() == "tty": 154 | result = "Headless" 155 | 156 | if result == "": # Default to Unknown 157 | result = "Unknown" 158 | 159 | proc getMemory*(mb: bool = true): string = 160 | # Returns statistics about the memory 161 | let 162 | dividend: uint = if mb: 1000 else: 1024 163 | suffix: string = if mb: "MB" else: "MiB" 164 | if defined(linux) or defined(bsd): 165 | let 166 | fileSeq: seq[string] = "/proc/meminfo".readLines(3) 167 | 168 | memTotalString = fileSeq[0].split(" ")[^2] 169 | memAvailableString = fileSeq[2].split(" ")[^2] 170 | 171 | memTotalInt = memTotalString.parseUInt div dividend 172 | memAvailableInt = memAvailableString.parseUInt div dividend 173 | 174 | memUsedInt = memTotalInt - memAvailableInt 175 | percentage = ((int(memUsedInt) / int(memTotalInt)) * 100).round().int() 176 | 177 | result = &"{memUsedInt} / {memTotalInt} {suffix} ({percentage}%)" 178 | elif defined(macosx): 179 | # The computation of free memory is very subjective on MacOS; multiple system utilites, ie vm_stat, top, memory_pressure, sysctl, all give different results 180 | # Here memory_pressure is used since it shows the most resemblance to the graph in the Activity Monitor 181 | let 182 | memPressureRaw = execProcess("memory_pressure").split("\n") 183 | 184 | memTotalString = memPressureRaw[0].split(" ")[3] 185 | freePercenString = memPressureRaw[^2].split(" ")[^1] 186 | 187 | memTotalInt = memTotalString.parseUInt div 1024 div dividend 188 | freePercentInt = parseUInt(freePercenString[0..^2]) # This string comes with a % sign at the end 189 | memUsedInt = memTotalInt * (100 - freePercentInt) div 100 190 | 191 | result = &"{memUsedInt} / {memTotalInt} {suffix} ({100 - freePercentInt}%)" 192 | else: 193 | result = "Unknown" 194 | 195 | proc getBattery*(): string = 196 | if defined(linux) or defined(bsd): 197 | # Credits to https://gitlab.com/prashere/battinfo for regex implementation. 198 | let 199 | BATTERY_REGEX = re"^BAT\d+$" 200 | powerPath = "/sys/class/power_supply/" 201 | 202 | var batterys: seq[tuple[idx: int, path: string]] 203 | 204 | # Collect all batterys 205 | for dir in os.walk_dir(powerPath): 206 | if re.match(os.last_path_part(dir.path), BATTERY_REGEX): 207 | let batteryPath = dir.path & "/" 208 | # Only check if battery has capacity and status 209 | if fileExists(batteryPath & "capacity") and fileExists(batteryPath & "status"): 210 | batterys.add((parseInt($dir.path[^1]), batteryPath)) 211 | 212 | if batterys.len < 1: 213 | logError("No battery detected!") 214 | 215 | # Sort batterys by number 216 | sort(batterys) 217 | 218 | # Get stats for battery with lowest number 219 | let 220 | batteryCapacity = readFile(batterys[0].path & "capacity").strip() 221 | batteryStatus = readFile(batterys[0].path & "status").strip() 222 | 223 | result = &"{batteryCapacity}% ({batteryStatus})" 224 | elif defined(macosx): 225 | let 226 | pmset = execProcess("pmset -g batt | tail -n 1").split("\t")[1].split("; ") 227 | batteryCapacity = pmset[0] 228 | batteryStatus = pmset[1] 229 | result = &"{batteryCapacity} ({batteryStatus})" 230 | else: 231 | result = "Unknown" 232 | 233 | proc getMounts*(): seq[string] = 234 | proc getMountPoints(): cstring {.importc, varargs, header: "getDisk.h".} 235 | 236 | let mounts_raw = $getMountPoints() 237 | if mounts_raw == "": 238 | logError("Failed to get disk mounting Points") 239 | 240 | let mounts = mounts_raw.split(',') 241 | 242 | for mount in mounts: 243 | if mount == "": 244 | continue 245 | 246 | if not mount.startsWith("/run/media"): 247 | if mount.startsWith("/proc") or 248 | mount.startsWith("/run") or 249 | mount.startsWith("/sys") or 250 | mount.startsWith("/tmp") or 251 | mount.startsWith("/boot") or 252 | mount.startsWith("/dev"): 253 | continue 254 | 255 | result.add(mount) 256 | 257 | proc getDisk*(mount: string): string = 258 | # Returns diskinfo for the mounting point 259 | proc getTotalDiskSpace(mountingPoint: cstring): cfloat {.importc, varargs, header: "getDisk.h".} 260 | proc getUsedDiskSpace(mountingPoint: cstring): cfloat {.importc, varargs, header: "getDisk.h".} 261 | 262 | let 263 | total = getTotalDiskSpace(mount.cstring).round().int() 264 | used = getUsedDiskSpace(mount.cstring).round().int() 265 | percentage = ((used / total) * 100).round().int() 266 | result = &"{used} / {total} GB ({percentage}%)" 267 | 268 | proc getCpu*(): string = 269 | # Returns the cpu name 270 | let cacheFile = "cpu".toCachePath 271 | result = readCache(cacheFile) 272 | if result != "": 273 | return 274 | 275 | if defined(linux) or defined(bsd): 276 | let rawLines = readFile("/proc/cpuinfo").split("\n") 277 | 278 | var key_name = "model name" 279 | if getDistroId().id == "raspbian": key_name = "Model" 280 | 281 | for rawLine in rawLines: 282 | let line = rawLine.split(":") 283 | 284 | if line.len < 2: continue 285 | 286 | let 287 | key = line[0].strip() 288 | val = line[1].strip() 289 | if key == key_name: 290 | result = val 291 | break 292 | elif defined(macosx): 293 | result = execProcess("sysctl -n machdep.cpu.brand_string").split("\n")[0] 294 | else: 295 | result = "Unknown" 296 | 297 | writeCache(cacheFile, result, initDuration(days=1)) 298 | 299 | proc getPkgManager(distroId: DistroId): string = 300 | for key in PKGMANAGERS.keys: 301 | if distroId.id == key: 302 | return PKGMANAGERS[key] 303 | 304 | for key in PKGMANAGERS.keys: 305 | if distroId.like == key: 306 | return PKGMANAGERS[key] 307 | 308 | return "Unknown" 309 | 310 | proc getPackages*(distroId: DistroId = getDistroId()): string = 311 | # Returns the installed package count 312 | let cacheFile = "packages".toCachePath 313 | 314 | result = readCache(cacheFile) 315 | if result != "": 316 | return 317 | 318 | let pkgManager = getPkgManager(distroId) 319 | if pkgManager == "Unknown": 320 | return "Unknown" 321 | 322 | var foundPkgCmd = false 323 | for key in PKGCOUNTCOMMANDS.keys: 324 | if pkgManager == key: 325 | foundPkgCmd = true 326 | break 327 | if not foundPkgCmd: 328 | return "Unknown" 329 | 330 | let tmpFile = "packages.txt".toTmpPath 331 | let cmd: string = PKGCOUNTCOMMANDS[pkgManager] & " > " & tmpFile 332 | if execCmd(cmd) != 0: 333 | logError("Failed to fetch pkg count!") 334 | 335 | let count = readFile(tmpFile).strip() 336 | result = count & " [" & pkgManager & "]" 337 | writeCache(cacheFile, result, initDuration(hours=2)) 338 | 339 | proc getGpu*(): string = 340 | # Returns the gpu name 341 | let cacheFile = "gpu".toCachePath 342 | 343 | result = readCache(cacheFile) 344 | if result != "": 345 | return 346 | 347 | if defined(linux) or defined(bsd): 348 | let tmpFile = "lspci.txt".toTmpPath 349 | 350 | if execCmd("lspci > " & tmpFile) != 0: 351 | logError("Failed to fetch GPU!") 352 | 353 | var vga = "Unknown" 354 | let lspci = readFile(tmpFile) 355 | for line in lspci.split('\n'): 356 | if line.split(' ')[1] == "VGA": 357 | vga = line 358 | break 359 | 360 | let vga_parts = vga.split(":") 361 | 362 | if vga_parts.len >= 2 or vga != "Unknown": 363 | result = vga_parts[vga_parts.len - 1].split("(")[0].strip() 364 | elif defined(macosx): 365 | result = execProcess("system_profiler SPDisplaysDataType | grep 'Chipset Model'").split(": ")[1].split("\n")[0] 366 | else: 367 | result = "Unknown" 368 | 369 | writeCache(cacheFile, result, initDuration(days=1)) 370 | 371 | proc getWeather*(config: Config): string = 372 | # Returns current weather 373 | let cacheFile = "weather".toCachePath 374 | var location = ""; 375 | if config.misc.contains("location"): 376 | location = config.misc["location"].getStr().replace(" ", "+") 377 | 378 | result = readCache(cacheFile) 379 | if result != "": 380 | return 381 | 382 | let tmpFile = "weather.txt".toTmpPath 383 | if execCmd("curl -s wttr.in/" & location & "?format=3 > " & tmpFile) != 0: 384 | logError("Failed to fetch weather!") 385 | 386 | result = readFile(tmpFile).strip() 387 | writeCache(cacheFile, result, initDuration(minutes=20)) 388 | -------------------------------------------------------------------------------- /src/catnaplib/terminal/colors.nim: -------------------------------------------------------------------------------- 1 | from "../global/definitions" import Color, ColorSet 2 | import strutils 3 | import re 4 | 5 | proc initForeground(): ColorSet = # (..) 6 | result.Black = "\e[30m" # BK 7 | result.Red = "\e[31m" # RD 8 | result.Green = "\e[32m" # GN 9 | result.Yellow = "\e[33m" # YW 10 | result.Blue = "\e[34m" # BE 11 | result.Magenta = "\e[35m" # MA 12 | result.Cyan = "\e[36m" # CN 13 | result.White = "\e[37m" # WE 14 | 15 | proc initForegroundBright(): ColorSet = # {..} 16 | result.Black = "\e[30;1m" # BK 17 | result.Red = "\e[31;1m" # RD 18 | result.Green = "\e[32;1m" # GN 19 | result.Yellow = "\e[33;1m" # YW 20 | result.Blue = "\e[34;1m" # BE 21 | result.Magenta = "\e[35;1m" # MA 22 | result.Cyan = "\e[36;1m" # CN 23 | result.White = "\e[37;1m" # WE 24 | 25 | proc initBackground(): ColorSet = # [..] 26 | result.Black = "\e[40m" # BK 27 | result.Red = "\e[41m" # RD 28 | result.Green = "\e[42m" # GN 29 | result.Yellow = "\e[43m" # YW 30 | result.Blue = "\e[44m" # BE 31 | result.Magenta = "\e[45m" # MA 32 | result.Cyan = "\e[46m" # CN 33 | result.White = "\e[47m" # WE 34 | 35 | proc initBackgroundBright(): ColorSet = # <..> 36 | result.Black = "\e[40;1m" # BK 37 | result.Red = "\e[41;1m" # RD 38 | result.Green = "\e[42;1m" # GN 39 | result.Yellow = "\e[43;1m" # YW 40 | result.Blue = "\e[44;1m" # BE 41 | result.Magenta = "\e[45;1m" # MA 42 | result.Cyan = "\e[46;1m" # CN 43 | result.White = "\e[47;1m" # WE 44 | 45 | # Global ColorSets: 46 | const 47 | Foreground*: ColorSet = static: initForeground() 48 | ForegroundBright*: ColorSet = static: initForegroundBright() 49 | Background*: ColorSet = static: initBackground() 50 | BackgroundBright*: ColorSet = static: initBackgroundBright() 51 | 52 | # Reset Value 53 | const Default*: Color = static: "\e[0m" # !DT! 54 | 55 | proc Colorize*(s: string): string = 56 | # Function to replace color codes with the correct ansi color code. 57 | 58 | result = s # Parse normal Foreground 59 | .replace("(BK)", Foreground.Black) 60 | .replace("(RD)", Foreground.Red) 61 | .replace("(GN)", Foreground.Green) 62 | .replace("(YW)", Foreground.Yellow) 63 | .replace("(BE)", Foreground.Blue) 64 | .replace("(MA)", Foreground.Magenta) 65 | .replace("(CN)", Foreground.Cyan) 66 | .replace("(WE)", Foreground.White) 67 | 68 | result = result # Parse bright Foreground 69 | .replace("{BK}", ForegroundBright.Black) 70 | .replace("{RD}", ForegroundBright.Red) 71 | .replace("{GN}", ForegroundBright.Green) 72 | .replace("{YW}", ForegroundBright.Yellow) 73 | .replace("{BE}", ForegroundBright.Blue) 74 | .replace("{MA}", ForegroundBright.Magenta) 75 | .replace("{CN}", ForegroundBright.Cyan) 76 | .replace("{WE}", ForegroundBright.White) 77 | 78 | result = result # Parse normal Background 79 | .replace("[BK]", Background.Black) 80 | .replace("[RD]", Background.Red) 81 | .replace("[GN]", Background.Green) 82 | .replace("[YW]", Background.Yellow) 83 | .replace("[BE]", Background.Blue) 84 | .replace("[MA]", Background.Magenta) 85 | .replace("[CN]", Background.Cyan) 86 | .replace("[WE]", Background.White) 87 | 88 | result = result # Parse bright Background 89 | .replace("", BackgroundBright.Black) 90 | .replace("", BackgroundBright.Red) 91 | .replace("", BackgroundBright.Green) 92 | .replace("", BackgroundBright.Yellow) 93 | .replace("", BackgroundBright.Blue) 94 | .replace("", BackgroundBright.Magenta) 95 | .replace("", BackgroundBright.Cyan) 96 | .replace("", BackgroundBright.White) 97 | 98 | result = result.replace("!DT!", Default) # Parse Default 99 | 100 | proc Uncolorize*(s: string): string = 101 | ## Removes ansi color codes from string 102 | result = re.replace(s, re"\e(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])", "") 103 | 104 | proc Reset*() = 105 | stdout.write(Default) 106 | stdout.flushFile() 107 | -------------------------------------------------------------------------------- /src/catnaplib/terminal/logging.nim: -------------------------------------------------------------------------------- 1 | import strformat 2 | import "colors.nim" 3 | 4 | proc logError*(msg: string, fatal: bool = true) = 5 | stderr.writeLine Foreground.Red & &"ERROR: {msg}" & Default 6 | if fatal: quit(1) -------------------------------------------------------------------------------- /src/extern/headers/getDisk.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #define GB 1073741824 6 | 7 | // Get all mounting points 8 | const char* getMountPoints() { 9 | FILE* mounts = fopen("/proc/mounts", "r"); 10 | if (!mounts) { 11 | return ""; 12 | } 13 | 14 | char line[256]; 15 | char mountPoint[256]; 16 | char device[256]; 17 | char fsType[256]; 18 | char result[4096] = ""; // Assuming max result size 19 | while (fgets(line, sizeof(line), mounts)) { 20 | sscanf(line, "%s %s %s", device, mountPoint, fsType); 21 | strcat(result, mountPoint); 22 | strcat(result, ","); 23 | } 24 | 25 | fclose(mounts); 26 | 27 | char* resultPtr = strdup(result); 28 | return resultPtr; 29 | } 30 | 31 | // Get 32 | double getTotalDiskSpace(const char* mountingPoint) { 33 | struct statvfs buffer; 34 | if (statvfs(mountingPoint, &buffer) != 0) return -1.f; 35 | const double total = (double)(buffer.f_blocks * buffer.f_frsize) / GB; 36 | return total; 37 | } 38 | 39 | double getUsedDiskSpace(const char* mountingPoint) { 40 | struct statvfs buffer; 41 | if (statvfs(mountingPoint, &buffer) != 0) return -1.f;; 42 | const double total = (double)(buffer.f_blocks * buffer.f_frsize) / GB; 43 | const double available = (double)(buffer.f_bfree * buffer.f_frsize) / GB; 44 | const double used = total - available; 45 | return used; 46 | } 47 | 48 | -------------------------------------------------------------------------------- /src/extern/libraries/parsetoml.nim: -------------------------------------------------------------------------------- 1 | ## :License: MIT 2 | ## 3 | ## Introduction 4 | ## ============ 5 | ## This module implements a TOML parser that is compliant with v0.5.0 of its spec. 6 | ## 7 | ## Source 8 | ## ====== 9 | ## `Repo link `_ 10 | ## 11 | 12 | # Copyright (c) 2015 Maurizio Tomasi and contributors 13 | # 14 | # Permission is hereby granted, free of charge, to any person 15 | # obtaining a copy of this software and associated documentation files 16 | # (the "Software"), to deal in the Software without restriction, 17 | # including without limitation the rights to use, copy, modify, merge, 18 | # publish, distribute, sublicense, and/or sell copies of the Software, 19 | # and to permit persons to whom the Software is furnished to do so, 20 | # subject to the following conditions: 21 | # 22 | # The above copyright notice and this permission notice shall be 23 | # included in all copies or substantial portions of the Software. 24 | # 25 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 26 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 27 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 28 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 29 | # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 30 | # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 31 | # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 32 | # SOFTWARE. 33 | 34 | import math 35 | import streams 36 | import strutils 37 | import tables 38 | import unicode 39 | from parseutils import parseFloat 40 | 41 | export tables 42 | 43 | when (NimMajor, NimMinor, NimPatch) < (1, 4, 0): 44 | type 45 | IndexDefect* = IndexError 46 | OverflowDefect* = OverflowError 47 | 48 | type 49 | Sign* = enum None, Pos, Neg 50 | 51 | TomlValueKind* {.pure.} = enum 52 | None 53 | Int, 54 | Float, 55 | Bool, 56 | Datetime, 57 | Date, 58 | Time, 59 | String, 60 | Array, 61 | Table 62 | 63 | TomlDate* = object 64 | year*: int 65 | month*: int 66 | day*: int 67 | 68 | TomlTime* = object 69 | hour*: int 70 | minute*: int 71 | second*: int 72 | subsecond*: int 73 | 74 | TomlDateTime* = object 75 | date*: TomlDate 76 | time*: TomlTime 77 | case shift*: bool 78 | of true: 79 | isShiftPositive*: bool 80 | zoneHourShift*: int 81 | zoneMinuteShift*: int 82 | of false: nil 83 | 84 | TomlTable* = OrderedTable[string, TomlValueRef] 85 | TomlTableRef* = ref TomlTable 86 | 87 | TomlValueRef* = ref TomlValue 88 | TomlValue* = object 89 | case kind*: TomlValueKind 90 | of TomlValueKind.None: nil 91 | of TomlValueKind.Int: intVal*: int64 92 | of TomlValueKind.Float: 93 | floatVal*: float64 94 | forcedSign*: Sign 95 | of TomlValueKind.Bool: boolVal*: bool 96 | of TomlValueKind.Datetime: dateTimeVal*: TomlDateTime 97 | of TomlValueKind.Date: dateVal*: TomlDate 98 | of TomlValueKind.Time: timeVal*: TomlTime 99 | of TomlValueKind.String: stringVal*: string 100 | of TomlValueKind.Array: arrayVal*: seq[TomlValueRef] 101 | of TomlValueKind.Table: tableVal*: TomlTableRef 102 | 103 | ParserState = object 104 | fileName*: string 105 | line*: int 106 | column*: int 107 | pushback: char 108 | stream*: streams.Stream 109 | curTableRef*: TomlTableRef 110 | 111 | TomlError* = object of ValueError 112 | location*: ParserState 113 | 114 | NumberBase = enum 115 | base10, base16, base8, base2 116 | 117 | StringType {.pure.} = enum 118 | Basic, # Enclosed within double quotation marks 119 | Literal # Enclosed within single quotation marks 120 | 121 | const 122 | defaultStringCapacity = 256 123 | ctrlChars = {'\x00' .. '\x08', '\x0A' .. '\x1F', '\x7F'} # '\x09' - TAB is not counted as control char 124 | ctrlCharsExclCrLf = ctrlChars - {'\x0A', '\x0D'} 125 | 126 | proc newTomlError(location: ParserState, msg: string): ref TomlError = 127 | result = newException(TomlError, location.fileName & "(" & $location.line & 128 | ":" & $location.column & ")" & " " & msg) 129 | result.location = location 130 | 131 | proc getNextChar(state: var ParserState): char = 132 | # Return the next available char from the stream associate with 133 | # the parser state, or '\0' if there are no characters left. 134 | 135 | if state.pushback != '\0': 136 | # If we've just read a character without having interpreted 137 | # it, just return it 138 | result = state.pushback 139 | state.pushback = '\0' 140 | else: 141 | if state.stream.atEnd(): 142 | return '\0' 143 | 144 | result = state.stream.readChar() 145 | 146 | # Update the line and column number 147 | if result == '\l': 148 | inc(state.line) 149 | state.column = 1 150 | elif result != '\r': 151 | inc(state.column) 152 | 153 | proc pushBackChar(state: var ParserState, c: char) {.inline.} = 154 | state.pushback = c 155 | 156 | type 157 | LfSkipMode = enum 158 | skipLf, skipNoLf 159 | 160 | proc getNextNonWhitespace(state: var ParserState, 161 | skip: LfSkipMode): char = 162 | # Note: this procedure does *not* consider a newline as a 163 | # "whitespace". Since newlines are often mandatory in TOML files 164 | # (e.g. after a key/value specification), we do not want to miss 165 | # them... 166 | 167 | let whitespaces = (case skip 168 | of skipLf: {' ', '\t', '\r', '\l'} 169 | of skipNoLf: {' ', '\t', '\r'}) 170 | 171 | var nextChar: char 172 | while true: 173 | nextChar = state.getNextChar() 174 | if nextChar == '#': 175 | # Skip the comment up to the newline, but do not jump over it 176 | while nextChar != '\l' and nextChar != '\0': 177 | nextChar = state.getNextChar() 178 | # https://toml.io/en/v1.0.0#comment 179 | # Control characters other than tab (U+0009) are not permitted in comments. 180 | # Invalid control characters: U+0000 to U+0008, U+000A to U+001F, U+007F 181 | if nextChar in ctrlCharsExclCrLf: 182 | raise newTomlError(state, "invalid control char 0x$# found in a comment" % [nextChar.ord.toHex(2)]) 183 | 184 | if nextChar notin whitespaces: break 185 | 186 | result = nextChar 187 | 188 | proc charToInt(c: char, base: NumberBase): int {.inline, noSideEffect.} = 189 | case base 190 | of base10, base8, base2: result = int(c) - int('0') 191 | of base16: 192 | if c in strutils.Digits: 193 | result = charToInt(c, base10) 194 | else: 195 | result = 10 + int(toUpperAscii(c)) - int('A') 196 | 197 | type 198 | LeadingChar {.pure.} = enum 199 | AllowZero, DenyZero 200 | 201 | proc parseInt(state: var ParserState, 202 | base: NumberBase, 203 | leadingChar: LeadingChar): int64 = 204 | var 205 | nextChar: char 206 | firstPos = true 207 | negative = false 208 | wasUnderscore = false 209 | 210 | let 211 | baseNum = (case base 212 | of base2: 2 213 | of base8: 8 214 | of base10: 10 215 | of base16: 16) 216 | digits = (case base 217 | of base2: {'0', '1'} 218 | of base8: {'0', '1', '2', '3', '4', '5', '6', '7'} 219 | of base10: strutils.Digits 220 | of base16: strutils.HexDigits) 221 | 222 | result = 0 223 | while true: 224 | wasUnderscore = nextChar == '_' 225 | nextChar = state.getNextChar() 226 | if nextChar == '_': 227 | if firstPos or wasUnderscore: 228 | raise(newTomlError(state, 229 | "underscore must be surrounded by digit")) 230 | continue 231 | 232 | if nextChar in {'+', '-'} and firstPos: 233 | firstPos = false 234 | if nextChar == '-': negative = true 235 | continue 236 | 237 | if nextChar == '0' and firstPos and leadingChar == LeadingChar.DenyZero: 238 | # TOML specifications forbid this 239 | var upcomingChar = state.getNextChar() 240 | if upcomingChar in Digits: 241 | raise(newTomlError(state, 242 | "leading zeroes are not allowed in integers")) 243 | else: 244 | state.pushBackChar(upcomingChar) 245 | 246 | if nextChar notin digits: 247 | if wasUnderscore: 248 | raise(newTomlError(state, 249 | "underscore must be surrounded by digit")) 250 | state.pushBackChar(nextChar) 251 | break 252 | 253 | try: 254 | result = result * baseNum - charToInt(nextChar, base) 255 | except OverflowDefect: 256 | raise(newTomlError(state, 257 | "integer numbers wider than 64 bits not allowed")) 258 | 259 | firstPos = false 260 | 261 | if not negative: 262 | try: 263 | result = -result 264 | except OverflowDefect: 265 | raise(newTomlError(state, 266 | "integer numbers wider than 64 bits not allowed")) 267 | 268 | proc parseEncoding(state: var ParserState): TomlValueRef = 269 | let nextChar = state.getNextChar() 270 | case nextChar: 271 | of 'b': 272 | return TomlValueRef(kind: TomlValueKind.Int, intVal: parseInt(state, base2, LeadingChar.AllowZero)) 273 | of 'o': 274 | return TomlValueRef(kind: TomlValueKind.Int, intVal: parseInt(state, base8, LeadingChar.AllowZero)) 275 | of 'x': 276 | return TomlValueRef(kind: TomlValueKind.Int, intVal: parseInt(state, base16, LeadingChar.AllowZero)) 277 | else: raise newTomlError(state, "illegal character") 278 | 279 | proc parseDecimalPart(state: var ParserState): float64 = 280 | var 281 | nextChar: char 282 | firstPos = true 283 | wasUnderscore = false 284 | decimalPartStr = "0." 285 | 286 | while true: 287 | wasUnderscore = nextChar == '_' 288 | nextChar = state.getNextChar() 289 | if nextChar == '_': 290 | if firstPos or wasUnderscore: 291 | raise(newTomlError(state, 292 | "underscore must be surrounded by digit")) 293 | continue 294 | if nextChar notin strutils.Digits: 295 | if wasUnderscore: 296 | raise(newTomlError(state, 297 | "underscore must be surrounded by digit")) 298 | state.pushBackChar(nextChar) 299 | if firstPos: 300 | raise newTomlError(state, "decimal part empty") 301 | break 302 | 303 | decimalPartStr.add(nextChar) 304 | 305 | firstPos = false 306 | doAssert decimalPartStr.len > 2 # decimalPartStr shouldn't still be "0." at this point 307 | discard parseutils.parseFloat(decimalPartStr, result) 308 | 309 | proc stringDelimiter(kind: StringType): char {.inline, noSideEffect.} = 310 | result = (case kind 311 | of StringType.Basic: '\"' 312 | of StringType.Literal: '\'') 313 | 314 | proc parseUnicode(state: var ParserState): string = 315 | let 316 | escapeKindChar = state.getNextChar() 317 | oldState = (column: state.column, line: state.line) 318 | code = parseInt(state, base16, LeadingChar.AllowZero) 319 | if state.line != oldState.line: 320 | raise newTomlError(state, "invalid Unicode codepoint, can't span lines") 321 | if escapeKindChar == 'u' and state.column - 5 != oldState.column: 322 | raise newTomlError(state, "invalid Unicode codepoint, 'u' must have " & 323 | "four character value") 324 | if escapeKindChar == 'U' and state.column - 9 != oldState.column: 325 | raise newTomlError(state, "invalid Unicode codepoint, 'U' must have " & 326 | "eight character value") 327 | if code notin 0'i64..0xD7FF and code notin 0xE000'i64..0x10FFFF: 328 | raise(newTomlError(state, "invalid Unicode codepoint, " & 329 | "must be a Unicode scalar value")) 330 | 331 | return unicode.toUTF8(Rune(code)) 332 | 333 | proc parseEscapeChar(state: var ParserState, escape: char): string = 334 | case escape 335 | of 'b': result = "\b" 336 | of 't': result = "\t" 337 | of 'n': result = "\l" 338 | of 'f': result = "\f" 339 | of 'r': result = "\r" 340 | of '\'': result = "\'" 341 | of '\"': result = "\"" 342 | of '\\': result = "\\" 343 | of 'u', 'U': 344 | state.pushBackChar(escape) 345 | result = parseUnicode(state) 346 | else: 347 | raise(newTomlError(state, 348 | "unknown escape " & 349 | "sequence \"\\" & escape & "\"")) 350 | 351 | proc parseSingleLineString(state: var ParserState, kind: StringType): string = 352 | # This procedure parses strings enclosed within single/double 353 | # quotation marks. It assumes that the quotation mark has already 354 | # been consumed by the "state" variable, which therefore is ready 355 | # to read the first character of the string. 356 | 357 | result = newStringOfCap(defaultStringCapacity) 358 | 359 | let delimiter = stringDelimiter(kind) 360 | 361 | var nextChar: char 362 | while true: 363 | nextChar = state.getNextChar() 364 | if nextChar == delimiter: 365 | break 366 | 367 | if nextChar == '\0': 368 | raise(newTomlError(state, "unterminated string")) 369 | 370 | # https://toml.io/en/v1.0.0#string 371 | # Any Unicode character may be used except those that must be escaped: 372 | # quotation mark, backslash, and the control characters other than tab 373 | # (U+0000 to U+0008, U+000A to U+001F, U+007F). 374 | if nextChar in ctrlChars: 375 | raise(newTomlError(state, "invalid character in string: 0x$#" % nextChar.ord.toHex(2))) 376 | 377 | if nextChar == '\\' and kind == StringType.Basic: 378 | nextChar = state.getNextChar() 379 | result.add(state.parseEscapeChar(nextChar)) 380 | continue 381 | 382 | result.add(nextChar) 383 | 384 | proc parseMultiLineString(state: var ParserState, kind: StringType): string = 385 | # This procedure parses strings enclosed within three consecutive 386 | # sigle/double quotation marks. It assumes that all the quotation 387 | # marks have already been consumed by the "state" variable, which 388 | # therefore is ready to read the first character of the string. 389 | 390 | result = newStringOfCap(defaultStringCapacity) 391 | let delimiter = stringDelimiter(kind) 392 | var 393 | isFirstChar = true 394 | nextChar: char 395 | while true: 396 | nextChar = state.getNextChar() 397 | 398 | # Skip the first newline, if it comes immediately after the 399 | # quotation marks 400 | if isFirstChar and (nextChar == '\l'): 401 | isFirstChar = false 402 | continue 403 | 404 | if nextChar == delimiter: 405 | # Are we done? 406 | nextChar = state.getNextChar() 407 | if nextChar == delimiter: 408 | nextChar = state.getNextChar() 409 | if nextChar == delimiter: 410 | # Done with this string 411 | return 412 | else: 413 | # Just got a double delimiter 414 | result.add(delimiter & delimiter) 415 | state.pushBackChar(nextChar) 416 | continue 417 | else: 418 | # Just got a lone delimiter 419 | result.add(delimiter) 420 | state.pushBackChar(nextChar) 421 | continue 422 | 423 | if nextChar == '\\' and kind == StringType.Basic: 424 | # This can either be an escape sequence or a end-of-line char 425 | nextChar = state.getNextChar() 426 | if nextChar in {'\l', '\r', ' '}: 427 | # We're at the end of a line: skip everything till the 428 | # next non-whitespace character 429 | while nextChar in {'\l', '\r', ' ', '\t'}: 430 | nextChar = state.getNextChar() 431 | 432 | state.pushBackChar(nextChar) 433 | continue 434 | else: 435 | # This is just an escape sequence (like "\t") 436 | #nextChar = state.getNextChar() 437 | result.add(state.parseEscapeChar(nextChar)) 438 | continue 439 | 440 | if nextChar == '\0': 441 | raise(newTomlError(state, "unterminated string")) 442 | 443 | # https://toml.io/en/v1.0.0#string 444 | # Any Unicode character may be used except those that must be 445 | # escaped: backslash and the control characters other than tab, 446 | # line feed, and carriage return (U+0000 to U+0008, U+000B, 447 | # U+000C, U+000E to U+001F, U+007F). 448 | if nextChar in ctrlCharsExclCrLf: 449 | raise(newTomlError(state, "invalid character in string: 0x$#" % nextChar.ord.toHex(2))) 450 | 451 | result.add(nextChar) 452 | isFirstChar = false 453 | 454 | proc parseString(state: var ParserState, kind: StringType): string = 455 | ## This function assumes that "state" has already consumed the 456 | ## first character (either \" or \', which is passed in the 457 | ## "openChar" parameter). 458 | 459 | let delimiter = stringDelimiter(kind) 460 | var nextChar: char = state.getNextChar() 461 | if nextChar == delimiter: 462 | # We have two possibilities here: (1) the empty string, or (2) 463 | # "long" multi-line strings. 464 | nextChar = state.getNextChar() 465 | if nextChar == delimiter: 466 | return parseMultiLineString(state, kind) 467 | else: 468 | # Empty string. This was easy! 469 | state.pushBackChar(nextChar) 470 | return "" 471 | else: 472 | state.pushBackChar(nextChar) 473 | return parseSingleLineString(state, kind) 474 | 475 | # Forward declaration 476 | proc parseValue(state: var ParserState): TomlValueRef 477 | proc parseInlineTable(state: var ParserState): TomlValueRef 478 | 479 | proc parseArray(state: var ParserState): seq[TomlValueRef] = 480 | # This procedure assumes that "state" has already consumed the '[' 481 | # character 482 | 483 | result = newSeq[TomlValueRef](0) 484 | 485 | while true: 486 | var nextChar: char = state.getNextNonWhitespace(skipLf) 487 | case nextChar 488 | of ']': 489 | return 490 | of ',': 491 | if len(result) == 0: 492 | # This happens with "[, 1, 2]", for instance 493 | raise(newTomlError(state, "first array element missing")) 494 | 495 | # Check that this is not a terminating comma (like in 496 | # "[b,]") 497 | nextChar = state.getNextNonWhitespace(skipLf) 498 | if nextChar == ']': 499 | return 500 | 501 | state.pushBackChar(nextChar) 502 | else: 503 | let oldState = state # Saved for error messages 504 | var newValue: TomlValueRef 505 | if nextChar != '{': 506 | state.pushBackChar(nextChar) 507 | newValue = parseValue(state) 508 | else: 509 | newValue = parseInlineTable(state) 510 | 511 | if len(result) > 0: 512 | # Check that the type of newValue is compatible with the 513 | # previous ones 514 | if newValue.kind != result[low(result)].kind: 515 | raise(newTomlError(oldState, 516 | "array members with incompatible types")) 517 | 518 | result.add(newValue) 519 | 520 | proc parseStrictNum(state: var ParserState, 521 | minVal: int, 522 | maxVal: int, 523 | count: Slice[int], 524 | msg: string): int = 525 | var 526 | nextChar: char 527 | parsedChars = 0 528 | 529 | result = 0 530 | while true: 531 | nextChar = state.getNextChar() 532 | 533 | if nextChar notin strutils.Digits: 534 | state.pushBackChar(nextChar) 535 | break 536 | 537 | try: 538 | result = result * 10 + charToInt(nextChar, base10) 539 | parsedChars += 1 540 | except OverflowDefect: 541 | raise(newTomlError(state, 542 | "integer numbers wider than 64 bits not allowed")) 543 | 544 | if parsedChars notin count: 545 | raise(newTomlError(state, 546 | "too few or too many characters in digit, expected " & 547 | $count & " got " & $parsedChars)) 548 | 549 | if result < minVal or result > maxVal: 550 | raise(newTomlError(state, msg & " (" & $result & ")")) 551 | 552 | template parseStrictNum(state: var ParserState, 553 | minVal: int, 554 | maxVal: int, 555 | count: int, 556 | msg: string): int = 557 | parseStrictNum(state, minVal, maxVal, (count..count), msg) 558 | 559 | proc parseTimePart(state: var ParserState, val: var TomlTime) = 560 | var 561 | nextChar: char 562 | curLine = state.line 563 | 564 | # Parse the minutes 565 | val.minute = parseStrictNum(state, minVal = 0, maxVal = 59, count = 2, 566 | "number out of range for minutes") 567 | if curLine != state.line: 568 | raise(newTomlError(state, "invalid date field, stopped in or after minutes field")) 569 | 570 | nextChar = state.getNextChar() 571 | if nextChar != ':': 572 | raise(newTomlError(state, 573 | "\":\" expected after the number of seconds")) 574 | 575 | # Parse the second. Note that seconds=60 *can* happen (leap second) 576 | val.second = parseStrictNum(state, minVal = 0, maxVal = 60, count = 2, 577 | "number out of range for seconds") 578 | 579 | nextChar = state.getNextChar() 580 | if nextChar == '.': 581 | val.subsecond = parseInt(state, base10, LeadingChar.AllowZero).int 582 | else: 583 | state.pushBackChar(nextChar) 584 | 585 | proc parseDateTimePart(state: var ParserState, 586 | dateTime: var TomlDateTime): bool = 587 | 588 | # This function is called whenever a datetime object is found. They follow 589 | # an ISO convention and can use one of the following format: 590 | # 591 | # - YYYY-MM-DDThh:mm:ss[+-]hh:mm 592 | # - YYYY-MM-DDThh:mm:ssZ 593 | # 594 | # where the "T" and "Z" letters are literals, [+-] indicates 595 | # *either* "+" or "-", YYYY is the 4-digit year, MM is the 2-digit 596 | # month, DD is the 2-digit day, hh is the 2-digit hour, mm is the 597 | # 2-digit minute, and ss is the 2-digit second. The hh:mm after 598 | # the +/- character is the timezone; a literal "Z" indicates the 599 | # local timezone. 600 | 601 | # This function assumes that the "YYYY-" part has already been 602 | # parsed (this happens because during parsing, finding a 4-digit 603 | # number like "YYYY" might just indicate the presence of an 604 | # integer or a floating-point number; it's the following "-" that 605 | # tells the parser that the value is a datetime). As a consequence 606 | # of this, we assume that "dateTime.year" has already been set. 607 | 608 | var 609 | nextChar: char 610 | curLine = state.line 611 | 612 | # Parse the month 613 | dateTime.date.month = parseStrictNum(state, minVal = 1, maxVal = 12, count = 2, 614 | "number out of range for the month") 615 | if curLine != state.line: 616 | raise(newTomlError(state, "invalid date field, stopped in or after month field")) 617 | 618 | nextChar = state.getNextChar() 619 | if nextChar != '-': 620 | raise(newTomlError(state, "\"-\" expected after the month number")) 621 | 622 | # Parse the day 623 | dateTime.date.day = parseStrictNum(state, minVal = 1, maxVal = 31, count = 2, 624 | "number out of range for the day") 625 | if curLine != state.line: 626 | return false 627 | else: 628 | nextChar = state.getNextChar() 629 | if nextChar notin {'t', 'T', ' '}: 630 | raise(newTomlError(state, "\"T\", \"t\", or space expected after the day number")) 631 | 632 | # Parse the hour 633 | dateTime.time.hour = parseStrictNum(state, minVal = 0, maxVal = 23, count = 2, 634 | "number out of range for the hours") 635 | if curLine != state.line: 636 | raise(newTomlError(state, "invalid date field, stopped in or after hours field")) 637 | 638 | nextChar = state.getNextChar() 639 | if nextChar != ':': 640 | raise(newTomlError(state, "\":\" expected after the number of hours")) 641 | 642 | # Parse the minutes 643 | dateTime.time.minute = parseStrictNum(state, minVal = 0, maxVal = 59, count = 2, 644 | "number out of range for minutes") 645 | if curLine != state.line: 646 | raise(newTomlError(state, "invalid date field, stopped in or after minutes field")) 647 | 648 | nextChar = state.getNextChar() 649 | if nextChar != ':': 650 | raise(newTomlError(state, 651 | "\":\" expected after the number of seconds")) 652 | 653 | # Parse the second. Note that seconds=60 *can* happen (leap second) 654 | dateTime.time.second = parseStrictNum(state, minVal = 0, maxVal = 60, count = 2, 655 | "number out of range for seconds") 656 | 657 | nextChar = state.getNextChar() 658 | if nextChar == '.': 659 | dateTime.time.subsecond = parseInt(state, base10, LeadingChar.AllowZero).int 660 | else: 661 | state.pushBackChar(nextChar) 662 | 663 | nextChar = state.getNextChar() 664 | case nextChar 665 | of 'z', 'Z': 666 | dateTime = TomlDateTime( 667 | time: dateTime.time, 668 | date: dateTime.date, 669 | shift: true, 670 | isShiftPositive: true, 671 | zoneHourShift: 0, 672 | zoneMinuteShift: 0 673 | ) 674 | of '+', '-': 675 | dateTime = TomlDateTime( 676 | time: dateTime.time, 677 | date: dateTime.date, 678 | shift: true, 679 | isShiftPositive: (nextChar == '+') 680 | ) 681 | dateTime.zoneHourShift = 682 | parseStrictNum(state, minVal = 0, maxVal = 23, count = 2, 683 | "number out of range for shift hours") 684 | if curLine != state.line: 685 | raise(newTomlError(state, "invalid date field, stopped in or after shift hours field")) 686 | 687 | nextChar = state.getNextChar() 688 | if nextChar != ':': 689 | raise(newTomlError(state, 690 | "\":\" expected after the number of shift hours")) 691 | 692 | dateTime.zoneMinuteShift = 693 | parseStrictNum(state, minVal = 0, maxVal = 59, count = 2, 694 | "number out of range for shift minutes") 695 | else: 696 | if curLine == state.line: 697 | raise(newTomlError(state, "unexpected character " & escape($nextChar) & 698 | " instead of the time zone")) 699 | else: # shift is automatically initialized to false 700 | state.pushBackChar(nextChar) 701 | 702 | return true 703 | 704 | proc parseDateOrTime(state: var ParserState, digits: int, yearOrHour: int): TomlValueRef = 705 | var 706 | nextChar: char 707 | yoh = yearOrHour 708 | d = digits 709 | while true: 710 | nextChar = state.getNextChar() 711 | case nextChar: 712 | of ':': 713 | if d != 2: 714 | raise newTomlError(state, "wrong number of characters for hour") 715 | var val: TomlTime 716 | val.hour = yoh 717 | 718 | parseTimePart(state, val) 719 | return TomlValueRef(kind: TomlValueKind.Time, timeVal: val) 720 | of '-': 721 | if d != 4: 722 | raise newTomlError(state, "wrong number of characters for year") 723 | var val: TomlDateTime 724 | val.date.year = yoh 725 | let fullDate = parseDateTimePart(state, val) 726 | 727 | if fullDate: 728 | return TomlValueRef(kind: TomlValueKind.DateTime, 729 | dateTimeVal: val) 730 | else: 731 | return TomlValueRef(kind: TomlValueKind.Date, 732 | dateVal: val.date) 733 | of strutils.Digits: 734 | if d == 4: 735 | raise newTomlError(state, "leading zero not allowed") 736 | try: 737 | yoh *= 10 738 | yoh += ord(nextChar) - ord('0') 739 | d += 1 740 | except OverflowDefect: 741 | raise newTomlError(state, "number larger than 64 bits wide") 742 | continue 743 | of strutils.Whitespace: 744 | raise newTomlError(state, "leading zero not allowed") 745 | else: raise newTomlError(state, "illegal character") 746 | 747 | proc parseFloat(state: var ParserState, intPart: int, forcedSign: Sign): TomlValueRef = 748 | var 749 | decimalPart = parseDecimalPart(state) 750 | nextChar = state.getNextChar() 751 | exponent: int64 = 0 752 | if nextChar in {'e', 'E'}: 753 | exponent = parseInt(state, base10, LeadingChar.AllowZero) 754 | else: 755 | state.pushBackChar(nextChar) 756 | 757 | let value = 758 | if intPart <= 0: 759 | pow(10.0, exponent.float64) * (float64(intPart) - decimalPart) 760 | else: 761 | pow(10.0, exponent.float64) * (float64(intPart) + decimalPart) 762 | return TomlValueRef(kind: TomlValueKind.Float, floatVal: if forcedSign != Neg: -value else: value, forcedSign: forcedSign) 763 | 764 | proc parseNumOrDate(state: var ParserState): TomlValueRef = 765 | var 766 | nextChar: char 767 | forcedSign: Sign = None 768 | 769 | while true: 770 | nextChar = state.getNextChar() 771 | case nextChar: 772 | of '0': 773 | nextChar = state.getNextChar() 774 | if forcedSign == None: 775 | if nextChar in {'b', 'x', 'o'}: 776 | state.pushBackChar(nextChar) 777 | return parseEncoding(state) 778 | else: 779 | # This must now be a float or a date/time, or a sole 0 780 | case nextChar: 781 | of '.': 782 | return parseFloat(state, 0, forcedSign) 783 | of strutils.Whitespace: 784 | state.pushBackChar(nextChar) 785 | return TomlValueRef(kind: TomlValueKind.Int, intVal: 0) 786 | of strutils.Digits: 787 | # This must now be a date/time 788 | return parseDateOrTime(state, digits = 2, yearOrHour = ord(nextChar) - ord('0')) 789 | else: 790 | # else is a sole 0 791 | return TomlValueRef(kind: TomlValueKind.Int, intVal: 0) 792 | else: 793 | # This must now be a float, or a sole 0 794 | case nextChar: 795 | of '.': 796 | return parseFloat(state, 0, forcedSign) 797 | of strutils.Whitespace: 798 | state.pushBackChar(nextChar) 799 | return TomlValueRef(kind: TomlValueKind.Int, intVal: 0) 800 | else: 801 | # else is a sole 0 802 | return TomlValueRef(kind: TomlValueKind.Int, intVal: 0) 803 | of strutils.Digits - {'0'}: 804 | # This might be a date/time, or an int or a float 805 | var 806 | digits = 1 807 | curSum = ord('0') - ord(nextChar) 808 | wasUnderscore = false 809 | while true: 810 | nextChar = state.getNextChar() 811 | if wasUnderscore and nextChar notin strutils.Digits: 812 | raise newTomlError(state, "underscores must be surrounded by digits") 813 | case nextChar: 814 | of ':': 815 | if digits != 2: 816 | raise newTomlError(state, "wrong number of characters for hour") 817 | var val: TomlTime 818 | val.hour = -curSum 819 | parseTimePart(state, val) 820 | return TomlValueRef(kind: TomlValueKind.Time, timeVal: val) 821 | of '-': 822 | if digits != 4: 823 | raise newTomlError(state, "wrong number of characters for year") 824 | var val: TomlDateTime 825 | val.date.year = -curSum 826 | let fullDate = parseDateTimePart(state, val) 827 | if fullDate: 828 | return TomlValueRef(kind: TomlValueKind.DateTime, 829 | dateTimeVal: val) 830 | else: 831 | return TomlValueRef(kind: TomlValueKind.Date, 832 | dateVal: val.date) 833 | of '.': 834 | return parseFloat(state, curSum, forcedSign) 835 | of 'e', 'E': 836 | var exponent = parseInt(state, base10, LeadingChar.AllowZero) 837 | let value = pow(10.0, exponent.float64) * float64(curSum) 838 | return TomlValueRef(kind: TomlValueKind.Float, floatVal: if forcedSign != Neg: -value else: value) 839 | of strutils.Digits: 840 | try: 841 | curSum *= 10 842 | curSum += ord('0') - ord(nextChar) 843 | digits += 1 844 | except OverflowDefect: 845 | raise newTomlError(state, "number larger than 64 bits wide") 846 | wasUnderscore = false 847 | continue 848 | of '_': 849 | wasUnderscore = true 850 | continue 851 | of strutils.Whitespace: 852 | state.pushBackChar(nextChar) 853 | return TomlValueRef(kind: TomlValueKind.Int, intVal: if forcedSign != Neg: -curSum else: curSum) 854 | else: 855 | state.pushBackChar(nextChar) 856 | return TomlValueRef(kind: TomlValueKind.Int, intVal: if forcedSign != Neg: -curSum else: curSum) 857 | of '+', '-': 858 | forcedSign = if nextChar == '+': Pos else: Neg 859 | continue 860 | of 'i': 861 | # Is this "inf"? 862 | let oldState = state 863 | if state.getNextChar() != 'n' or 864 | state.getNextChar() != 'f': 865 | raise(newTomlError(oldState, "unknown identifier")) 866 | return TomlValueRef(kind: TomlValueKind.Float, floatVal: if forcedSign == Neg: NegInf else: Inf, forcedSign: forcedSign) 867 | 868 | of 'n': 869 | # Is this "nan"? 870 | let oldState = state 871 | if state.getNextChar() != 'a' or 872 | state.getNextChar() != 'n': 873 | raise(newTomlError(oldState, "unknown identifier")) 874 | return TomlValueRef(kind: TomlValueKind.Float, floatVal: NaN, forcedSign: forcedSign) 875 | else: 876 | raise newTomlError(state, "illegal character " & escape($nextChar)) 877 | break 878 | 879 | proc parseValue(state: var ParserState): TomlValueRef = 880 | var nextChar: char 881 | 882 | nextChar = state.getNextNonWhitespace(skipNoLf) 883 | case nextChar 884 | of strutils.Digits, '+', '-', 'i', 'n': 885 | state.pushBackChar(nextChar) 886 | return parseNumOrDate(state) 887 | of 't': 888 | # Is this "true"? 889 | let oldState = state # Only used for error messages 890 | if state.getNextChar() != 'r' or 891 | state.getNextChar() != 'u' or 892 | state.getNextChar() != 'e': 893 | raise(newTomlError(oldState, "unknown identifier")) 894 | result = TomlValueRef(kind: TomlValueKind.Bool, boolVal: true) 895 | 896 | of 'f': 897 | # Is this "false"? 898 | let oldState = state # Only used for error messages 899 | if state.getNextChar() != 'a' or 900 | state.getNextChar() != 'l' or 901 | state.getNextChar() != 's' or 902 | state.getNextChar() != 'e': 903 | raise(newTomlError(oldState, "unknown identifier")) 904 | result = TomlValueRef(kind: TomlValueKind.Bool, boolVal: false) 905 | 906 | of '\"': 907 | # A basic string (accepts \ escape codes) 908 | result = TomlValueRef(kind: TomlValueKind.String, 909 | stringVal: parseString(state, StringType.Basic)) 910 | 911 | of '\'': 912 | # A literal string (does not accept \ escape codes) 913 | result = TomlValueRef(kind: TomlValueKind.String, 914 | stringVal: parseString(state, StringType.Literal)) 915 | 916 | of '[': 917 | # An array 918 | result = TomlValueRef(kind: TomlValueKind.Array, 919 | arrayVal: parseArray(state)) 920 | else: 921 | raise(newTomlError(state, 922 | "unexpected character " & escape($nextChar))) 923 | 924 | proc parseName(state: var ParserState): string = 925 | # This parses the name of a key or a table 926 | result = newStringOfCap(defaultStringCapacity) 927 | 928 | var nextChar = state.getNextNonWhitespace(skipNoLf) 929 | if nextChar == '\"': 930 | return state.parseString(StringType.Basic) 931 | elif nextChar == '\'': 932 | return state.parseString(StringType.Literal) 933 | state.pushBackChar(nextChar) 934 | while true: 935 | nextChar = state.getNextChar() 936 | if (nextChar in {'=', '.', '[', ']', '\0', ' ', '\t'}): 937 | # Any of the above characters marks the end of the name 938 | state.pushBackChar(nextChar) 939 | break 940 | elif (nextChar notin {'a'..'z', 'A'..'Z', '0'..'9', '_', '-'}): 941 | raise(newTomlError(state, 942 | "bare key has illegal character: " & escape($nextChar))) 943 | else: 944 | result.add(nextChar) 945 | 946 | type 947 | BracketType {.pure.} = enum 948 | single, double 949 | 950 | proc parseTableName(state: var ParserState, 951 | brackets: BracketType): seq[string] = 952 | # This code assumes that '[' has already been consumed 953 | result = newSeq[string](0) 954 | 955 | while true: 956 | #let partName = state.parseName(SpecialChars.AllowNumberSign) 957 | var 958 | nextChar = state.getNextChar() 959 | partName: string 960 | if nextChar == '"': 961 | partName = state.parseString(StringType.Basic) 962 | else: 963 | state.pushBackChar(nextChar) 964 | partName = state.parseName() 965 | result.add(partName) 966 | 967 | nextChar = state.getNextNonWhitespace(skipNoLf) 968 | case nextChar 969 | of ']': 970 | if brackets == BracketType.double: 971 | nextChar = state.getNextChar() 972 | if nextChar != ']': 973 | raise(newTomlError(state, 974 | "\"]]\" expected")) 975 | 976 | # We must check that there is nothing else in this line 977 | nextChar = state.getNextNonWhitespace(skipNoLf) 978 | if nextChar notin {'\l', '\0'}: 979 | raise(newTomlError(state, 980 | "unexpected character " & escape($nextChar))) 981 | 982 | break 983 | 984 | of '.': continue 985 | else: 986 | raise(newTomlError(state, 987 | "unexpected character " & escape($nextChar))) 988 | 989 | proc setEmptyTableVal(val: var TomlValueRef) = 990 | val = TomlValueRef(kind: TomlValueKind.Table) 991 | new(val.tableVal) 992 | val.tableVal[] = initOrderedTable[string, TomlValueRef]() 993 | 994 | proc parseInlineTable(state: var ParserState): TomlValueRef = 995 | new(result) 996 | setEmptyTableVal(result) 997 | var firstComma = true 998 | while true: 999 | var nextChar = state.getNextNonWhitespace(skipNoLf) 1000 | case nextChar 1001 | of '}': 1002 | return 1003 | of ',': 1004 | if firstComma: 1005 | raise(newTomlError(state, "first inline table element missing")) 1006 | # Check that this is not a terminating comma (like in 1007 | # "[b,]") 1008 | nextChar = state.getNextNonWhitespace(skipNoLf) 1009 | if nextChar == '}': 1010 | return 1011 | 1012 | state.pushBackChar(nextChar) 1013 | of '\n': 1014 | raise(newTomlError(state, "inline tables cannot contain newlines")) 1015 | else: 1016 | firstComma = false 1017 | state.pushBackChar(nextChar) 1018 | var key = state.parseName() 1019 | 1020 | nextChar = state.getNextNonWhitespace(skipNoLf) 1021 | var curTable = result.tableVal 1022 | while nextChar == '.': 1023 | var deepestTable = new(TomlTableRef) 1024 | deepestTable[] = initOrderedTable[string, TomlValueRef]() 1025 | curTable[key] = TomlValueRef(kind: TomlValueKind.Table, tableVal: deepestTable) 1026 | curTable = deepestTable 1027 | key = state.parseName() 1028 | nextChar = state.getNextNonWhitespace(skipNoLf) 1029 | 1030 | if nextChar != '=': 1031 | raise(newTomlError(state, 1032 | "key names cannot contain spaces")) 1033 | nextChar = state.getNextNonWhitespace(skipNoLf) 1034 | if nextChar == '{': 1035 | curTable[key] = state.parseInlineTable() 1036 | else: 1037 | state.pushBackChar(nextChar) 1038 | curTable[key] = state.parseValue() 1039 | 1040 | proc createTableDef(state: var ParserState, 1041 | tableNames: seq[string], 1042 | dotted = false) 1043 | 1044 | proc parseKeyValuePair(state: var ParserState) = 1045 | var 1046 | tableKeys: seq[string] 1047 | key: string 1048 | nextChar: char 1049 | oldTableRef = state.curTableRef 1050 | 1051 | while true: 1052 | let subkey = state.parseName() 1053 | 1054 | nextChar = state.getNextNonWhitespace(skipNoLf) 1055 | if nextChar == '.': 1056 | tableKeys.add subkey 1057 | else: 1058 | if tableKeys.len != 0: 1059 | createTableDef(state, tableKeys, dotted = true) 1060 | key = subkey 1061 | break 1062 | 1063 | if nextChar != '=': 1064 | raise(newTomlError(state, 1065 | "key names cannot contain character \"" & nextChar & "\"")) 1066 | 1067 | nextChar = state.getNextNonWhitespace(skipNoLf) 1068 | # Check that this is a regular value and not an inline table 1069 | if nextChar != '{': 1070 | state.pushBackChar(nextChar) 1071 | let value = state.parseValue() 1072 | 1073 | # We must check that there is nothing else in this line 1074 | nextChar = state.getNextNonWhitespace(skipNoLf) 1075 | if nextChar != '\l' and nextChar != '\0': 1076 | raise(newTomlError(state, 1077 | "unexpected character " & escape($nextChar))) 1078 | 1079 | if state.curTableRef.hasKey(key): 1080 | raise(newTomlError(state, 1081 | "duplicate key, \"" & key & "\" already in table")) 1082 | state.curTableRef[key] = value 1083 | else: 1084 | #createTableDef(state, @[key]) 1085 | if key.len == 0: 1086 | raise newTomlError(state, "empty key not allowed") 1087 | if state.curTableRef.hasKey(key): 1088 | raise newTomlError(state, "duplicate table key not allowed") 1089 | state.curTableRef[key] = parseInlineTable(state) 1090 | 1091 | state.curTableRef = oldTableRef 1092 | 1093 | proc newParserState(s: streams.Stream, 1094 | fileName: string = ""): ParserState = 1095 | result = ParserState(fileName: fileName, line: 1, column: 1, stream: s) 1096 | 1097 | proc setArrayVal(val: var TomlValueRef, numOfElems: int = 0) = 1098 | val = TomlValueRef(kind: TomlValueKind.Array) 1099 | val.arrayVal = newSeq[TomlValueRef](numOfElems) 1100 | 1101 | proc advanceToNextNestLevel(state: var ParserState, 1102 | tableName: string) = 1103 | let target = state.curTableRef[tableName] 1104 | case target.kind 1105 | of TomlValueKind.Table: 1106 | state.curTableRef = target.tableVal 1107 | of TomlValueKind.Array: 1108 | let arr = target.arrayVal[high(target.arrayVal)] 1109 | if arr.kind != TomlValueKind.Table: 1110 | raise(newTomlError(state, "\"" & tableName & 1111 | "\" elements are not tables")) 1112 | state.curTableRef = arr.tableVal 1113 | else: 1114 | raise(newTomlError(state, "\"" & tableName & 1115 | "\" is not a table")) 1116 | 1117 | # This function is called by the TOML parser whenever a 1118 | # "[[table.name]]" line is encountered in the parsing process. Its 1119 | # purpose is to make sure that all the parent nodes in "table.name" 1120 | # exist and are tables, and that a terminal node of the correct type 1121 | # is created. 1122 | # 1123 | # Starting from "curTableRef" (which is usually the root object), 1124 | # traverse the object tree following the names in "tableNames" and 1125 | # create a new TomlValueRef object of kind "TomlValueKind.Array" at 1126 | # the terminal node. This array is going to be an array of tables: the 1127 | # function will create an element and will make "curTableRef" 1128 | # reference it. Example: if tableNames == ["a", "b", "c"], the code 1129 | # will look for the "b" table that is child of "a", and then it will 1130 | # check if "c" is a child of "b". If it is, it must be an array of 1131 | # tables, and a new element will be appended. Otherwise, a new "c" 1132 | # array is created, and an empty table element is added in "c". In 1133 | # either cases, curTableRef will refer to the last element of "c". 1134 | 1135 | proc createOrAppendTableArrayDef(state: var ParserState, 1136 | tableNames: seq[string]) = 1137 | # This is a table array entry (e.g. "[[entry]]") 1138 | for idx, tableName in tableNames: 1139 | if tableName.len == 0: 1140 | raise(newTomlError(state, 1141 | "empty key not allowed")) 1142 | let lastTableInChain = idx == high(tableNames) 1143 | 1144 | var newValue: TomlValueRef 1145 | if not state.curTableRef.hasKey(tableName): 1146 | # If this element does not exist, create it 1147 | new(newValue) 1148 | 1149 | # If this is the last name in the chain (e.g., 1150 | # "c" in "a.b.c"), its value should be an 1151 | # array of tables, otherwise just a table 1152 | if lastTableInChain: 1153 | setArrayVal(newValue, 1) 1154 | 1155 | new(newValue.arrayVal[0]) 1156 | setEmptyTableVal(newValue.arrayVal[0]) 1157 | 1158 | state.curTableRef[tableName] = newValue 1159 | state.curTableRef = newValue.arrayVal[0].tableVal 1160 | else: 1161 | setEmptyTableVal(newValue) 1162 | 1163 | # Add the newly created object to the current table 1164 | state.curTableRef[tableName] = newValue 1165 | 1166 | # Update the pointer to the current table 1167 | state.curTableRef = newValue.tableVal 1168 | else: 1169 | # The element exists: is it of the right type? 1170 | let target = state.curTableRef[tableName] 1171 | 1172 | if lastTableInChain: 1173 | if target.kind != TomlValueKind.Array: 1174 | raise(newTomlError(state, "\"" & tableName & 1175 | "\" is not an array")) 1176 | 1177 | var newValue: TomlValueRef 1178 | new(newValue) 1179 | setEmptyTableVal(newValue) 1180 | target.arrayVal.add(newValue) 1181 | state.curTableRef = newValue.tableVal 1182 | else: 1183 | advanceToNextNestLevel(state, tableName) 1184 | 1185 | # Starting from "curTableRef" (which is usually the root object), 1186 | # traverse the object tree following the names in "tableNames" and 1187 | # create a new TomlValueRef object of kind "TomlValueKind.Table" at 1188 | # the terminal node. Example: if tableNames == ["a", "b", "c"], the 1189 | # code will look for the "b" table that is child of "a" and it will 1190 | # create a new table "c" which is "b"'s children. 1191 | 1192 | proc createTableDef(state: var ParserState, 1193 | tableNames: seq[string], 1194 | dotted = false) = 1195 | var newValue: TomlValueRef 1196 | 1197 | # This starts a new table (e.g. "[table]") 1198 | for i, tableName in tableNames: 1199 | if tableName.len == 0: 1200 | raise(newTomlError(state, 1201 | "empty key not allowed")) 1202 | if not state.curTableRef.hasKey(tableName): 1203 | new(newValue) 1204 | setEmptyTableVal(newValue) 1205 | 1206 | # Add the newly created object to the current table 1207 | state.curTableRef[tableName] = newValue 1208 | 1209 | # Update the pointer to the current table 1210 | state.curTableRef = newValue.tableVal 1211 | else: 1212 | if i == tableNames.high and state.curTableRef.hasKey(tableName) and 1213 | state.curTableRef[tableName].kind == TomlValueKind.Table: 1214 | if state.curTableRef[tableName].tableVal.len == 0: 1215 | raise newTomlError(state, "duplicate table key not allowed") 1216 | elif not dotted: 1217 | for value in state.curTableRef[tableName].tableVal.values: 1218 | if value.kind != TomlValueKind.Table: 1219 | raise newTomlError(state, "duplicate table key not allowed") 1220 | advanceToNextNestLevel(state, tableName) 1221 | 1222 | proc parseStream*(inputStream: streams.Stream, 1223 | fileName: string = ""): TomlValueRef = 1224 | ## Parses a stream of TOML formatted data into a TOML table. The optional 1225 | ## filename is used for error messages. 1226 | if inputStream == nil: 1227 | raise newException(IOError, 1228 | "Unable to read from the stream created from: \"" & fileName & "\", " & 1229 | "possibly a missing file") 1230 | var state = newParserState(inputStream, fileName) 1231 | result = TomlValueRef(kind: TomlValueKind.Table) 1232 | new(result.tableVal) 1233 | result.tableVal[] = initOrderedTable[string, TomlValueRef]() 1234 | 1235 | # This pointer will always point to the table that should get new 1236 | # key/value pairs found in the TOML file during parsing 1237 | state.curTableRef = result.tableVal 1238 | 1239 | # Unlike "curTableRef", this pointer never changes: it always 1240 | # points to the uppermost table in the tree 1241 | let baseTable = result.tableVal 1242 | 1243 | var nextChar: char 1244 | while true: 1245 | nextChar = state.getNextNonWhitespace(skipLf) 1246 | case nextChar 1247 | of '[': 1248 | # A new section/table begins. We'll have to start again 1249 | # from the uppermost level, so let's rewind curTableRef to 1250 | # the root node 1251 | state.curTableRef = baseTable 1252 | 1253 | # First, decompose the table name into its part (e.g., 1254 | # "a.b.c" -> ["a", "b", "c"]) 1255 | nextChar = state.getNextChar() 1256 | let isTableArrayDef = nextChar == '[' 1257 | var tableNames: seq[string] 1258 | if isTableArrayDef: 1259 | tableNames = state.parseTableName(BracketType.double) 1260 | else: 1261 | state.pushBackChar(nextChar) 1262 | tableNames = state.parseTableName(BracketType.single) 1263 | 1264 | # Now create the proper (empty) data structure: either a 1265 | # table or an array of tables. Note that both functions 1266 | # update the "curTableRef" variable: they have to, since 1267 | # the TOML specification says that any "key = value" 1268 | # statement that follows is a child of the table we're 1269 | # defining right now, and we use "curTableRef" as a 1270 | # reference to the table that gets every next key/value 1271 | # definition. 1272 | if isTableArrayDef: 1273 | createOrAppendTableArrayDef(state, tableNames) 1274 | else: 1275 | createTableDef(state, tableNames) 1276 | 1277 | of '=': 1278 | raise(newTomlError(state, "key name missing")) 1279 | of '#', '.', ']': 1280 | raise(newTomlError(state, 1281 | "unexpected character " & escape($nextChar))) 1282 | of '\0': # EOF 1283 | return 1284 | else: 1285 | # Everything else marks the presence of a "key = value" pattern 1286 | state.pushBackChar(nextChar) 1287 | parseKeyValuePair(state) 1288 | 1289 | proc parseString*(tomlStr: string, fileName: string = ""): TomlValueRef = 1290 | ## Parses a string of TOML formatted data into a TOML table. The optional 1291 | ## filename is used for error messages. 1292 | let strStream = newStringStream(tomlStr) 1293 | try: 1294 | result = parseStream(strStream, fileName) 1295 | finally: 1296 | strStream.close() 1297 | 1298 | proc parseFile*(f: File, fileName: string = ""): TomlValueRef = 1299 | ## Parses a file of TOML formatted data into a TOML table. The optional 1300 | ## filename is used for error messages. 1301 | let fStream = newFileStream(f) 1302 | try: 1303 | result = parseStream(fStream, fileName) 1304 | finally: 1305 | fStream.close() 1306 | 1307 | proc parseFile*(fileName: string): TomlValueRef = 1308 | ## Parses the file found at fileName with TOML formatted data into a TOML 1309 | ## table. 1310 | let fStream = newFileStream(fileName, fmRead) 1311 | if not isNil(fStream): 1312 | try: 1313 | result = parseStream(fStream, fileName) 1314 | finally: 1315 | fStream.close() 1316 | else: 1317 | raise newException(IOError, "cannot open: " & fileName) 1318 | 1319 | 1320 | proc `$`*(val: TomlDate): string = 1321 | ## Converts the TOML date object into the ISO format read by the parser 1322 | result = ($val.year).align(4, '0') & "-" & ($val.month).align(2, '0') & "-" & 1323 | ($val.day).align(2, '0') 1324 | 1325 | proc `$`*(val: TomlTime): string = 1326 | ## Converts the TOML time object into the ISO format read by the parser 1327 | result = ($val.hour).align(2, '0') & ":" & 1328 | ($val.minute).align(2, '0') & ":" & ($val.second).align(2, '0') & 1329 | (if val.subsecond > 0: ("." & $val.subsecond) else: "") 1330 | 1331 | proc `$`*(val: TomlDateTime): string = 1332 | ## Converts the TOML date-time object into the ISO format read by the parser 1333 | result = $val.date & "T" & $val.time & 1334 | (if not val.shift: "" else: ( 1335 | (if val.zoneHourShift == 0 and val.zoneMinuteShift == 0: "Z" else: ( 1336 | ((if val.isShiftPositive: "+" else: "-") & 1337 | ($val.zoneHourShift).align(2, '0') & ":" & 1338 | ($val.zoneMinuteShift).align(2, '0')) 1339 | )) 1340 | )) 1341 | 1342 | proc toTomlString*(value: TomlValueRef): string 1343 | 1344 | proc `$`*(val: TomlValueRef): string = 1345 | ## Turns whatever value into a regular Nim value representtation 1346 | case val.kind 1347 | of TomlValueKind.None: 1348 | result = "nil" 1349 | of TomlValueKind.Int: 1350 | result = $val.intVal 1351 | of TomlValueKind.Float: 1352 | result = $val.floatVal 1353 | of TomlValueKind.Bool: 1354 | result = $val.boolVal 1355 | of TomlValueKind.Datetime: 1356 | result = $val.dateTimeVal 1357 | of TomlValueKind.Date: 1358 | result = $val.dateVal 1359 | of TomlValueKind.Time: 1360 | result = $val.timeVal 1361 | of TomlValueKind.String: 1362 | result = $val.stringVal 1363 | of TomlValueKind.Array: 1364 | result = "" 1365 | for elem in val.arrayVal: 1366 | result.add($(elem[])) 1367 | of TomlValueKind.Table: 1368 | result = val.toTomlString 1369 | 1370 | proc `$`*(val: TomlValue): string = 1371 | ## Turns whatever value into a type and value representation, used by ``dump`` 1372 | case val.kind 1373 | of TomlValueKind.None: 1374 | result = "none()" 1375 | of TomlValueKind.Int: 1376 | result = "int(" & $val.intVal & ")" 1377 | of TomlValueKind.Float: 1378 | result = "float(" & $val.floatVal & ")" 1379 | of TomlValueKind.Bool: 1380 | result = "boolean(" & $val.boolVal & ")" 1381 | of TomlValueKind.Datetime: 1382 | result = "datetime(" & $val.dateTimeVal & ")" 1383 | of TomlValueKind.Date: 1384 | result = "date(" & $val.dateVal & ")" 1385 | of TomlValueKind.Time: 1386 | result = "time(" & $val.timeVal & ")" 1387 | of TomlValueKind.String: 1388 | result = "string(\"" & $val.stringVal & "\")" 1389 | of TomlValueKind.Array: 1390 | result = "array(" 1391 | for elem in val.arrayVal: 1392 | result.add($(elem[])) 1393 | result.add(")") 1394 | of TomlValueKind.Table: 1395 | result = "table(" & $(len(val.tableVal)) & " elements)" 1396 | 1397 | proc dump*(table: TomlTableRef, indentLevel: int = 0) = 1398 | ## Dump out the entire table as it was parsed. This procedure is mostly 1399 | ## useful for debugging purposes 1400 | let space = spaces(indentLevel) 1401 | for key, val in pairs(table): 1402 | if val.kind == TomlValueKind.Table: 1403 | echo space & key & " = table" 1404 | dump(val.tableVal, indentLevel + 4) 1405 | elif (val.kind == TomlValueKind.Array and 1406 | val.arrayVal[0].kind == TomlValueKind.Table): 1407 | for idx, val in val.arrayVal: 1408 | echo space & key & "[" & $idx & "] = table" 1409 | dump(val.tableVal, indentLevel + 4) 1410 | else: 1411 | echo space & key & " = " & $(val[]) 1412 | 1413 | import json, sequtils 1414 | 1415 | proc toJson*(value: TomlValueRef): JsonNode 1416 | 1417 | proc toJson*(table: TomlTableRef): JsonNode = 1418 | ## Converts a TOML table to a JSON node. This uses the format specified in 1419 | ## the validation suite for it's output: 1420 | ## https://github.com/BurntSushi/toml-test#example-json-encoding 1421 | result = newJObject() 1422 | for key, value in pairs(table): 1423 | result[key] = value.toJson 1424 | 1425 | proc toJson*(value: TomlValueRef): JsonNode = 1426 | ## Converts a TOML value to a JSON node. This uses the format specified in 1427 | ## the validation suite for it's output: 1428 | ## https://github.com/BurntSushi/toml-test#example-json-encoding 1429 | case value.kind: 1430 | of TomlValueKind.Int: 1431 | %*{"type": "integer", "value": $value.intVal} 1432 | of TomlValueKind.Float: 1433 | if classify(value.floatVal) == fcNan: 1434 | if value.forcedSign != Pos: 1435 | %*{"type": "float", "value": $value.floatVal} 1436 | else: 1437 | %*{"type": "float", "value": "+" & $value.floatVal} 1438 | else: 1439 | %*{"type": "float", "value": $value.floatVal} 1440 | of TomlValueKind.Bool: 1441 | %*{"type": "bool", "value": $value.boolVal} 1442 | of TomlValueKind.Datetime: 1443 | if value.dateTimeVal.shift == false: 1444 | %*{"type": "datetime-local", "value": $value.dateTimeVal} 1445 | else: 1446 | %*{"type": "datetime", "value": $value.dateTimeVal} 1447 | of TomlValueKind.Date: 1448 | %*{"type": "date", "value": $value.dateVal} 1449 | of TomlValueKind.Time: 1450 | %*{"type": "time", "value": $value.timeVal} 1451 | of TomlValueKind.String: 1452 | %*{"type": "string", "value": newJString(value.stringVal)} 1453 | of TomlValueKind.Array: 1454 | if value.arrayVal.len == 0: 1455 | when defined(newtestsuite): 1456 | %[] 1457 | else: 1458 | %*{"type": "array", "value": []} 1459 | elif value.arrayVal[0].kind == TomlValueKind.Table: 1460 | %value.arrayVal.map(toJson) 1461 | else: 1462 | when defined(newtestsuite): 1463 | %*value.arrayVal.map(toJson) 1464 | else: 1465 | %*{"type": "array", "value": value.arrayVal.map(toJson)} 1466 | of TomlValueKind.Table: 1467 | value.tableVal.toJson 1468 | of TomlValueKind.None: 1469 | %*{"type": "ERROR"} 1470 | 1471 | proc toKey(str: string): string = 1472 | for c in str: 1473 | if (c notin {'a'..'z', 'A'..'Z', '0'..'9', '_', '-'}): 1474 | return "\"" & str & "\"" 1475 | str 1476 | 1477 | 1478 | proc toTomlString*(value: TomlTableRef, parents = ""): string = 1479 | ## Converts a TOML table to a TOML formatted string for output to a file. 1480 | result = "" 1481 | var subtables: seq[tuple[key: string, value: TomlValueRef]] = @[] 1482 | for key, value in pairs(value): 1483 | block outer: 1484 | if value.kind == TomlValueKind.Table: 1485 | subtables.add((key: key, value: value)) 1486 | elif value.kind == TomlValueKind.Array and 1487 | value.arrayVal.len > 0 and 1488 | value.arrayVal[0].kind == TomlValueKind.Table: 1489 | let tables = value.arrayVal.map(toTomlString) 1490 | for table in tables: 1491 | result = result & "[[" & key & "]]\n" & table & "\n" 1492 | else: 1493 | result = result & key.toKey & " = " & toTomlString(value) & "\n" 1494 | for kv in subtables: 1495 | let fullKey = (if parents.len > 0: parents & "." else: "") & kv.key.toKey 1496 | block outer: 1497 | for ikey, ivalue in pairs(kv.value.tableVal): 1498 | if ivalue.kind != TomlValueKind.Table: 1499 | result = result & "[" & fullKey & "]\n" & 1500 | kv.value.tableVal.toTomlString(fullKey) & "\n" 1501 | break outer 1502 | result = result & kv.value.tableVal.toTomlString(fullKey) 1503 | 1504 | proc toTomlString*(value: TomlValueRef): string = 1505 | ## Converts a TOML value to a TOML formatted string for output to a file. 1506 | case value.kind: 1507 | of TomlValueKind.Int: $value.intVal 1508 | of TomlValueKind.Float: $value.floatVal 1509 | of TomlValueKind.Bool: $value.boolVal 1510 | of TomlValueKind.Datetime: $value.dateTimeVal 1511 | of TomlValueKind.String: "\"" & value.stringVal & "\"" 1512 | of TomlValueKind.Array: 1513 | if value.arrayVal.len == 0: 1514 | "[]" 1515 | elif value.arrayVal[0].kind == TomlValueKind.Table: 1516 | value.arrayVal.map(toTomlString).join("\n") 1517 | else: 1518 | "[" & value.arrayVal.map(toTomlString).join(", ") & "]" 1519 | of TomlValueKind.Table: value.tableVal.toTomlString 1520 | else: 1521 | "UNKNOWN" 1522 | 1523 | proc newTString*(s: string): TomlValueRef = 1524 | ## Creates a new `TomlValueKind.String TomlValueRef`. 1525 | TomlValueRef(kind: TomlValueKind.String, stringVal: s) 1526 | 1527 | proc newTInt*(n: int64): TomlValueRef = 1528 | ## Creates a new `TomlValueKind.Int TomlValueRef`. 1529 | TomlValueRef(kind: TomlValueKind.Int, intVal: n) 1530 | 1531 | proc newTFloat*(n: float): TomlValueRef = 1532 | ## Creates a new `TomlValueKind.Float TomlValueRef`. 1533 | TomlValueRef(kind: TomlValueKind.Float, floatVal: n) 1534 | 1535 | proc newTBool*(b: bool): TomlValueRef = 1536 | ## Creates a new `TomlValueKind.Bool TomlValueRef`. 1537 | TomlValueRef(kind: TomlValueKind.Bool, boolVal: b) 1538 | 1539 | proc newTNull*(): TomlValueRef = 1540 | ## Creates a new `JNull TomlValueRef`. 1541 | TomlValueRef(kind: TomlValueKind.None) 1542 | 1543 | proc newTTable*(): TomlValueRef = 1544 | ## Creates a new `TomlValueKind.Table TomlValueRef` 1545 | result = TomlValueRef(kind: TomlValueKind.Table) 1546 | new(result.tableVal) 1547 | result.tableVal[] = initOrderedTable[string, TomlValueRef](4) 1548 | 1549 | proc newTArray*(): TomlValueRef = 1550 | ## Creates a new `TomlValueKind.Array TomlValueRef` 1551 | TomlValueRef(kind: TomlValueKind.Array, arrayVal: @[]) 1552 | 1553 | proc getStr*(n: TomlValueRef, default: string = ""): string = 1554 | ## Retrieves the string value of a `TomlValueKind.String TomlValueRef`. 1555 | ## 1556 | ## Returns ``default`` if ``n`` is not a ``TomlValueKind.String``, or if ``n`` is nil. 1557 | if n.isNil or n.kind != TomlValueKind.String: return default 1558 | else: return n.stringVal 1559 | 1560 | proc getInt*(n: TomlValueRef, default: int = 0): int = 1561 | ## Retrieves the int value of a `TomlValueKind.Int TomlValueRef`. 1562 | ## 1563 | ## Returns ``default`` if ``n`` is not a ``TomlValueKind.Int``, or if ``n`` is nil. 1564 | if n.isNil or n.kind != TomlValueKind.Int: return default 1565 | else: return int(n.intVal) 1566 | 1567 | proc getBiggestInt*(n: TomlValueRef, default: int64 = 0): int64 = 1568 | ## Retrieves the int64 value of a `TomlValueKind.Int TomlValueRef`. 1569 | ## 1570 | ## Returns ``default`` if ``n`` is not a ``TomlValueKind.Int``, or if ``n`` is nil. 1571 | if n.isNil or n.kind != TomlValueKind.Int: return default 1572 | else: return n.intVal 1573 | 1574 | proc getFloat*(n: TomlValueRef, default: float = 0.0): float = 1575 | ## Retrieves the float value of a `TomlValueKind.Float TomlValueRef`. 1576 | ## 1577 | ## Returns ``default`` if ``n`` is not a ``TomlValueKind.Float`` or ``TomlValueKind.Int``, or if ``n`` is nil. 1578 | if n.isNil: return default 1579 | case n.kind 1580 | of TomlValueKind.Float: return n.floatVal 1581 | of TomlValueKind.Int: return float(n.intVal) 1582 | else: return default 1583 | 1584 | proc getBool*(n: TomlValueRef, default: bool = false): bool = 1585 | ## Retrieves the bool value of a `TomlValueKind.Bool TomlValueRef`. 1586 | ## 1587 | ## Returns ``default`` if ``n`` is not a ``TomlValueKind.Bool``, or if ``n`` is nil. 1588 | if n.isNil or n.kind != TomlValueKind.Bool: return default 1589 | else: return n.boolVal 1590 | 1591 | proc getTable*(n: TomlValueRef, default = new(TomlTableRef)): TomlTableRef = 1592 | ## Retrieves the key, value pairs of a `TomlValueKind.Table TomlValueRef`. 1593 | ## 1594 | ## Returns ``default`` if ``n`` is not a ``TomlValueKind.Table``, or if ``n`` is nil. 1595 | if n.isNil or n.kind != TomlValueKind.Table: return default 1596 | else: return n.tableVal 1597 | 1598 | proc getElems*(n: TomlValueRef, default: seq[TomlValueRef] = @[]): seq[TomlValueRef] = 1599 | ## Retrieves the int value of a `TomlValueKind.Array TomlValueRef`. 1600 | ## 1601 | ## Returns ``default`` if ``n`` is not a ``TomlValueKind.Array``, or if ``n`` is nil. 1602 | if n.isNil or n.kind != TomlValueKind.Array: return default 1603 | else: return n.arrayVal 1604 | 1605 | proc add*(father, child: TomlValueRef) = 1606 | ## Adds `child` to a TomlValueKind.Array node `father`. 1607 | assert father.kind == TomlValueKind.Array 1608 | father.arrayVal.add(child) 1609 | 1610 | proc add*(obj: TomlValueRef, key: string, val: TomlValueRef) = 1611 | ## Sets a field from a `TomlValueKind.Table`. 1612 | assert obj.kind == TomlValueKind.Table 1613 | obj.tableVal[key] = val 1614 | 1615 | proc `?`*(s: string): TomlValueRef = 1616 | ## Generic constructor for TOML data. Creates a new `TomlValueKind.String TomlValueRef`. 1617 | TomlValueRef(kind: TomlValueKind.String, stringVal: s) 1618 | 1619 | proc `?`*(n: int64): TomlValueRef = 1620 | ## Generic constructor for TOML data. Creates a new `TomlValueKind.Int TomlValueRef`. 1621 | TomlValueRef(kind: TomlValueKind.Int, intVal: n) 1622 | 1623 | proc `?`*(n: float): TomlValueRef = 1624 | ## Generic constructor for TOML data. Creates a new `TomlValueKind.Float TomlValueRef`. 1625 | TomlValueRef(kind: TomlValueKind.Float, floatVal: n) 1626 | 1627 | proc `?`*(b: bool): TomlValueRef = 1628 | ## Generic constructor for TOML data. Creates a new `TomlValueKind.Bool TomlValueRef`. 1629 | TomlValueRef(kind: TomlValueKind.Bool, boolVal: b) 1630 | 1631 | proc `?`*(keyVals: openArray[tuple[key: string, val: TomlValueRef]]): TomlValueRef = 1632 | ## Generic constructor for TOML data. Creates a new `TomlValueKind.Table TomlValueRef` 1633 | if keyVals.len == 0: return newTArray() 1634 | result = newTTable() 1635 | for key, val in items(keyVals): result.tableVal[key] = val 1636 | 1637 | template `?`*(j: TomlValueRef): TomlValueRef = j 1638 | 1639 | proc `?`*[T](elements: openArray[T]): TomlValueRef = 1640 | ## Generic constructor for TOML data. Creates a new `TomlValueKind.Array TomlValueRef` 1641 | result = newTArray() 1642 | for elem in elements: result.add(?elem) 1643 | 1644 | when false: 1645 | # For 'consistency' we could do this, but that only pushes people further 1646 | # into that evil comfort zone where they can use Nim without understanding it 1647 | # causing problems later on. 1648 | proc `?`*(elements: set[bool]): TomlValueRef = 1649 | ## Generic constructor for TOML data. Creates a new `TomlValueKind.Table TomlValueRef`. 1650 | ## This can only be used with the empty set ``{}`` and is supported 1651 | ## to prevent the gotcha ``%*{}`` which used to produce an empty 1652 | ## TOML array. 1653 | result = newTTable() 1654 | assert false notin elements, "usage error: only empty sets allowed" 1655 | assert true notin elements, "usage error: only empty sets allowed" 1656 | 1657 | proc `?`*(o: object): TomlValueRef = 1658 | ## Generic constructor for TOML data. Creates a new `TomlValueKind.Table TomlValueRef` 1659 | result = newTTable() 1660 | for k, v in o.fieldPairs: result[k] = ?v 1661 | 1662 | proc `?`*(o: ref object): TomlValueRef = 1663 | ## Generic constructor for TOML data. Creates a new `TomlValueKind.Table TomlValueRef` 1664 | if o.isNil: 1665 | result = newTNull() 1666 | else: 1667 | result = ?(o[]) 1668 | 1669 | proc `?`*(o: enum): TomlValueRef = 1670 | ## Construct a TomlValueRef that represents the specified enum value as a 1671 | ## string. Creates a new ``TomlValueKind.String TomlValueRef``. 1672 | result = ?($o) 1673 | 1674 | import macros 1675 | 1676 | proc toToml(x: NimNode): NimNode {.compileTime.} = 1677 | case x.kind 1678 | of nnkBracket: # array 1679 | if x.len == 0: return newCall(bindSym"newTArray") 1680 | result = newNimNode(nnkBracket) 1681 | for i in 0 ..< x.len: 1682 | result.add(toToml(x[i])) 1683 | result = newCall(bindSym("?", brOpen), result) 1684 | of nnkTableConstr: # object 1685 | if x.len == 0: return newCall(bindSym"newTTable") 1686 | result = newNimNode(nnkTableConstr) 1687 | for i in 0 ..< x.len: 1688 | x[i].expectKind nnkExprColonExpr 1689 | result.add newTree(nnkExprColonExpr, x[i][0], toToml(x[i][1])) 1690 | result = newCall(bindSym("?", brOpen), result) 1691 | of nnkCurly: # empty object 1692 | x.expectLen(0) 1693 | result = newCall(bindSym"newTTable") 1694 | of nnkNilLit: 1695 | result = newCall(bindSym"newTNull") 1696 | else: 1697 | result = newCall(bindSym("?", brOpen), x) 1698 | 1699 | macro `?*`*(x: untyped): untyped = 1700 | ## Convert an expression to a TomlValueRef directly, without having to specify 1701 | ## `?` for every element. 1702 | result = toToml(x) 1703 | echo result.repr 1704 | 1705 | proc toTomlValue(x: NimNode): NimNode {.compileTime.} = 1706 | newCall(bindSym("?", brOpen), x) 1707 | 1708 | proc toTomlNew(x: NimNode): NimNode {.compileTime.} = 1709 | echo x.treeRepr 1710 | var 1711 | i = 0 1712 | curTable: NimNode = nil 1713 | while i < x.len: 1714 | echo x[i].kind 1715 | case x[i].kind: 1716 | of nnkAsgn: 1717 | if curTable.isNil: 1718 | curTable = newNimNode(nnkTableConstr) 1719 | result = curTable 1720 | curTable.add newTree(nnkExprColonExpr, newLit($x[i][0]), toTomlValue(x[i][1])) 1721 | of nnkBracket: 1722 | if curTable.isNil: 1723 | curTable = newNimNode(nnkTableConstr) 1724 | result = curTable 1725 | else: 1726 | var table = newNimNode(nnkTableConstr) 1727 | result.add newTree(nnkExprColonExpr, newLit($x[i][0]), newCall(bindSym("?", brOpen), table)) 1728 | curTable = table 1729 | else: discard 1730 | i += 1 1731 | result = newCall(bindSym("?", brOpen), result) 1732 | 1733 | macro `parseToml`*(x: untyped): untyped = 1734 | ## Convert an expression to a TomlValueRef directly, without having to specify 1735 | ## `?` for every element. 1736 | result = toTomlNew(x) 1737 | echo result.repr 1738 | 1739 | func `==`* (a, b: TomlValueRef): bool = 1740 | ## Check two nodes for equality 1741 | if a.isNil: 1742 | if b.isNil: return true 1743 | return false 1744 | elif b.isNil or a.kind != b.kind: 1745 | return false 1746 | else: 1747 | case a.kind 1748 | of TomlValueKind.String: 1749 | result = a.stringVal == b.stringVal 1750 | of TomlValueKind.Int: 1751 | result = a.intVal == b.intVal 1752 | of TomlValueKind.Float: 1753 | result = a.floatVal == b.floatVal 1754 | of TomlValueKind.Bool: 1755 | result = a.boolVal == b.boolVal 1756 | of TomlValueKind.None: 1757 | result = true 1758 | of TomlValueKind.Array: 1759 | result = a.arrayVal == b.arrayVal 1760 | of TomlValueKind.Table: 1761 | # we cannot use OrderedTable's equality here as 1762 | # the order does not matter for equality here. 1763 | if a.tableVal.len != b.tableVal.len: return false 1764 | for key, val in a.tableVal: 1765 | if not b.tableVal.hasKey(key): return false 1766 | {.noSideEffect.}: 1767 | if b.tableVal[key] != val: return false 1768 | result = true 1769 | of TomlValueKind.DateTime: 1770 | result = 1771 | a.dateTimeVal.date.year == b.dateTimeVal.date.year and 1772 | a.dateTimeVal.date.month == b.dateTimeVal.date.month and 1773 | a.dateTimeVal.date.day == b.dateTimeVal.date.day and 1774 | a.dateTimeVal.time.hour == b.dateTimeVal.time.hour and 1775 | a.dateTimeVal.time.minute == b.dateTimeVal.time.minute and 1776 | a.dateTimeVal.time.second == b.dateTimeVal.time.second and 1777 | a.dateTimeVal.time.subsecond == b.dateTimeVal.time.subsecond and 1778 | a.dateTimeVal.shift == b.dateTimeVal.shift and 1779 | (a.dateTimeVal.shift == true and 1780 | (a.dateTimeVal.isShiftPositive == b.dateTimeVal.isShiftPositive and 1781 | a.dateTimeVal.zoneHourShift == b.dateTimeVal.zoneHourShift and 1782 | a.dateTimeVal.zoneMinuteShift == b.dateTimeVal.zoneMinuteShift)) or 1783 | a.dateTimeVal.shift == false 1784 | of TomlValueKind.Date: 1785 | result = 1786 | a.dateVal.year == b.dateVal.year and 1787 | a.dateVal.month == b.dateVal.month and 1788 | a.dateVal.day == b.dateVal.day 1789 | of TomlValueKind.Time: 1790 | result = 1791 | a.timeVal.hour == b.timeVal.hour and 1792 | a.timeVal.minute == b.timeVal.minute and 1793 | a.timeVal.second == b.timeVal.second and 1794 | a.timeVal.subsecond == b.timeVal.subsecond 1795 | 1796 | import hashes 1797 | 1798 | proc hash*(n: OrderedTable[string, TomlValueRef]): Hash {.noSideEffect.} 1799 | 1800 | proc hash*(n: TomlValueRef): Hash {.noSideEffect.} = 1801 | ## Compute the hash for a TOML node 1802 | case n.kind 1803 | of TomlValueKind.Array: 1804 | result = hash(n.arrayVal) 1805 | of TomlValueKind.Table: 1806 | result = hash(n.tableVal[]) 1807 | of TomlValueKind.Int: 1808 | result = hash(n.intVal) 1809 | of TomlValueKind.Float: 1810 | result = hash(n.floatVal) 1811 | of TomlValueKind.Bool: 1812 | result = hash(n.boolVal.int) 1813 | of TomlValueKind.String: 1814 | result = hash(n.stringVal) 1815 | of TomlValueKind.None: 1816 | result = Hash(0) 1817 | of TomlValueKind.DateTime: 1818 | result = hash($n.dateTimeVal) 1819 | of TomlValueKind.Date: 1820 | result = hash($n.dateVal) 1821 | of TomlValueKind.Time: 1822 | result = hash($n.timeVal) 1823 | 1824 | proc hash*(n: OrderedTable[string, TomlValueRef]): Hash = 1825 | for key, val in n: 1826 | result = result xor (hash(key) !& hash(val)) 1827 | result = !$result 1828 | 1829 | proc len*(n: TomlValueRef): int = 1830 | ## If `n` is a `TomlValueKind.Array`, it returns the number of elements. 1831 | ## If `n` is a `TomlValueKind.Table`, it returns the number of pairs. 1832 | ## Else it returns 0. 1833 | case n.kind 1834 | of TomlValueKind.Array: result = n.arrayVal.len 1835 | of TomlValueKind.Table: result = n.tableVal.len 1836 | else: discard 1837 | 1838 | proc `[]`*(node: TomlValueRef, name: string): TomlValueRef {.inline.} = 1839 | ## Gets a field from a `TomlValueKind.Table`, which must not be nil. 1840 | ## If the value at `name` does not exist, raises KeyError. 1841 | assert(not isNil(node)) 1842 | assert(node.kind == TomlValueKind.Table) 1843 | result = node.tableVal[name] 1844 | 1845 | proc `[]`*(node: TomlValueRef, index: int): TomlValueRef {.inline.} = 1846 | ## Gets the node at `index` in an Array. Result is undefined if `index` 1847 | ## is out of bounds, but as long as array bound checks are enabled it will 1848 | ## result in an exception. 1849 | assert(not isNil(node)) 1850 | assert(node.kind == TomlValueKind.Array) 1851 | return node.arrayVal[index] 1852 | 1853 | proc hasKey*(node: TomlValueRef, key: string): bool = 1854 | ## Checks if `key` exists in `node`. 1855 | assert(node.kind == TomlValueKind.Table) 1856 | result = node.tableVal.hasKey(key) 1857 | 1858 | proc contains*(node: TomlValueRef, key: string): bool = 1859 | ## Checks if `key` exists in `node`. 1860 | assert(node.kind == TomlValueKind.Table) 1861 | node.tableVal.hasKey(key) 1862 | 1863 | proc contains*(node: TomlValueRef, val: TomlValueRef): bool = 1864 | ## Checks if `val` exists in array `node`. 1865 | assert(node.kind == TomlValueKind.Array) 1866 | find(node.arrayVal, val) >= 0 1867 | 1868 | proc existsKey*(node: TomlValueRef, key: string): bool {.deprecated.} = node.hasKey(key) 1869 | ## Deprecated for `hasKey` 1870 | 1871 | proc `[]=`*(obj: TomlValueRef, key: string, val: TomlValueRef) {.inline.} = 1872 | ## Sets a field from a `TomlValueKind.Table`. 1873 | assert(obj.kind == TomlValueKind.Table) 1874 | obj.tableVal[key] = val 1875 | 1876 | proc `{}`*(node: TomlValueRef, keys: varargs[string]): TomlValueRef = 1877 | ## Traverses the node and gets the given value. If any of the 1878 | ## keys do not exist, returns ``nil``. Also returns ``nil`` if one of the 1879 | ## intermediate data structures is not an object. 1880 | result = node 1881 | for key in keys: 1882 | if isNil(result) or result.kind != TomlValueKind.Table: 1883 | return nil 1884 | result = result.tableVal.getOrDefault(key) 1885 | 1886 | proc getOrDefault*(node: TomlValueRef, key: string): TomlValueRef = 1887 | ## Gets a field from a `node`. If `node` is nil or not an object or 1888 | ## value at `key` does not exist, returns nil 1889 | if not isNil(node) and node.kind == TomlValueKind.Table: 1890 | result = node.tableVal.getOrDefault(key) 1891 | 1892 | template simpleGetOrDefault*{`{}`(node, [key])}(node: TomlValueRef, key: string): TomlValueRef = node.getOrDefault(key) 1893 | 1894 | proc `{}=`*(node: TomlValueRef, keys: varargs[string], value: TomlValueRef) = 1895 | ## Traverses the node and tries to set the value at the given location 1896 | ## to ``value``. If any of the keys are missing, they are added. 1897 | var node = node 1898 | for i in 0..(keys.len-2): 1899 | if not node.hasKey(keys[i]): 1900 | node[keys[i]] = newTTable() 1901 | node = node[keys[i]] 1902 | node[keys[keys.len-1]] = value 1903 | 1904 | proc delete*(obj: TomlValueRef, key: string) = 1905 | ## Deletes ``obj[key]``. 1906 | assert(obj.kind == TomlValueKind.Table) 1907 | if not obj.tableVal.hasKey(key): 1908 | raise newException(IndexDefect, "key not in object") 1909 | obj.tableVal.del(key) 1910 | 1911 | proc copy*(p: TomlValueRef): TomlValueRef = 1912 | ## Performs a deep copy of `a`. 1913 | case p.kind 1914 | of TomlValueKind.String: 1915 | result = newTString(p.stringVal) 1916 | of TomlValueKind.Int: 1917 | result = newTInt(p.intVal) 1918 | of TomlValueKind.Float: 1919 | result = newTFloat(p.floatVal) 1920 | of TomlValueKind.Bool: 1921 | result = newTBool(p.boolVal) 1922 | of TomlValueKind.None: 1923 | result = newTNull() 1924 | of TomlValueKind.Table: 1925 | result = newTTable() 1926 | for key, val in pairs(p.tableVal): 1927 | result.tableVal[key] = copy(val) 1928 | of TomlValueKind.Array: 1929 | result = newTArray() 1930 | for i in items(p.arrayVal): 1931 | result.arrayVal.add(copy(i)) 1932 | of TomlValueKind.DateTime: 1933 | new(result) 1934 | result[] = p[] 1935 | of TomlValueKind.Date: 1936 | new(result) 1937 | result[] = p[] 1938 | of TomlValueKind.Time: 1939 | new(result) 1940 | result[] = p[] --------------------------------------------------------------------------------