diff --git a/.github/workflows/clear-workflow-cache.yml b/.github/workflows/clear_workflow_cache.yml similarity index 100% rename from .github/workflows/clear-workflow-cache.yml rename to .github/workflows/clear_workflow_cache.yml diff --git a/.github/workflows/post-release.yml b/.github/workflows/post_release.yml similarity index 100% rename from .github/workflows/post-release.yml rename to .github/workflows/post_release.yml diff --git a/.github/workflows/test-docs.yml b/.github/workflows/test_docs.yml similarity index 97% rename from .github/workflows/test-docs.yml rename to .github/workflows/test_docs.yml index 128f6e48..c91b6389 100644 --- a/.github/workflows/test-docs.yml +++ b/.github/workflows/test_docs.yml @@ -19,7 +19,7 @@ jobs: uses: fkirc/skip-duplicate-actions@f75f66ce1886f00957d99748a42c724f4330bdcf # v5.3.1 with: skip_after_successful_duplicate: "true" - paths: '["docs/**", ".github/workflows/docs.yml", ".github/workflows/test-docs.yml"]' + paths: '["docs/**", ".github/workflows/docs.yml", ".github/workflows/test_docs.yml"]' do_not_skip: '["workflow_dispatch"]' test-build-documentation: diff --git a/.github/workflows/validate_schema.yml b/.github/workflows/validate_schema.yml new file mode 100644 index 00000000..d14d7f66 --- /dev/null +++ b/.github/workflows/validate_schema.yml @@ -0,0 +1,56 @@ +# Workflow to validate the latest schema. + +name: "validate schema" +on: + workflow_dispatch: + pull_request: + push: + branches: + - main + paths: + - "schema/**" + - ".github/workflows/validate_schema.yml" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' || github.repository != 'ClementTsang/bottom' }} + +jobs: + pre-job: + runs-on: ubuntu-latest + outputs: + should_skip: ${{ steps.skip_check.outputs.should_skip }} + steps: + - id: skip_check + uses: fkirc/skip-duplicate-actions@f75f66ce1886f00957d99748a42c724f4330bdcf # v5.3.1 + with: + skip_after_successful_duplicate: "true" + paths: '["schema/**", ".github/workflows/validate_schema.yml"]' + do_not_skip: '["workflow_dispatch"]' + + test-build-documentation: + name: Test validating schema + needs: pre-job + if: ${{ needs.pre-job.outputs.should_skip != 'true' }} + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + with: + fetch-depth: 0 + + - uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 + with: + python-version: 3.11 + + - name: Install Python dependencies + run: pip install -r scripts/schema/requirements.txt + + - name: Test nightly validates on valid sample configs + run: | + python3 scripts/schema/validator.py -s ./schema/nightly/bottom.json -f ./sample_configs/default_config.toml + python3 scripts/schema/validator.py -s ./schema/nightly/bottom.json -f ./sample_configs/demo_config.toml + + - name: Test nightly catches on a bad sample config + run: | + python3 scripts/schema/validator.py -s ./schema/nightly/bottom.json -f scripts/schema/bad_file.toml --should_fail diff --git a/.gitignore b/.gitignore index 2e446385..a89e65d3 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,5 @@ supply-chain/ # samply profiling profile.json + +**/venv/ \ No newline at end of file diff --git a/schema/v1.0/bottom.json b/schema/v1.0/bottom.json new file mode 100644 index 00000000..807862d9 --- /dev/null +++ b/schema/v1.0/bottom.json @@ -0,0 +1,385 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://github.com/ClementTsang/bottom/tree/main/schema/nightly/schema.json", + "$comment": "https://clementtsang.github.io/bottom/nightly/configuration/config-file", + "title": "Schema for bottom's configs (v1)", + "type": "object", + "definitions": { + "row": { + "items": { + "properties": { + "ratio": { + "default": 1, + "type": "integer" + }, + "type": { + "enum": ["cpu", "mem", "proc", "net", "temp", "disk", "empty"], + "type": "string" + }, + "default": { + "default": true, + "type": "boolean" + } + }, + "patternProperties": { + "row(.child)+": { + "$ref": "#/definitions/row" + } + }, + "type": "object" + }, + "type": "array" + }, + "filter": { + "description": "hide specific temperature sensors, network interfaces, and disks using filters", + "properties": { + "is_list_ignored": { + "default": true, + "type": "boolean" + }, + "list": { + "type": "array" + }, + "regex": { + "default": true, + "type": "boolean" + }, + "case_sensitive": { + "default": false, + "type": "boolean" + }, + "whole_word": { + "default": false, + "type": "boolean" + } + }, + "type": "object" + } + }, + "properties": { + "flags": { + "description": "This group of options represents a command-line flag/option. Flags explicitly added when running (ie: btm -a) will override this config file if an option is also set here", + "properties": { + "hide_avg_cpu": { + "default": false, + "description": "Whether to hide the average cpu entry", + "type": "boolean" + }, + "dot_marker": { + "default": false, + "description": "Whether to use dot markers rather than braille", + "type": "boolean" + }, + "rate": { + "default": 1000, + "description": "The update rate of the application", + "type": "integer" + }, + "left_legend": { + "default": false, + "description": "Whether to put the CPU legend to the left", + "type": "boolean" + }, + "current_usage": { + "default": false, + "description": "Whether to set CPU% on a process to be based on the total CPU or just current usage", + "type": "boolean" + }, + "unnormalized_cpu": { + "default": false, + "description": "Whether to set CPU% on a process to be based on the total CPU or per-core CPU% (not divided by the number of cpus)", + "type": "boolean" + }, + "group_processes": { + "default": false, + "description": "Whether to group processes with the same name together by default", + "type": "boolean" + }, + "case_sensitive": { + "default": false, + "description": "Whether to make process searching case sensitive by default", + "type": "boolean" + }, + "whole_word": { + "default": false, + "description": "Whether to make process searching look for matching the entire word by default", + "type": "boolean" + }, + "regex": { + "default": false, + "description": "Whether to make process searching use regex by default", + "type": "boolean" + }, + "temperature_type": { + "default": "k", + "enum": ["k", "f", "c", "kelvin", "fahrenheit", "celsius"], + "description": "Defaults to Celsius", + "type": "string" + }, + "default_time_value": { + "default": 60000, + "description": "The default time interval in milliseconds", + "type": "integer" + }, + "time_delta": { + "default": 15000, + "description": "The time delta on each zoom in/out action in milliseconds", + "type": "integer" + }, + "hide_time": { + "default": false, + "description": "Hides the time scale", + "type": "boolean" + }, + "default_widget_type": { + "default": "proc", + "description": "Override layout default widget", + "type": "string" + }, + "default_widget_count": { + "default": 1, + "description": "Override layout default widget", + "type": "integer" + }, + "expanded_on_startup": { + "default": true, + "description": "Expand selected widget upon starting the app", + "type": "boolean" + }, + "basic": { + "default": false, + "description": "Use basic mode", + "type": "boolean" + }, + "use_old_network_legend": { + "default": false, + "description": "Use the old network legend style", + "type": "boolean" + }, + "hide_table_gap": { + "default": false, + "description": "Remove space in tables", + "type": "boolean" + }, + "battery": { + "default": false, + "description": "Show the battery widgets", + "type": "boolean" + }, + "disable_click": { + "default": false, + "description": "Disable mouse clicks", + "type": "boolean" + }, + "color": { + "default": "default", + "enum": [ + "default", + "default-light", + "gruvbox", + "gruvbox-light", + "nord", + "nord-light" + ], + "description": "Built-in themes", + "type": "string" + }, + "mem_as_value": { + "default": false, + "description": "Show memory values in the processes widget as values by default", + "type": "boolean" + }, + "tree": { + "default": false, + "description": "Show tree mode by default in the processes widget", + "type": "boolean" + }, + "show_table_scroll_position": { + "default": false, + "description": "Shows an indicator in table widgets tracking where in the list you are", + "type": "boolean" + }, + "process_command": { + "default": false, + "description": "Show processes as their commands by default in the process widget", + "type": "boolean" + }, + "network_use_binary_prefix": { + "default": false, + "description": "Displays the network widget with binary prefixes", + "type": "boolean" + }, + "network_use_bytes": { + "default": false, + "description": "Displays the network widget using bytes", + "type": "boolean" + }, + "network_use_log": { + "default": false, + "description": "Displays the network widget with a log scale", + "type": "boolean" + }, + "disable_advanced_kill": { + "default": false, + "description": "Hides advanced options to stop a process on Unix-like systems", + "type": "boolean" + }, + "enable_gpu_memory": { + "default": false, + "description": "Shows GPU(s) memory", + "type": "boolean" + }, + "retention": { + "default": "10m", + "description": "How much data is stored at once in terms of time", + "type": "string" + } + }, + "type": "object" + }, + "colors": { + "description": "These are all the components that support custom theming. Note that colour support will depend on terminal support", + "properties": { + "table_header_color": { + "default": "LightBlue", + "description": "Represents the colour of table headers (processes, CPU, disks, temperature)", + "type": "string" + }, + "widget_title_color": { + "default": "Gray", + "description": "Represents the colour of the label each widget has", + "type": "string" + }, + "avg_cpu_color": { + "default": "Red", + "description": "Represents the average CPU color", + "type": "string" + }, + "cpu_core_colors": { + "items": { + "uniqueItems": true, + "minItems": 1, + "type": "string" + }, + "default": [ + "LightMagenta", + "LightYellow", + "LightCyan", + "LightGreen", + "LightBlue", + "LightRed", + "Cyan", + "Green", + "Blue", + "Red" + ], + "description": "Represents the colour the core will use in the CPU legend and graph", + "type": "array" + }, + "ram_color": { + "default": "LightMagenta", + "description": "Represents the colour RAM will use in the memory legend and graph", + "type": "string" + }, + "swap_color": { + "default": "LightYellow", + "description": "Represents the colour SWAP will use in the memory legend and graph", + "type": "string" + }, + "arc_color": { + "default": "LightCyan", + "description": "Represents the colour ARC will use in the memory legend and graph", + "type": "string" + }, + "gpu_core_colors": { + "items": { + "uniqueItems": true, + "minItems": 1, + "type": "string" + }, + "default": [ + "LightGreen", + "LightBlue", + "LightRed", + "Cyan", + "Green", + "Blue", + "Red" + ], + "description": "Represents the colour the GPU will use in the memory legend and graph", + "type": "array" + }, + "rx_color": { + "default": "LightCyan", + "description": "Represents the colour rx will use in the network legend and graph", + "type": "string" + }, + "tx_color": { + "default": "LightGreen", + "description": "Represents the colour tx will use in the network legend and graph", + "type": "string" + }, + "border_color": { + "default": "Gray", + "description": "Represents the colour of the border of unselected widgets", + "type": "string" + }, + "highlighted_border_color": { + "default": "LightBlue", + "description": "Represents the colour of the border of selected widgets", + "type": "string" + }, + "text_color": { + "default": "Gray", + "description": "Represents the colour of most text", + "type": "string" + }, + "selected_text_color": { + "default": "Black", + "description": "Represents the colour of text that is selected", + "type": "string" + }, + "selected_bg_color": { + "default": "LightBlue", + "description": "Represents the background colour of text that is selected", + "type": "string" + }, + "graph_color": { + "default": "Gray", + "description": "Represents the colour of the lines and text of the graph", + "type": "string" + }, + "high_battery_color": { + "default": "green", + "description": "Represents the colours of the battery based on charge", + "type": "string" + }, + "medium_battery_color": { + "default": "yellow", + "description": "Represents the colours of the battery based on charge", + "type": "string" + }, + "low_battery_color": { + "default": "red", + "description": "Represents the colours of the battery based on charge", + "type": "string" + } + } + }, + "row": { + "$ref": "#/definitions/row" + }, + "disk_filter": { + "$ref": "#/definitions/filter" + }, + "mount_filter": { + "$ref": "#/definitions/filter" + }, + "temp_filter": { + "$ref": "#/definitions/filter" + }, + "net_filter": { + "$ref": "#/definitions/filter" + } + } +} diff --git a/scripts/schema/bad_file.toml b/scripts/schema/bad_file.toml new file mode 100644 index 00000000..8107aef5 --- /dev/null +++ b/scripts/schema/bad_file.toml @@ -0,0 +1,2 @@ +[flags] +hide_avg_cpu = 'bad' diff --git a/scripts/schema/requirements.txt b/scripts/schema/requirements.txt new file mode 100644 index 00000000..1db290b1 --- /dev/null +++ b/scripts/schema/requirements.txt @@ -0,0 +1,2 @@ +jsonschema-rs == 0.17.1 +toml == 0.10.2 \ No newline at end of file diff --git a/scripts/schema/validator.py b/scripts/schema/validator.py new file mode 100644 index 00000000..d537f928 --- /dev/null +++ b/scripts/schema/validator.py @@ -0,0 +1,55 @@ +#!/bin/python3 + +# A simple script to validate that a schema is valid for a file. + +import argparse +import toml +import jsonschema_rs + + +def main(): + parser = argparse.ArgumentParser( + description="Validates a file against a JSON schema" + ) + parser.add_argument( + "-f", "--file", type=str, required=True, help="The file to check." + ) + parser.add_argument( + "-s", "--schema", type=str, required=True, help="The schema to use." + ) + parser.add_argument( + "--should_fail", + required=False, + action="store_true", + help="Whether the checked file should fail.", + ) + args = parser.parse_args() + + file = args.file + schema = args.schema + should_fail = args.should_fail + + with open(file) as f, open(schema) as s: + try: + validator = jsonschema_rs.JSONSchema.from_str(s.read()) + except: + print("Coudln't create validator.") + exit() + + is_valid = validator.is_valid(toml.load(f)) + if is_valid: + if should_fail: + print("Fail!") + exit(1) + else: + print("All good!") + else: + if should_fail: + print("Caught error, good!") + else: + print("Fail!") + exit(1) + + +if __name__ == "__main__": + main()