├── .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 |
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 |
32 | username
33 | hostname
34 | uptime
35 | os
36 | kernel
37 | desktop
38 | shell
39 | terminal
40 | memory
41 | battery
42 | disk space
43 | cpu info
44 | packages
45 | weather
46 | colors
47 |
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[]
--------------------------------------------------------------------------------