├── CHANGELOG.md ├── lib ├── debug.dart ├── globals.dart ├── unescape_json_string.dart ├── prompts_instruct.dart ├── wrapper_script.dart ├── cache.dart ├── ansi_codes.dart ├── request_ollama_explain.dart ├── request_ollama_instruct.dart ├── system_information.dart ├── config.dart ├── request_openai_explain.dart ├── request_openai_instruct.dart ├── prompts_explain.dart ├── arguments.dart ├── get_latest_version.dart ├── installation_and_update.dart └── ter_print.dart ├── .gitignore ├── pubspec.yaml ├── .vscode └── launch.json ├── LICENSE ├── analysis_options.yaml ├── .github └── workflows │ └── dart_build.yml ├── README.md ├── bin └── ht.dart └── pubspec.lock /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.0.0 2 | 3 | - Initial version. 4 | -------------------------------------------------------------------------------- /lib/debug.dart: -------------------------------------------------------------------------------- 1 | import 'globals.dart'; 2 | import 'ansi_codes.dart'; 3 | 4 | void dbg(String text) { 5 | if (debug) print("$acBlue dbg:$acReset $acYellow$text$acReset"); 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Files and directories created by pub. 2 | .dart_tool/ 3 | .packages 4 | 5 | # Conventional directory for build output. 6 | build/ 7 | 8 | # compiled binary 9 | bin/ht.exe 10 | 11 | # personal journal 12 | docs/JOURNAL 13 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: ht 2 | description: a shell command that answers your questions about shell commands. 3 | version: 1.0.0 4 | homepage: https://github.com/catallo/ht/ 5 | environment: 6 | sdk: '>=2.18.3 <3.0.0' 7 | 8 | # dependencies: 9 | # path: ^1.8.0 10 | dev_dependencies: 11 | lints: ^2.0.0 12 | test: ^1.16.0 13 | dependencies: 14 | compute: ^1.0.2 15 | http: ^1.1.0 16 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "ht", 9 | "request": "launch", 10 | "type": "dart" 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /lib/globals.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:ht/config.dart'; 3 | 4 | bool debug = false; 5 | 6 | const version = "2.3.11"; 7 | const compileDate = "2024-01-06"; 8 | 9 | String os = "Linux"; 10 | String distro = "Debian derivate"; 11 | String uname = "-"; 12 | String shell = ""; 13 | 14 | String model = "gpt-3.5-turbo"; 15 | final double temp = 0.0; 16 | 17 | String? openAIapiKey = "notset"; 18 | 19 | String line = ""; 20 | 21 | var config = Config(); 22 | 23 | String home = Platform.environment['HOME'] ?? ""; 24 | 25 | String htPath = "$home/.config/ht/"; 26 | -------------------------------------------------------------------------------- /lib/unescape_json_string.dart: -------------------------------------------------------------------------------- 1 | String unescapeJsonString(String input) { 2 | var unescaped = input 3 | .replaceAll(r'\"', '"') 4 | .replaceAll(r'\\', r'\') 5 | .replaceAll(r'\n', '\n') 6 | .replaceAll(r'\t', '\t') 7 | .replaceAll(r'\b', '\b') 8 | .replaceAll(r'\f', '\f') 9 | .replaceAll(r'\r', '\r'); 10 | 11 | // Handle unicode character escapes 12 | RegExp unicodeEscape = RegExp(r'\\u[0-9a-fA-F]{4}'); 13 | unescaped = unescaped.replaceAllMapped(unicodeEscape, (match) { 14 | var hexCode = match[0]!.substring(2); 15 | var charCode = int.parse(hexCode, radix: 16); 16 | return String.fromCharCode(charCode); 17 | }); 18 | 19 | return unescaped; 20 | } 21 | -------------------------------------------------------------------------------- /lib/prompts_instruct.dart: -------------------------------------------------------------------------------- 1 | import 'package:ht/globals.dart'; 2 | 3 | String promptInstSystem = 4 | "You're an assistant for using $shell shell on $distro. You always answer with only the command IN ONE LINE without any further explanation! If you really and under no circumstances can't answer with a command, explain why but start with '🤖 ...'"; 5 | 6 | String promptInstUser1 = 7 | "$distro $os command to replace every IP address in file logfile with 192.168.0.1"; 8 | 9 | String promptInstAssistant1 = 10 | "sed -i 's/[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}/192.168.0.1/g' logfile"; 11 | 12 | String promptInstUser2 = "$distro $os command to remove /"; 13 | 14 | String promptInstAssistant2 = "sudo rm -rf /"; 15 | 16 | String promptInstUser = "$distro $os command to "; 17 | -------------------------------------------------------------------------------- /lib/wrapper_script.dart: -------------------------------------------------------------------------------- 1 | import 'globals.dart'; 2 | 3 | String wrapperScript = ''' 4 | #!/bin/sh 5 | # ht v$version ($compileDate) 6 | 7 | DOWNLOADS_FOLDER="\$(dirname "\$0")/download" 8 | DESTINATION_FILE="\$(dirname "\$0")/ht.bin" 9 | 10 | # check for updated version 11 | if [ -f "\$DOWNLOADS_FOLDER"/ht_* ]; then 12 | mv "\$DOWNLOADS_FOLDER"/ht_* "\$DESTINATION_FILE" 13 | rm -rf "\$DOWNLOADS_FOLDER"/* 14 | fi 15 | 16 | if [ "\$1" = "x" ] || [ "\$1" = "execute" ]; then 17 | if [ -f "\$(dirname "\$0")/last_response" ]; then 18 | "\$(dirname "\$0")/last_response" 19 | else 20 | echo " 21 | 🤖 My previous answer wasn't a valid shell command. Please generate a new command. 22 | " 23 | fi 24 | else 25 | "\$(dirname "\$0")/ht.bin" "\$@" 26 | fi 27 | '''; 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright © 2023 Sandro Catallo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all 8 | copies or substantial portions of the Software. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the static analysis results for your project (errors, 2 | # warnings, and lints). 3 | # 4 | # This enables the 'recommended' set of lints from `package:lints`. 5 | # This set helps identify many issues that may lead to problems when running 6 | # or consuming Dart code, and enforces writing Dart using a single, idiomatic 7 | # style and format. 8 | # 9 | # If you want a smaller set of lints you can change this to specify 10 | # 'package:lints/core.yaml'. These are just the most critical lints 11 | # (the recommended set includes the core lints). 12 | # The core lints are also what is used by pub.dev for scoring packages. 13 | 14 | include: package:lints/recommended.yaml 15 | 16 | # Uncomment the following section to specify additional rules. 17 | 18 | # linter: 19 | # rules: 20 | # - camel_case_types 21 | 22 | # analyzer: 23 | # exclude: 24 | # - path/to/excluded/files/** 25 | 26 | # For more information about the core and recommended set of lints, see 27 | # https://dart.dev/go/core-lints 28 | 29 | # For additional information about configuring this file, see 30 | # https://dart.dev/guides/language/analysis-options 31 | -------------------------------------------------------------------------------- /lib/cache.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'dart:convert'; 3 | 4 | import 'package:ht/globals.dart'; 5 | 6 | class Cache { 7 | Cache(this.prompt, this.response); 8 | 9 | String prompt; 10 | String response; 11 | 12 | bool save() { 13 | if (response == "null") { 14 | print("response is null"); 15 | return false; 16 | } 17 | 18 | // check if cache exists. if not, create it 19 | if (!File('${htPath}cache').existsSync()) { 20 | try { 21 | //print("creating cache"); 22 | File('${htPath}cache').create(recursive: true); 23 | } catch (e) { 24 | print("Error creating cache file: $e"); 25 | return false; 26 | } 27 | } 28 | 29 | response = response.replaceAll('"', '\\"'); 30 | //response = response.replaceAll('\n', '\\n'); 31 | 32 | // append to file 33 | var json = jsonEncode({'prompt': prompt, 'response': response}); 34 | File('${htPath}cache').writeAsStringSync('$json\n', mode: FileMode.append); 35 | return true; 36 | } 37 | 38 | // search in database 39 | String? search() { 40 | if (!File('${htPath}cache').existsSync()) { 41 | //print("cache does not exist"); 42 | return null; 43 | } 44 | // read file 45 | var file = File('${htPath}cache').readAsStringSync(); 46 | var lines = file.split('\n'); 47 | for (var line in lines) { 48 | if (line.isEmpty) { 49 | // skip line 50 | continue; 51 | } 52 | var json = jsonDecode(line); 53 | if (json['prompt'] == prompt) { 54 | json['response'] = json['response'].replaceAll('\\"', '"'); 55 | return json['response']; 56 | } 57 | } 58 | // return null if no match was found 59 | return null; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /.github/workflows/dart_build.yml: -------------------------------------------------------------------------------- 1 | name: ht Build 2 | 3 | on: [workflow_dispatch] 4 | 5 | jobs: 6 | compile: 7 | name: Compile Dart Project 8 | strategy: 9 | matrix: 10 | include: 11 | - runs-on: ubuntu-latest 12 | archive-suffix: linux_x64 13 | - runs-on: macos-latest 14 | archive-suffix: MacOS_Intel_x64 15 | - runs-on: macos-latest-xlarge 16 | archive-suffix: MacOS_arm64 17 | runs-on: ${{ matrix.runs-on }} 18 | steps: 19 | - uses: actions/checkout@v2 20 | - uses: dart-lang/setup-dart@v1 21 | - run: dart pub get 22 | 23 | # Extract version 24 | - name: Extract version 25 | id: get_version 26 | run: | 27 | VERSION=$(awk -F '"' '/const version =/ {print $2}' lib/globals.dart) 28 | echo "VERSION=$VERSION" >> $GITHUB_ENV 29 | echo "::set-output name=version::$VERSION" 30 | VERSION_FORMATTED=$(echo $VERSION | sed 's/\./-/g') 31 | echo "VERSION_FORMATTED=$VERSION_FORMATTED" >> $GITHUB_ENV 32 | 33 | # Debug: Print extracted version 34 | - name: Print version 35 | run: echo "Extracted version is $VERSION" 36 | 37 | # Compile executable 38 | - run: | 39 | mkdir -p out 40 | dart compile exe bin/ht.dart -o out/ht_${{ env.VERSION_FORMATTED }}_${{ matrix.archive-suffix }} 41 | 42 | # Set executable bit 43 | - run: chmod +x out/ht_${{ env.VERSION_FORMATTED }}_${{ matrix.archive-suffix }} 44 | 45 | # Zip the executable to preserve file permissions 46 | - run: | 47 | cd out 48 | zip -j ht_${{ env.VERSION_FORMATTED }}_${{ matrix.archive-suffix }}.zip ht_${{ env.VERSION_FORMATTED }}_${{ matrix.archive-suffix }} 49 | 50 | # Upload the artifact 51 | - uses: actions/upload-artifact@v2 52 | with: 53 | name: ht_${{ env.VERSION_FORMATTED }}_${{ matrix.archive-suffix }}.zip 54 | path: out/ht_${{ env.VERSION_FORMATTED }}_${{ matrix.archive-suffix }}.zip 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ### ht - a shell command that answers your questions about shell commands. 3 | 4 | 5 | https://github.com/catallo/ht/assets/45834058/ec95fa8f-038a-4a1d-a85e-130b0af1630d 6 | 7 | ht is a shell helper tool focused on simplicity that can generate, explain and execute shell commands using AI. 8 | 9 | ##### Usage 10 | 11 | - **ht <instruction>** - answers with shell command 12 | 13 | - **ht e|explain** - explains last answer 14 | 15 | - **ht e|explain  [command]** - explains command 16 | 17 | - **ht x|execute** - executes last answer 18 | 19 | ##### Examples 20 | 21 | - `ht find all IPv4 addresses in file A and write to file B` 22 | - `ht explain` 23 | - `ht explain ls -lS` 24 | - `ht explain "ps -aux | grep nvidia"` 25 | - `ht execute` 26 | 27 | #### Features 28 | 29 | - **Low token usage** 30 | - **Cached responses** 31 | - **Easy installation (no root required)** 32 | - **Automatic updates** 33 | 34 | ##### About 35 | 36 | I initially created ht as a simple experiment to test GPT3's usefulness with shell commands. However, I now find myself using it extensively in my daily tasks. So I'm sharing it with the hope that it can benefit others in the same way. It's using OpenAI's GPT3.5-Turbo model now and I plan to add more models in the future, including locally running models. 37 | 38 | ht is written in Dart. As a result, it is compiled into a single, self-contained binary. This means that the ht binary operates independently without requiring any external dependencies or runtime environments. Linux (x86_64) and MacOS (arm64 & Intel) binaries are available for download. 39 | 40 | To use ht, you'll need an OpenAI API key. The good news is that due to ht's low token usage, a typical request costs about $0.00025, making it an incredibly budget-friendly tool for daily usage. You can [sign up for an API key here](https://platform.openai.com/signup) or refer to [this article](https://www.howtogeek.com/885918/how-to-get-an-openai-api-key) for detailed instructions. 41 | 42 | ##### Privacy 43 | 44 | ht communicates directly with OpenAI's API, without involving a third-party server. For automated updates to work, ht will send a request to the GitHub API to check for new releases. 45 | 46 | ##### Installation 47 | 48 | 1. Download the archive for your platform from the Downloads section below. 49 | 2. Unzip the archive. 50 | 3. Using a terminal, navigate to the directory containing the ht binary and run it with the -i flag to start the installation process. 51 | 52 | ``` 53 | cd Downloads 54 | ./ht_2-0-3_linux64 -i 55 | ``` 56 | 57 | ht will be installed to '~/.config/ht' and the directory will be added to your PATH. Future updates will be installed automatically. 58 | 59 | ##### Downloads 60 | 61 | - [Releases](https://github.com/catallo/ht/releases) 62 | 63 | 64 | -------------------------------------------------------------------------------- /lib/ansi_codes.dart: -------------------------------------------------------------------------------- 1 | // ANSI color codes and text effects 2 | 3 | const String acBlack = '\x1B[30m'; 4 | const String acRed = '\x1B[31m'; 5 | const String acGreen = '\x1B[32m'; 6 | const String acYellow = '\x1B[33m'; 7 | const String acBlue = '\x1B[34m'; 8 | const String acMagenta = '\x1B[35m'; 9 | const String acCyan = '\x1B[36m'; 10 | const String acWhite = '\x1B[37m'; 11 | const String acGrey = '\x1B[90m'; 12 | 13 | const String acBrightBlack = '\x1B[90m'; 14 | const String acBrightRed = '\x1B[91m'; 15 | const String acBrightGreen = '\x1B[92m'; 16 | const String acBrightYellow = '\x1B[93m'; 17 | const String acBrightBlue = '\x1B[94m'; 18 | const String acBrightMagenta = '\x1B[95m'; 19 | const String acBrightCyan = '\x1B[96m'; 20 | const String acBrightWhite = '\x1B[97m'; 21 | const String acBrightGrey = '\x1B[37m'; 22 | 23 | // Background color codes 24 | const String acBgBlack = '\x1B[40m'; 25 | const String acBgRed = '\x1B[41m'; 26 | const String acBgGreen = '\x1B[42m'; 27 | const String acBgYellow = '\x1B[43m'; 28 | const String acBgBlue = '\x1B[44m'; 29 | const String acBgMagenta = '\x1B[45m'; 30 | const String acBgCyan = '\x1B[46m'; 31 | const String acBgWhite = '\x1B[47m'; 32 | const String acBgGrey = '\x1B[100m'; 33 | 34 | const String acBgBrightBlack = '\x1B[100m'; 35 | const String acBgBrightRed = '\x1B[101m'; 36 | const String acBgBrightGreen = '\x1B[102m'; 37 | const String acBgBrightYellow = '\x1B[103m'; 38 | const String acBgBrightBlue = '\x1B[104m'; 39 | const String acBgBrightMagenta = '\x1B[105m'; 40 | const String acBgBrightCyan = '\x1B[106m'; 41 | const String acBgBrightWhite = '\x1B[107m'; 42 | const String bgBrightGrey = '\x1B[47m'; 43 | 44 | // Text effects 45 | const String acBold = '\x1B[1m'; 46 | const String acItalic = '\x1B[3m'; 47 | const String acUnderline = '\x1B[4m'; 48 | const String acBlink = '\x1B[5m'; 49 | const String acReverse = '\x1B[7m'; 50 | 51 | const String acReset = '\x1B[0m'; 52 | 53 | // cursor movement 54 | const String acCursorUp = '\x1B[1A'; 55 | const String acCursorDown = '\x1B[1B'; 56 | const String acCursorForward = '\x1B[1C'; 57 | const String acCursorBack = '\x1B[1D'; 58 | 59 | const String acCursorNextLine = '\x1B[1E'; 60 | const String acCursorPreviousLine = '\x1B[1F'; 61 | 62 | const String acCursorHorizontalAbsolute = '\x1B[1G'; 63 | 64 | const String acCursorPosition = '\x1B[1;1H'; 65 | 66 | const String acEraseDisplay = '\x1B[2J'; 67 | const String acEraseLine = '\x1B[2K'; 68 | 69 | const String acScrollUp = '\x1B[1S'; 70 | const String acScrollDown = '\x1B[1T'; 71 | 72 | const String acSaveCursorPosition = '\x1B[s'; 73 | const String acRestoreCursorPosition = '\x1B[u'; 74 | 75 | const String acHideCursor = '\x1B[?25l'; 76 | const String acShowCursor = '\x1B[?25h'; 77 | 78 | const String acClearScreen = '\x1B[2J\x1B[0;0H'; 79 | 80 | const String acClearLine = '\x1B[2K\x1B[0;0H'; 81 | 82 | void main() { 83 | // Example usage: 84 | print( 85 | "$acRed${acBold}This ${acReset}is $acGreen$acItalic$acUnderline$acBgBlue${acBlink}blinking$acReset and $acBgYellow reverse$acBgBrightMagenta text$acReset."); 86 | } 87 | -------------------------------------------------------------------------------- /lib/request_ollama_explain.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | import 'dart:async'; 4 | 5 | import 'globals.dart'; 6 | import 'prompts_explain.dart'; 7 | import 'ansi_codes.dart'; 8 | import 'cache.dart'; 9 | import 'debug.dart'; 10 | import 'unescape_json_string.dart'; 11 | import 'ter_print.dart'; 12 | 13 | void requestOllamaExplain(String prompt) async { 14 | dbg("requestOllamaExplain started"); 15 | print("\n $acBold$prompt$acReset\n"); 16 | 17 | String completeResponse = ""; 18 | String currentLine = ""; 19 | 20 | var httpClient = HttpClient(); 21 | var request = 22 | await httpClient.postUrl(Uri.parse('http://localhost:11434/api/chat')); 23 | 24 | request.headers.set('Content-Type', 'application/json'); 25 | 26 | var requestBody = jsonEncode({ 27 | 'model': 'starcoder', 28 | 'messages': [ 29 | {'role': 'system', 'content': promptExSystemRole}, 30 | {'role': 'user', 'content': promptExUser1}, 31 | {'role': 'assistant', 'content': promptExAssistant1}, 32 | {'role': 'user', 'content': promptExUser2}, 33 | {'role': 'assistant', 'content': promptExAssistant2}, 34 | {'role': 'user', 'content': promptExUser3}, 35 | {'role': 'assistant', 'content': promptExAssistant3}, 36 | {'role': 'user', 'content': promptExUser4}, 37 | {'role': 'assistant', 'content': promptExAssistant4}, 38 | {'role': 'user', 'content': promptExUser5}, 39 | {'role': 'assistant', 'content': promptExAssistant5}, 40 | {'role': 'user', 'content': promptExUser + prompt} 41 | ], 42 | 'stream': true, 43 | 'options': { 44 | 'temperature': temp, 45 | 'num_thread': 4, 46 | 'num_keep': 0, 47 | } 48 | }); 49 | 50 | request.add(utf8.encode(requestBody)); 51 | 52 | var response = await request.close(); 53 | 54 | StreamSubscription? subscription; 55 | 56 | subscription = response.transform(utf8.decoder).listen( 57 | (chunk) { 58 | dbg("chunk: $chunk"); 59 | 60 | var jsonResponse = jsonDecode(chunk); 61 | if (jsonResponse.containsKey('message')) { 62 | var content = jsonResponse['message']['content']; 63 | currentLine += content; 64 | 65 | if (currentLine.endsWith('\n')) { 66 | dbg("currentLine: $currentLine"); 67 | terPrint(currentLine); 68 | currentLine = ""; 69 | } 70 | 71 | completeResponse += content; 72 | } 73 | 74 | if (jsonResponse['done']) { 75 | dbg("\nresponse complete"); 76 | terPrint(currentLine); 77 | done(prompt, completeResponse); 78 | subscription?.cancel(); 79 | dbg("subscription cancelled"); 80 | httpClient.close(); 81 | dbg("httpClient closed"); 82 | return; 83 | } 84 | }, 85 | onError: (error) { 86 | print(error); 87 | subscription?.cancel(); 88 | exit(1); 89 | }, 90 | onDone: () { 91 | done(prompt, completeResponse); 92 | }, 93 | cancelOnError: true, 94 | ); 95 | } 96 | 97 | void done(var prompt, var completeResponse) { 98 | Cache(prompt, completeResponse).save(); 99 | exit(0); 100 | } 101 | -------------------------------------------------------------------------------- /lib/request_ollama_instruct.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | import 'dart:async'; 4 | 5 | import 'package:ht/ansi_codes.dart'; 6 | 7 | import 'globals.dart'; 8 | import 'prompts_instruct.dart'; 9 | import 'cache.dart'; 10 | import 'debug.dart'; 11 | import 'unescape_json_string.dart'; 12 | 13 | void requestOllamaChat(String prompt) async { 14 | dbg("requestOllamaChat started"); 15 | stdout.write("\n "); 16 | 17 | String completeResponse = ""; 18 | String accumulatedChunk = ""; 19 | 20 | var httpClient = HttpClient(); 21 | var request = 22 | await httpClient.postUrl(Uri.parse('http://localhost:11434/api/chat')); 23 | 24 | request.headers.set('Content-Type', 'application/json'); 25 | 26 | var requestBody = jsonEncode({ 27 | 'model': 'mistral-openorca', 28 | 'messages': [ 29 | {'role': 'system', 'content': promptInstSystem}, 30 | {'role': 'user', 'content': promptInstUser1}, 31 | {'role': 'assistant', 'content': promptInstAssistant1}, 32 | {'role': 'user', 'content': promptInstUser2}, 33 | {'role': 'assistant', 'content': promptInstAssistant2}, 34 | {'role': 'user', 'content': "$promptInstUser$prompt"} 35 | ], 36 | 'stream': true, 37 | 'options': { 38 | 'temperature': temp, 39 | 'num_thread': 4, 40 | 'num_keep': 0, 41 | } 42 | }); 43 | 44 | request.add(utf8.encode(requestBody)); 45 | 46 | var response = await request.close(); 47 | 48 | StreamSubscription? subscription; 49 | 50 | subscription = response.transform(utf8.decoder).listen( 51 | (chunk) { 52 | dbg("chunk: $chunk"); 53 | 54 | var jsonResponse = jsonDecode(chunk); 55 | 56 | if (jsonResponse.containsKey('message')) { 57 | var content = jsonResponse['message']['content']; 58 | stdout.write(content); 59 | completeResponse += content; 60 | } 61 | 62 | if (jsonResponse.containsKey('error')) { 63 | var content = jsonResponse['error']['content']; 64 | // get message, type and code from error 65 | print("\n 🤖 there was an error calling the API:\n"); 66 | print("type: " + jsonResponse['error']['type']); 67 | print("code " + jsonResponse['error']['code']); 68 | print("message: " + jsonResponse['error']['message']); 69 | exit(1); 70 | } 71 | 72 | if (jsonResponse['done']) { 73 | dbg("\nresponse complete"); 74 | done(prompt, completeResponse); 75 | subscription?.cancel(); 76 | dbg("subscription cancelled"); 77 | // stop http request 78 | httpClient.close(); 79 | dbg("httpClient closed"); 80 | return; 81 | } 82 | }, 83 | onError: (error) { 84 | print(error); 85 | subscription?.cancel(); 86 | exit(1); 87 | }, 88 | onDone: () { 89 | done(prompt, completeResponse); 90 | }, 91 | cancelOnError: true, 92 | ); 93 | } 94 | 95 | void done(var prompt, var completeResponse) { 96 | print("\n"); 97 | 98 | // Check if the last response is a valid command 99 | if (!completeResponse.contains("🤖")) { 100 | File file = File("${htPath}last_response"); 101 | file.writeAsStringSync(completeResponse); 102 | Process.runSync('chmod', ['+x', "${htPath}last_response"]); 103 | dbg("chmod +x ${htPath}last_response"); 104 | Cache(prompt, completeResponse).save(); 105 | exit(0); 106 | } else { 107 | if (File("${htPath}last_response").existsSync()) { 108 | File file = File("${htPath}last_response"); 109 | file.deleteSync(); 110 | } 111 | 112 | Cache(prompt, completeResponse).save(); 113 | exit(1); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /lib/system_information.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:ht/globals.dart'; 4 | import 'package:ht/debug.dart'; 5 | 6 | void gatherSystemInfo() { 7 | os = Platform.operatingSystem; 8 | dbg("os: $os"); 9 | 10 | var out = Process.runSync('uname', ['-a']); 11 | uname = out.stdout.toString().trim(); 12 | dbg("uname: $uname"); 13 | 14 | if (Platform.isMacOS) { 15 | distro = "Darwin"; 16 | } else { 17 | distro = checkDistro(uname) ?? "Debian derivate"; 18 | } 19 | 20 | dbg("distro: $distro"); 21 | 22 | // works only for system default shell, not actual shell 23 | shell = Platform.environment['SHELL']!; 24 | shell = shell.substring(shell.lastIndexOf('/') + 1); 25 | 26 | dbg("shell: $shell"); 27 | } 28 | 29 | // Following function uses a set to identify the distribution by checking if any 30 | // of these names appear as a substring in the output of the 'uname' command. This 31 | // approach seems to be necessary because surprisingly there is no standardized 32 | // way across all Linux distributions to reliably and directly obtain the distribution 33 | // name. The 'uname' command's output varies significantly among distributions, and 34 | // often the distribution name is part of a longer string, hence the substring checking. 35 | // If you know a better way, please let me know. 36 | 37 | String? checkDistro(String uname) { 38 | final Set distros = { 39 | 'Ubuntu', 40 | 'Debian', 41 | 'Fedora', 42 | 'CentOS', 43 | 'Arch', 44 | 'Manjaro', 45 | 'openSUSE', 46 | 'SUSE', 47 | 'Gentoo', 48 | 'Slackware', 49 | 'Alpine', 50 | 'Raspbian', 51 | 'Kali', 52 | 'LinuxMint', 53 | 'ElementaryOS', 54 | 'PopOS', 55 | 'ZorinOS', 56 | 'Solus', 57 | 'Void', 58 | 'NixOS', 59 | 'ClearLinux', 60 | 'ParrotOS', 61 | 'MXLinux', 62 | 'Deepin', 63 | 'Mageia', 64 | 'EndeavourOS', 65 | 'ArcoLinux', 66 | 'MX Linux', 67 | 'Mint', 68 | 'Pop!_OS', 69 | 'Lite', 70 | 'Zorin', 71 | 'Garuda', 72 | 'KDE neon', 73 | 'antiX', 74 | 'elementary', 75 | 'Nobara', 76 | 'PCLinuxOS', 77 | 'Puppy', 78 | 'Vanilla', 79 | 'AlmaLinux', 80 | 'SparkyLinux', 81 | 'EasyOS', 82 | 'Q4OS', 83 | 'FreeBSD', 84 | 'CachyOS', 85 | 'Peppermint', 86 | 'blendOS', 87 | 'Voyager', 88 | 'Bodhi', 89 | 'Devuan', 90 | 'Kubuntu', 91 | 'Gnoppix', 92 | 'TUXEDO', 93 | 'Lubuntu', 94 | 'Tails', 95 | 'SmartOS', 96 | 'Bluestar', 97 | 'OpenMandriva', 98 | 'Rocky', 99 | 'Regata', 100 | 'Archcraft', 101 | 'XeroLinux', 102 | 'PureOS', 103 | 'Parrot', 104 | 'Xubuntu', 105 | 'Linuxfx', 106 | 'siduction', 107 | 'Murena', 108 | 'Red Hat', 109 | 'redhat', 110 | 'Photon', 111 | 'Clear', 112 | 'Neptune', 113 | 'Qubes', 114 | 'EuroLinux', 115 | 'KaOS', 116 | 'Tiny Core', 117 | 'Athena', 118 | '4MLinux', 119 | 'Mabox', 120 | 'TrueNAS', 121 | 'wattOS', 122 | 'ReactOS', 123 | 'Artix', 124 | 'deepin', 125 | 'Endless', 126 | 'MakuluLinux', 127 | 'RebornOS', 128 | 'Archman', 129 | 'Proxmox', 130 | 'GhostBSD', 131 | 'ALT', 132 | 'ExTiX', 133 | 'Nitrux', 134 | 'Feren', 135 | 'Fatdog64', 136 | 'Oracle', 137 | 'OpenBSD', 138 | 'ROSA', 139 | 'Emmabuntüs', 140 | 'LXLE', 141 | 'Absolute', 142 | 'Gecko', 143 | 'Haiku', 144 | 'Kodachi', 145 | 'Peropesis', 146 | 'NuTyX', 147 | 'Spiral', 148 | 'NetBSD', 149 | 'raspi', 150 | }; 151 | 152 | uname = uname.toLowerCase(); 153 | for (final distro in distros) { 154 | if (uname.contains(distro.toLowerCase())) { 155 | return distro; 156 | } 157 | } 158 | 159 | return null; 160 | } 161 | -------------------------------------------------------------------------------- /bin/ht.dart: -------------------------------------------------------------------------------- 1 | // ht (for 'how-to') 2 | // - a shell command that answers your questions about shell commands. 3 | // 4 | // MIT License 5 | // 6 | // Copyright (c) 2023 Sandro Catallo 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | // THE SOFTWARE. 25 | 26 | import 'dart:io'; 27 | import 'package:compute/compute.dart'; 28 | 29 | import 'package:ht/cache.dart'; 30 | import 'package:ht/ansi_codes.dart'; 31 | import 'package:ht/globals.dart'; 32 | import 'package:ht/request_openai_instruct.dart'; 33 | import 'package:ht/arguments.dart'; 34 | import 'package:ht/system_information.dart'; 35 | import 'package:ht/ter_print.dart'; 36 | import 'package:ht/debug.dart'; 37 | import 'package:ht/get_latest_version.dart'; 38 | import 'package:ht/installation_and_update.dart'; 39 | import 'package:ht/wrapper_script.dart'; 40 | 41 | import 'package:ht/request_ollama_instruct.dart'; 42 | import 'package:ht/config.dart'; 43 | 44 | void initialize() { 45 | dbg("initialize started"); 46 | 47 | config.checkConfig(); 48 | debug = config.readDebug() ?? false; 49 | openAIapiKey = config.readApiKey(); 50 | } 51 | 52 | Future checkForLatestRelease() async { 53 | // Wrap the call in another function that takes a dummy argument 54 | bool result = await compute(wrapperLatestVersionCheck, null); 55 | return result; 56 | } 57 | 58 | bool updateAvailable() { 59 | if (File("${htPath}update_available").existsSync()) { 60 | return true; 61 | } 62 | return false; 63 | } 64 | 65 | void main(List arguments) async { 66 | dbg("ht started"); 67 | //dbg(wrapperScript); 68 | 69 | // install 70 | if (arguments.isNotEmpty && 71 | (arguments[0] == '-i' || arguments[0] == '--install')) { 72 | checkInstallation(); 73 | exit(0); 74 | } 75 | 76 | if (updateAvailable()) await downloadUpdate(); 77 | 78 | checkForLatestRelease(); 79 | 80 | initialize(); 81 | 82 | if (openAIapiKey == null) { 83 | setupApiKey(); 84 | } 85 | 86 | gatherSystemInfo(); 87 | 88 | if (await parseArguments(arguments)) { 89 | String instruction = arguments.join(' '); 90 | // search if exists in cache 91 | dbg("searching in cache"); 92 | Cache cache = Cache(instruction, ""); 93 | String? cachedResponse = cache.search(); 94 | if (cachedResponse != null) { 95 | dbg("found in cache"); 96 | print("\n $cachedResponse\n"); 97 | // save to last_response if valid command, delete last_response if not 98 | if (!cachedResponse.contains("🤖")) { 99 | try { 100 | dbg("saving to last_response"); 101 | File lastResponse = File("${htPath}last_response"); 102 | await lastResponse.writeAsString(cachedResponse); 103 | Process.runSync('chmod', ['+x', "${htPath}last_response"]); 104 | } catch (error) { 105 | print("Error saving file: $error"); 106 | } 107 | } else { 108 | if (File("${htPath}last_response").existsSync()) { 109 | File file = File("${htPath}last_response"); 110 | file.deleteSync(); 111 | } 112 | } 113 | exit(0); 114 | } 115 | requestOpenAIinstruct(instruction); 116 | //requestOllamaChat(instruction); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /lib/config.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:ht/ansi_codes.dart'; 3 | import 'package:ht/globals.dart'; 4 | import 'package:ht/installation_and_update.dart'; 5 | 6 | import 'debug.dart'; 7 | import 'ter_print.dart'; 8 | 9 | // configuration settings at ~/.config/ht/config. 10 | // key: value 11 | // API-KEY: sk-1234567890 12 | // debug: false 13 | 14 | class Config { 15 | Config({this.apiKey, this.debug}); 16 | 17 | String? apiKey; 18 | bool? debug; 19 | String? home = Platform.environment['HOME']; 20 | 21 | bool checkConfig() { 22 | if (!Directory(htPath).existsSync()) { 23 | checkInstallation(); 24 | } else { 25 | return true; 26 | } 27 | return false; 28 | } 29 | 30 | // Read property from config file 31 | String? _readProperty(String propertyName) { 32 | // check if config file exists 33 | if (!File("${htPath}config").existsSync()) { 34 | print( 35 | "\n🤖 Config file not found. Please run$acBold rm -rf ~/.config/ht/$acReset and reinstall ht.\n"); 36 | exit(1); 37 | } 38 | 39 | final configFile = File("${htPath}config"); 40 | final fileContents = configFile.readAsStringSync(); 41 | final lines = fileContents.split('\n'); 42 | 43 | for (var line in lines) { 44 | final parts = line.split(':'); 45 | if (parts[0].trim() == propertyName) { 46 | return parts[1].trim(); 47 | } 48 | } 49 | return null; 50 | } 51 | 52 | // Set property in config file 53 | bool _setProperty(String propertyName, String propertyValue) { 54 | final configFile = File("${htPath}config"); 55 | final regex = RegExp('$propertyName:.*'); 56 | 57 | String fileContents = 58 | configFile.readAsStringSync(); // Assign to fileContents 59 | 60 | if (regex.hasMatch(fileContents)) { 61 | try { 62 | fileContents = fileContents.replaceAll( 63 | regex, '$propertyName: $propertyValue'); // Update fileContents 64 | configFile.writeAsStringSync(fileContents); 65 | return true; 66 | } catch (e) { 67 | print("Error setting $propertyName: $e"); 68 | return false; 69 | } 70 | } else { 71 | try { 72 | configFile.writeAsStringSync('$propertyName: $propertyValue\n', 73 | mode: FileMode.append); 74 | return true; 75 | } catch (e) { 76 | print("Error setting $propertyName: $e"); 77 | return false; 78 | } 79 | } 80 | } 81 | 82 | String? readApiKey() { 83 | if (getOpenAIApiKeyFromENV()) { 84 | return openAIapiKey; 85 | } 86 | return _readProperty('API-KEY'); 87 | } 88 | 89 | bool setApiKey(String apiKey) { 90 | return _setProperty('API-KEY', apiKey); 91 | } 92 | 93 | bool? readDebug() { 94 | final debugValue = _readProperty('debug'); 95 | return debugValue == 'true'; 96 | } 97 | 98 | bool setDebug(bool debug) { 99 | final debugValue = debug ? 'true' : 'false'; 100 | return _setProperty('debug', debugValue); 101 | } 102 | } 103 | 104 | void setupApiKey() { 105 | terPrint( 106 | "\n\n 🤖 Welcome to ht. To use this application, you need to set an OpenAI API key."); 107 | print(""); 108 | terPrint( 109 | "The good news is that due to ht's low token usage, a typical request costs about \$0.00025, making it a budget-friendly tool for daily usage. You can obtain an API key by signing up at https://platform.openai.com/signup. For a more detailed guide on how to get an OpenAI API key, you can refer to this article: https://www.howtogeek.com/885918/how-to-get-an-openai-api-key/."); 110 | stdout.write( 111 | "\n${acBold}Paste your OpenAI API key here (or press enter to exit):$acReset "); 112 | openAIapiKey = stdin.readLineSync(); 113 | if (openAIapiKey!.isEmpty) { 114 | print("Exiting..."); 115 | exit(1); 116 | } 117 | config.setApiKey(openAIapiKey!); 118 | print("API key set. Please run ht again."); 119 | exit(0); 120 | } 121 | 122 | bool getOpenAIApiKeyFromENV() { 123 | if (Platform.environment.containsKey('OPENAI_API_KEY')) { 124 | dbg("OPENAI_API_KEY found in environment"); 125 | openAIapiKey = Platform.environment['OPENAI_API_KEY']; 126 | return true; 127 | } else { 128 | dbg("OPENAI_API_KEY not found in environment"); 129 | return false; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /lib/request_openai_explain.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | import 'dart:async'; 4 | 5 | import 'globals.dart'; 6 | import 'prompts_explain.dart'; 7 | import 'ansi_codes.dart'; 8 | import 'cache.dart'; 9 | import 'debug.dart'; 10 | import 'unescape_json_string.dart'; 11 | import 'ter_print.dart'; 12 | 13 | void requestOpenAIexplain(String prompt) async { 14 | dbg("requestGPTexplain started"); 15 | print("\n $acBold$prompt$acReset\n"); 16 | 17 | String completeResponse = ""; 18 | String accumulatedChunk = ""; 19 | String currentLine = ""; 20 | 21 | var baseURL = "https://api.openai.com/v1/chat/completions"; 22 | //var baseURL = "http://localhost:4891/v1"; 23 | 24 | var httpClient = HttpClient(); 25 | var request = await httpClient.postUrl(Uri.parse(baseURL)); 26 | 27 | request.headers.set('Content-Type', 'application/json'); 28 | request.headers.set('Authorization', 'Bearer $openAIapiKey'); 29 | 30 | var requestBody = jsonEncode({ 31 | 'model': model, 32 | 'messages': [ 33 | {'role': 'system', 'content': promptExSystemRole}, 34 | {'role': 'user', 'content': promptExUser1}, 35 | {'role': 'assistant', 'content': promptExAssistant1}, 36 | {'role': 'user', 'content': promptExUser2}, 37 | {'role': 'assistant', 'content': promptExAssistant2}, 38 | {'role': 'user', 'content': promptExUser3}, 39 | {'role': 'assistant', 'content': promptExAssistant3}, 40 | {'role': 'user', 'content': promptExUser4}, 41 | {'role': 'assistant', 'content': promptExAssistant4}, 42 | {'role': 'user', 'content': promptExUser5}, 43 | {'role': 'assistant', 'content': promptExAssistant5}, 44 | {'role': 'user', 'content': promptExUser + prompt} 45 | ], 46 | 'temperature': temp, 47 | 'stream': true, 48 | }); 49 | 50 | request.add(utf8.encode(requestBody)); 51 | 52 | var response = await request.close(); 53 | 54 | StreamSubscription? subscription; 55 | 56 | subscription = response.transform(utf8.decoder).listen( 57 | (chunk) { 58 | dbg("chunk: $chunk"); 59 | accumulatedChunk += chunk; 60 | 61 | if (chunk.endsWith('\n')) { 62 | // Check for errors in response 63 | if (accumulatedChunk.contains('"error":')) { 64 | final errorResponse = jsonDecode(accumulatedChunk); 65 | final error = errorResponse['error']; 66 | 67 | print("🤖 An error occurred:\n"); 68 | print(" Code: ${error['code']}"); 69 | print(" Type: ${error['type']}"); 70 | terPrint("\n$acItalic${error['message']}"); 71 | 72 | // if message contains "API key" 73 | if (error['message'].contains("API")) { 74 | print( 75 | "\nUse$acBold ht -set $acReset to set your API key.\n"); 76 | } 77 | 78 | subscription?.cancel(); 79 | exit(1); 80 | } 81 | 82 | RegExp exp = RegExp(r'"delta":\{"content":"(.*?)"\}'); 83 | var matches = exp.allMatches(accumulatedChunk); 84 | 85 | for (var match in matches) { 86 | var content = match.group(1); 87 | content = unescapeJsonString(content!); 88 | currentLine += content; 89 | 90 | if (currentLine.endsWith('\n')) { 91 | dbg("currentLine: $currentLine"); 92 | terPrint(currentLine); 93 | currentLine = ""; 94 | } 95 | 96 | completeResponse += content; 97 | } 98 | 99 | RegExp reasonExp = RegExp(r'"finish_reason":"(.*?)"'); 100 | var reasonMatches = reasonExp.allMatches(accumulatedChunk); 101 | for (var reasonMatch in reasonMatches) { 102 | var reason = reasonMatch.group(1); 103 | if (reason == "stop") { 104 | dbg("\nfinish_reason: $reason"); 105 | terPrint(currentLine); 106 | done(prompt, completeResponse); 107 | subscription?.cancel(); 108 | return; 109 | } 110 | } 111 | 112 | accumulatedChunk = ""; 113 | dbg("next chunk"); 114 | } 115 | }, 116 | onError: (error) { 117 | print(error); 118 | subscription?.cancel(); 119 | exit(1); 120 | }, 121 | onDone: () { 122 | done(prompt, completeResponse); 123 | }, 124 | cancelOnError: true, 125 | ); 126 | } 127 | 128 | void done(var prompt, var completeResponse) { 129 | if (!completeResponse.isEmpty) { 130 | Cache(prompt, completeResponse).save(); 131 | } 132 | exit(0); 133 | } 134 | -------------------------------------------------------------------------------- /lib/request_openai_instruct.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | import 'dart:async'; 4 | 5 | import 'package:ht/ansi_codes.dart'; 6 | 7 | import 'globals.dart'; 8 | import 'prompts_instruct.dart'; 9 | import 'cache.dart'; 10 | import 'debug.dart'; 11 | import 'unescape_json_string.dart'; 12 | import 'ter_print.dart'; 13 | 14 | void requestOpenAIinstruct(String prompt) async { 15 | dbg("requestGPTinstruct started"); 16 | stdout.write("\n "); 17 | 18 | String completeResponse = ""; 19 | String accumulatedChunk = ""; 20 | 21 | var baseURL = "https://api.openai.com/v1/chat/completions"; 22 | // var baseURL = "http://localhost:4891/v1/chat/completions"; 23 | //model = "Mistral Instruct"; 24 | 25 | var httpClient = HttpClient(); 26 | var request = await httpClient.postUrl(Uri.parse(baseURL)); 27 | 28 | request.headers.set('Content-Type', 'application/json'); 29 | request.headers.set('Authorization', 'Bearer $openAIapiKey'); 30 | 31 | var requestBody = jsonEncode({ 32 | 'model': model, 33 | 'messages': [ 34 | {'role': 'system', 'content': promptInstSystem}, 35 | {'role': 'user', 'content': promptInstUser1}, 36 | {'role': 'assistant', 'content': promptInstAssistant1}, 37 | {'role': 'user', 'content': promptInstUser2}, 38 | {'role': 'assistant', 'content': promptInstAssistant2}, 39 | {'role': 'user', 'content': "$promptInstUser$prompt"} 40 | ], 41 | 'temperature': temp, 42 | 'stream': true, 43 | }); 44 | 45 | request.add(utf8.encode(requestBody)); 46 | 47 | var response = await request.close(); 48 | 49 | StreamSubscription? subscription; 50 | 51 | subscription = response.transform(utf8.decoder).listen( 52 | (chunk) { 53 | dbg("chunk: $chunk"); 54 | accumulatedChunk += chunk; 55 | 56 | if (chunk.endsWith('\n')) { 57 | // Check for errors in response 58 | if (accumulatedChunk.contains('"error":')) { 59 | final errorResponse = jsonDecode(accumulatedChunk); 60 | final error = errorResponse['error']; 61 | 62 | print("🤖 An error occurred:\n"); 63 | print(" Code: ${error['code']}"); 64 | print(" Type: ${error['type']}\n"); 65 | terPrint("$acItalic${error['message']}"); 66 | 67 | // if message contains "API key" 68 | if (error['message'].contains("API")) { 69 | print( 70 | "\n${acReset}Use$acBold ht -set $acReset to set your API key.\n"); 71 | } 72 | 73 | subscription?.cancel(); 74 | exit(1); 75 | } 76 | 77 | // extract content within delta object 78 | RegExp exp = RegExp(r'"delta":\{"content":"(.*?)"\}'); 79 | var matches = exp.allMatches(accumulatedChunk); 80 | 81 | for (var match in matches) { 82 | var content = match.group(1); 83 | content = unescapeJsonString(content!); 84 | stdout.write(content); 85 | completeResponse += content; 86 | } 87 | 88 | // extract finish_reason 89 | RegExp reasonExp = RegExp(r'"finish_reason":"(.*?)"'); 90 | var reasonMatches = reasonExp.allMatches(accumulatedChunk); 91 | // if the finish_reason is "stop", the response is complete 92 | for (var reasonMatch in reasonMatches) { 93 | var reason = reasonMatch.group(1); 94 | if (reason == "stop") { 95 | dbg("\nfinish_reason: $reason"); 96 | done(prompt, completeResponse); 97 | subscription?.cancel(); 98 | dbg("subscription cancelled"); 99 | // stop http request 100 | httpClient.close(); 101 | dbg("httpClient closed"); 102 | return; 103 | } 104 | } 105 | 106 | accumulatedChunk = ""; 107 | dbg("next chunk"); 108 | } 109 | }, 110 | onError: (error) { 111 | print(error); 112 | subscription?.cancel(); 113 | exit(1); 114 | }, 115 | onDone: () { 116 | done(prompt, completeResponse); 117 | }, 118 | cancelOnError: true, 119 | ); 120 | } 121 | 122 | void done(var prompt, var completeResponse) { 123 | print("\n"); 124 | 125 | // Check if the last response is a valid command 126 | dbg("completeResponse: |$completeResponse|"); 127 | if ((!completeResponse.contains("🤖")) && 128 | (!completeResponse.trim().isEmpty)) { 129 | File file = File("${htPath}last_response"); 130 | file.writeAsStringSync(completeResponse); 131 | Process.runSync('chmod', ['+x', "${htPath}last_response"]); 132 | dbg("chmod +x ${htPath}last_response"); 133 | Cache(prompt, completeResponse).save(); 134 | exit(0); 135 | } else { 136 | dbg("last_response is not a valid command"); 137 | if (File("${htPath}last_response").existsSync()) { 138 | File file = File("${htPath}last_response"); 139 | file.deleteSync(); 140 | } 141 | exit(1); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /lib/prompts_explain.dart: -------------------------------------------------------------------------------- 1 | import 'package:ht/globals.dart'; 2 | 3 | String promptExSystemRole = 4 | """You're a $distro $os shell expert. You sort parts of commands into the following categories followed by '#DSCR:' and a description: 5 | 6 | #CMD: actual command #DSCR: 7 | #SB1: options and other things following the command #DSCR: 8 | #SB2: sub parts of the above #DSCR: 9 | #SB3: sub parts of the above #DSCR: 10 | #OPR: Operators like | or > that are not part of an actual command #DSCR: 11 | 12 | Your output will be parsed to a hierachical structure, this is what #SB1:, #SB2:and #SB3 are for. 13 | 14 | Describe parts in the same order as in the original command. 15 | 16 | If necessary these three tags shall appear before explaining the command: 17 | #WRN: Use this for warnings when user is potentially about to harm their system 18 | #ERR: Use this to describe every error or typo that makes the command invalid. Only show if you're sure! 19 | #COR: The corrected version if an error was found. Write only the command in one line without any further explanation! 20 | 21 | If you found an error, explain the #COR: version! 22 | 23 | #SS: really short summary 24 | 25 | ALWAYS stick to the instructed format! Don't make up new tags or change the order!; 26 | """; 27 | 28 | String promptExUser1 = "ls -l -R"; 29 | 30 | String promptExAssistant1 = """#ERR: none 31 | #CMD: ls #DSCR: lists directory contents 32 | #SB1: -l #DSCR: lists in long format 33 | #SB1: -R #DSCR: lists subdirectories recursively 34 | #SS: Lists directory and subdirectories in long format"""; 35 | 36 | String promptExUser2 = 37 | "sed -i 's/[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}/192.168.0.1/g' logfile"; 38 | 39 | String promptExAssistant2 = """#ERR: none 40 | #CMD: sed #DSCR: stream editor for filtering and transforming text 41 | #SB1: -i #DSCR: edit files in-place 42 | #SB1: 's/[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}/192.168.0.1/g' #DSCR: search and replace pattern 43 | #SB2: [0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3} #DSCR: searches for IPv4 addresses 44 | #SB2: /192.168.0.1/ #DSCR: specifies replacement text 45 | #SB2: g #DSCR: specifies global replacement (replace all occurrences on each line) 46 | #SB1: logfile #DSCR: file name on which the command is to be applied 47 | #SS: Uses sed, a stream editor, to search and replace all occurrences of an IPv4 address pattern in 'logfile' with the IP address '192.168.0.1'."""; 48 | 49 | String promptExUser3 = "curl -s https://www.wired.com | grep -oE '\bw+\b'"; 50 | 51 | String promptExAssistant3 = """#ERR: none 52 | #CMD: curl #DSCR: command line tool for transferring data from or to a server 53 | #SB1: -s #DSCR: silent mode, does not show progress or error messages 54 | #SB1: https://www.google.de #DSCR: specifies URL to retrieve data from 55 | #OPR: | #DSCR: pipe, redirects the output of curl command to input of grep command 56 | #CMD: grep #DSCR: searches for patterns in text 57 | #SB1: -oE #DSCR: tells grep to show only matched portion of line using extended regular expressions 58 | #SB1: '\bw+\b' #DSCR: regular expression pattern to match words 59 | #SS: Uses curl to fetch content from 'https://www.wired.com', and then pipes this content to grep to extract and print all whole words."""; 60 | 61 | String promptExUser4 = "rm / -rf"; 62 | 63 | String promptExAssistant4 = 64 | """#WRN: Warning! This command is attempting to remove the root directory ("/") which results in the deletion of the entire file system. 65 | #CMD: rm #DSCR: removes files and directories 66 | #SB1: / #DSCR: specifies root directory 67 | #SB1: -rf #DSCR: options to force removal of files and directories recursively 68 | #SS: Attempts to remove all files and directories starting from the root directory ("/") recursively and forcefully."""; 69 | 70 | String promptExUser5 = 71 | """mkdirr ~/bin && echo 'export PATH=\$PATH:\$HOME/bin' >> ~/.bashrt; rm /"""; 72 | 73 | String promptExAssistant5 = """#ERR: The comands name is mkdir, not mkdirr. 74 | #ERR: The name of the bash configuration file is .bashrc, not .bashrt 75 | #ERR: The rm command requires the -r (or --recursive) flag to remove directories and their contents recursively 76 | #COR: mkdir ~/bin && echo "export PATH=\$PATH:\$HOME/bin" >> ~/.bashrc; rm / -r 77 | #WRN: Warning! This command is attempting to remove the root directory ("/") which results in the deletion of the entire file system. 78 | #CMD: mkdir #DSCR: creates new directory 79 | #SB1: ~/bin #DSCR: specifies path of new directory to add to PATH 80 | #OPR: && #DSCR: executes next command only if previous command succeeds 81 | #CMD: echo #DSCR: prints string to the standard output 82 | #SB1: "export PATH=\$PATH: \$HOME/bin" #DSCR: string to be printed 83 | #OPR: >> #DSCR: appends output to file 84 | #SB1: ~/.bashrc #DSCR: file to append the output to 85 | #SS: Creates a new directory named "bin" in the user's home directory and then appends the line "export PATH=\$PATH:\$HOME/bin" to the end of the ~/.bashrc file. This ensures that the newly created "bin" directory is added to the system's PATH variable."""; 86 | 87 | String promptExUser = ""; 88 | -------------------------------------------------------------------------------- /lib/arguments.dart: -------------------------------------------------------------------------------- 1 | import 'package:ht/ansi_codes.dart'; 2 | import 'dart:io'; 3 | import 'dart:async'; 4 | 5 | import 'package:ht/globals.dart'; 6 | import 'package:ht/request_openai_explain.dart'; 7 | import 'package:ht/cache.dart'; 8 | import 'package:ht/ter_print.dart'; 9 | import 'installation_and_update.dart'; 10 | import 'debug.dart'; 11 | 12 | import 'package:ht/request_ollama_explain.dart'; 13 | 14 | Future parseArguments(List arguments) async { 15 | // help 16 | if (arguments.isEmpty || 17 | (arguments[0] == '-h') || 18 | (arguments[0] == '--help')) { 19 | print( 20 | "\n$acItalic${acBold}ht (for how-to)$acReset,$acItalic a shell command that answers your questions about shell commands.\n"); 21 | print("$acItalic Usage$acReset:"); 22 | print( 23 | "$acBold ht $acReset - answers with a shell command"); 24 | print("$acBold ht e|explain$acReset - explains last answer"); 25 | print("$acBold ht e|explain [command]$acReset - explains command"); 26 | print("$acBold ht x|execute $acReset - executes last command\n"); 27 | print("$acBold ht -h|--help$acReset - help"); 28 | print("$acBold ht -s|--settings$acReset - settings overview"); 29 | print("$acBold ht -v|--version$acReset - show version"); 30 | print("$acItalic\nExamples:$acReset"); 31 | print("ht find all IPv4 addresses in log file and write to new file"); 32 | print("ht explain"); 33 | print("ht explain ls -lS"); 34 | print('ht explain "ps -aux | grep nvidia"'); 35 | print( 36 | "$acReset$acGrey https://github.com/catallo/ht$acReset"); 37 | exit(0); 38 | } 39 | 40 | // debug 41 | if ((arguments[0] == '-d') || (arguments[0] == '--debug')) { 42 | debug = true; 43 | arguments = arguments.sublist(1); 44 | } 45 | 46 | // show settings 47 | if ((arguments[0] == '-s') || (arguments[0] == '--settings')) { 48 | print(acBold); 49 | //print("$acBold model: $model"); 50 | print( 51 | " ${acBold}apikey: ..${openAIapiKey!.substring(openAIapiKey!.length - 6)}"); 52 | 53 | print(" debug: $debug"); 54 | print( 55 | "\n$acReset${acItalic}Use 'ht -set ' to change setting (or edit ~/.config/ht/config)."); 56 | print("example: ht -set apikey >\n"); 57 | exit(0); 58 | } 59 | 60 | // set setting 61 | if (arguments[0] == '-set') { 62 | if (arguments.length != 3) { 63 | print("\n$acBold Wrong number of arguments.\n"); 64 | print("$acBold$acItalic Usage:$acReset ht -set "); 65 | exit(1); 66 | } 67 | 68 | var setting = arguments[1]; 69 | var value = arguments[2]; 70 | 71 | switch (setting) { 72 | case "model": 73 | print("Setting model to $value"); 74 | break; 75 | case "apikey": 76 | print("Setting apikey to $value"); 77 | config.setApiKey(value); 78 | break; 79 | case "debug": 80 | if (value == "true") { 81 | debug = true; 82 | print("Setting debug to true. Use ht -set debug false to disable."); 83 | config.setDebug(true); 84 | } else { 85 | debug = false; 86 | print("Setting debug to false"); 87 | config.setDebug(false); 88 | } 89 | break; 90 | default: 91 | print( 92 | "Unknown setting $setting, type ht -s to see available settings."); 93 | exit(1); 94 | } 95 | return false; 96 | } 97 | // version 98 | if ((arguments[0] == '-v') || (arguments[0] == '--version')) { 99 | print( 100 | "\n$acItalic$acBold ht$acReset$acItalic v$version ($compileDate)$acReset\n"); 101 | print("$acGrey Detected OS:$acBrightGrey $os"); 102 | print("$acGrey Detected Distro:$acBrightGrey $distro"); 103 | print("$acGrey Default Shell:$acBrightGrey $shell"); 104 | print("$acGrey Model:$acBrightGrey $model\n"); 105 | exit(0); 106 | } 107 | // explain last response 108 | if ((arguments[0] == 'explain' || arguments[0] == 'e')) { 109 | var command = ""; 110 | 111 | if (arguments.length < 2) { 112 | // check if last_response exists 113 | if (!File('${htPath}last_response').existsSync()) { 114 | print("\n 🤖 My last response wasn't a valid shell command.\n"); 115 | exit(1); 116 | } 117 | 118 | var lastResponse = File('${htPath}last_response').readAsStringSync(); 119 | // remove all empty lines from last response, also trim 120 | command = lastResponse 121 | .split('\n') 122 | .where((element) => element.isNotEmpty) 123 | .join('\n') 124 | .trim(); 125 | if (lastResponse.isEmpty) { 126 | print("\n 🤖 My last response wasn't a valid shell command.\n"); 127 | exit(1); 128 | } 129 | } 130 | if (arguments.length > 1) { 131 | command = arguments.sublist(1).join(' '); 132 | } 133 | Cache cache = Cache(command, ""); 134 | String? cachedResponse = cache.search(); 135 | if (cachedResponse != null) { 136 | print("\n$acBold $command$acReset\n"); 137 | // split cachedResponse into lines 138 | var lines = cachedResponse.split('\n'); 139 | // terPrint all lines 140 | for (var line in lines) { 141 | terPrint(line); 142 | } 143 | exit(0); 144 | } 145 | requestOpenAIexplain(command); 146 | //requestOllamaExplain(command); 147 | return false; 148 | } 149 | return true; 150 | } 151 | -------------------------------------------------------------------------------- /lib/get_latest_version.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | import 'package:path/path.dart' as path; 4 | 5 | import 'globals.dart'; 6 | import 'package:http/http.dart' as http; 7 | import 'package:ht/installation_and_update.dart'; 8 | import 'package:ht/debug.dart'; 9 | 10 | Future fetchLatestReleaseVersion() async { 11 | var url = 12 | Uri.parse('https://api.github.com/repos/catallo/ht/releases/latest'); 13 | var response = await http.get(url); 14 | 15 | if (response.statusCode == 200) { 16 | var jsonResponse = jsonDecode(response.body); 17 | 18 | return jsonResponse['tag_name']; // contains the version 19 | } else { 20 | throw Exception( 21 | 'Failed to load latest release version, status code: ${response.statusCode}'); 22 | } 23 | } 24 | 25 | // Wrapper function 26 | Future wrapperLatestVersionCheck(void _) async { 27 | return await checkForLatestVersion(); 28 | } 29 | 30 | Future checkForLatestVersion() async { 31 | var latestVersion = 'v0.0.0'; 32 | try { 33 | latestVersion = await fetchLatestReleaseVersion(); 34 | dbg('Latest Release Version: $latestVersion, this version: $version'); 35 | } catch (e) { 36 | print(e); 37 | } 38 | if (latestVersion.startsWith('v')) latestVersion = latestVersion.substring(1); 39 | if (isSemVerHigher(version, latestVersion)) { 40 | dbg('New version available'); 41 | try { 42 | File("${htPath}update_available").createSync(recursive: false); 43 | File("${htPath}update_available") 44 | .writeAsStringSync('$latestVersion\n', mode: FileMode.append); 45 | } catch (e) { 46 | print("Error creating update_available file: $e"); 47 | return false; 48 | } 49 | } else { 50 | dbg('No new version available'); 51 | } 52 | return true; 53 | } 54 | 55 | downloadUpdate() async { 56 | // ToDo: Refactor this function 57 | dbg('downloadUpdate started'); 58 | 59 | // read the version number from update_available and trim it 60 | var newReleaseVersionNumber = 61 | File("${htPath}update_available").readAsStringSync(); 62 | newReleaseVersionNumber = newReleaseVersionNumber.trim(); 63 | 64 | print( 65 | "\n 🤖 There is an updated version ($newReleaseVersionNumber) available. Downloading ...\n"); 66 | 67 | // if it doesn't exist, create ~/.config/ht/download/ 68 | if (!Directory("${htPath}download").existsSync()) { 69 | try { 70 | Directory("${htPath}download").createSync(recursive: false); 71 | } catch (e) { 72 | print("Error creating directory: $e"); 73 | return false; 74 | } 75 | } 76 | 77 | // determine platform for update. 78 | // ToDo: add ARM linux support 79 | String platformKey = ""; 80 | 81 | if (Platform.isLinux) { 82 | platformKey = 'linux_x64'; 83 | } else { 84 | // use uname -a to determine if it's an x86 or ARM Mac 85 | var result = await Process.run('uname', ['-a']); 86 | if (result.exitCode != 0) { 87 | print('Error determining platform: ${result.stderr}'); 88 | exit(1); 89 | } 90 | var uname = result.stdout.toString(); 91 | dbg("uname: $uname"); 92 | if (uname.contains('x86_64')) { 93 | platformKey = 'MacOS_Intel_x64'; 94 | } else { 95 | platformKey = 'MacOS_arm64'; 96 | } 97 | dbg("platformKey: $platformKey"); 98 | } 99 | 100 | // Fetch the latest release data from GitHub 101 | var url = 102 | Uri.parse('https://api.github.com/repos/catallo/ht/releases/latest'); 103 | var response = await http.get(url); 104 | 105 | if (response.statusCode == 200) { 106 | var jsonResponse = jsonDecode(response.body); 107 | var body = jsonResponse['body']; 108 | 109 | // Regular expression to find download links 110 | RegExp regExp = RegExp( 111 | r'\[ht_[^\]]*_' + 112 | platformKey.replaceAll('_', r'_') + 113 | r'\.zip\]\((https?[^\)]+)\)', 114 | caseSensitive: false); 115 | 116 | var matches = regExp.allMatches(body); 117 | if (matches.isNotEmpty) { 118 | var downloadUrl = matches.first.group(1); 119 | dbg('Download URL: $downloadUrl'); 120 | 121 | // Download the file 122 | var client = HttpClient(); 123 | var request = await client.getUrl(Uri.parse(downloadUrl!)); 124 | var response = await request.close(); 125 | 126 | // Read the response and write it to a file 127 | String fileName = path.basename(downloadUrl); 128 | String filePath = '${htPath}download/$fileName'; 129 | var file = File(filePath); 130 | var fileSink = file.openWrite(); 131 | await response.pipe(fileSink); 132 | fileSink.close(); 133 | 134 | dbg('downloaded to $filePath. Extracting ...'); 135 | 136 | await unzipFile(filePath); 137 | 138 | file.deleteSync(); 139 | 140 | var extractedFileName = Directory("${htPath}download").listSync()[0].path; 141 | dbg("extractedFileName: $extractedFileName"); 142 | 143 | Process.runSync(extractedFileName, ['-i']); 144 | dbg("Process.runSync finished"); 145 | 146 | File("${htPath}update_available").deleteSync(); 147 | 148 | print(" Updated to $newReleaseVersionNumber. Please run ht again.\n"); 149 | 150 | // wrapper script will handle the rest 151 | exit(0); 152 | } else { 153 | print('No matching asset found for platform $platformKey'); 154 | } 155 | } else { 156 | print('Failed to load latest release version'); 157 | } 158 | 159 | exit(0); 160 | } 161 | 162 | Future unzipFile(String filePath) async { 163 | var destinationPath = "${htPath}download"; 164 | var result = 165 | await Process.run('unzip', ['-o', filePath, '-d', destinationPath]); 166 | 167 | if (result.exitCode != 0) { 168 | print('Error unzipping file: ${result.stderr}'); 169 | } else { 170 | dbg('File unzipped to $destinationPath'); 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /lib/installation_and_update.dart: -------------------------------------------------------------------------------- 1 | import 'package:ht/ansi_codes.dart'; 2 | 3 | import 'wrapper_script.dart'; 4 | import 'dart:io'; 5 | import 'globals.dart'; 6 | import 'debug.dart'; 7 | 8 | bool checkInstallation() { 9 | dbg("checkInstallation started"); 10 | if (!File("${htPath}ht").existsSync()) { 11 | dbg("${htPath}ht not found"); 12 | installOrUpdate(); 13 | return true; 14 | } else { 15 | dbg("ht found, checking version"); 16 | // extracting version from installed warpper script 17 | var htWrapperScript = File("${htPath}ht").readAsStringSync(); 18 | dbg("htWrapperScript:\n $htWrapperScript"); 19 | var foundVersion = parseSemVer(htWrapperScript); 20 | dbg("this version: $version installed version: $foundVersion"); 21 | if (isSemVerHigher(foundVersion, version)) { 22 | dbg("installed version is lower"); 23 | installOrUpdate(); 24 | return true; 25 | } else { 26 | dbg("installed version is the same or higher"); 27 | print(" 🤖 ht is already installed and up to date."); 28 | exit(1); 29 | } 30 | } 31 | } 32 | 33 | bool installOrUpdate() { 34 | dbg("install started"); 35 | 36 | // check if ~/.config/ exists 37 | if (!Directory("$home/.config").existsSync()) { 38 | dbg("creating $home/.config"); 39 | try { 40 | Directory("$home/.config").createSync(recursive: false); 41 | } catch (e) { 42 | print("Error creating directory: $e"); 43 | return false; 44 | } 45 | } 46 | 47 | // check if ~/.config/ht exists 48 | if (!Directory(htPath).existsSync()) { 49 | dbg("creating $htPath"); 50 | try { 51 | Directory(htPath).createSync(recursive: false); 52 | } catch (e) { 53 | print("Error creating directory: $e"); 54 | return false; 55 | } 56 | } 57 | final configFile = File("${htPath}config"); 58 | if (!configFile.existsSync()) { 59 | try { 60 | configFile.createSync(recursive: true); 61 | configFile.writeAsStringSync('debug: false\n', mode: FileMode.append); 62 | } catch (e) { 63 | print("Error creating config file: $e"); 64 | return false; 65 | } 66 | } 67 | 68 | // check if ~/.config/ht/ht exists 69 | if (!File("${htPath}ht").existsSync()) { 70 | dbg("creating ${htPath}ht"); 71 | try { 72 | File("${htPath}ht").createSync(recursive: false); 73 | } catch (e) { 74 | print("Error creating file: $e"); 75 | return false; 76 | } 77 | } 78 | // write wrapper script 79 | if (!writeWrapperScript()) { 80 | print("Error writing wrapper script."); 81 | return false; 82 | } 83 | // add to PATH 84 | if (!addToPATH()) { 85 | print("Error adding ht to PATH."); 86 | return false; 87 | } 88 | // copy this file to ~/.config/ht/ht.bin 89 | // find the path of this file 90 | var thisFilePath = Platform.script.path; 91 | dbg("path: $thisFilePath"); 92 | // copy this file to ~/.config/ht/ht.bin 93 | try { 94 | File(thisFilePath).copySync("${htPath}ht.bin"); 95 | } catch (e) { 96 | print("Error copying file: $e"); 97 | //return false; 98 | } 99 | 100 | print( 101 | " 🤖 ht installed successfully. Please close and reopen the terminal and type ${acBold}ht$acReset to start."); 102 | exit(0); 103 | } 104 | 105 | String parseSemVer(String line) { 106 | final RegExp semVerRegex = RegExp(r'\bv(\d+\.\d+\.\d+)\b'); 107 | final match = semVerRegex.firstMatch(line); 108 | 109 | if (match != null) { 110 | return match.group(1) ?? ''; // Return the matched version string 111 | } else { 112 | return ''; // Return empty string if no match is found 113 | } 114 | } 115 | 116 | bool isSemVerHigher(String installedVersion, String thisVersion) { 117 | List installedParts = 118 | installedVersion.split('.').map(int.parse).toList(); 119 | List thisParts = thisVersion.split('.').map(int.parse).toList(); 120 | 121 | for (int i = 0; i < installedParts.length; i++) { 122 | if (thisParts[i] > installedParts[i]) { 123 | return true; 124 | } else if (thisParts[i] < installedParts[i]) { 125 | return false; 126 | } 127 | } 128 | return false; // Return false if versions are identical 129 | } 130 | 131 | bool writeWrapperScript() { 132 | dbg("writeWrapperScript started"); 133 | var wrapperScriptFile = File("${htPath}ht"); 134 | try { 135 | wrapperScriptFile.writeAsStringSync(wrapperScript); 136 | Process.runSync("chmod", ["+x", "${htPath}ht"]); 137 | return true; 138 | } catch (e) { 139 | print("Problem writing wrapper script: $e"); 140 | return false; 141 | } 142 | } 143 | 144 | bool addToPATH() { 145 | // add to PATH for bash, fish and zsh 146 | dbg("addToPATH started"); 147 | dbg("home: $home"); 148 | 149 | // bash 150 | if (File("$home/.bashrc").existsSync()) { 151 | dbg("bash config found"); 152 | // check if ~/.bashrc contains htPath 153 | var bashrc = File("$home/.bashrc").readAsStringSync(); 154 | if (bashrc.contains(htPath)) { 155 | dbg("ht is already in bash PATH."); 156 | } else { 157 | // add htPath to ~/.bashrc 158 | var output = Process.runSync( 159 | "bash", ["-c", "echo 'export PATH=\$PATH:$htPath' >> ~/.bashrc"]); 160 | dbg("ht added to bash PATH. $output"); 161 | } 162 | } else { 163 | dbg("creating ~/.bashrc"); 164 | try { 165 | File("$home/.bashrc").createSync(recursive: false); 166 | var output = Process.runSync( 167 | "bash", ["-c", "echo 'export PATH=\$PATH:$htPath' >> ~/.bashrc"]); 168 | dbg("ht added to bash PATH. $output"); 169 | } catch (e) { 170 | print("Error creating file: $e"); 171 | return false; 172 | } 173 | } 174 | 175 | // fish 176 | if (File("$home/.config/fish/config.fish").existsSync()) { 177 | dbg("fish config found"); 178 | var fishConfig = File("$home/.config/fish/config.fish").readAsStringSync(); 179 | if (fishConfig.contains(htPath)) { 180 | dbg("ht is already in fish PATH."); 181 | } else { 182 | var output = Process.runSync("fish", [ 183 | "-c", 184 | "echo 'set -gx PATH \$PATH $htPath' >> ~/.config/fish/config.fish" 185 | ]); 186 | dbg("ht added to fish PATH. $output"); 187 | } 188 | } 189 | 190 | // zsh 191 | if (File("$home/.zshrc").existsSync()) { 192 | dbg("zsh config found"); 193 | var zshrc = File("$home/.zshrc").readAsStringSync(); 194 | if (zshrc.contains(htPath)) { 195 | dbg("ht is already in zsh PATH."); 196 | } else { 197 | var output = Process.runSync( 198 | "zsh", ["-c", "echo 'export PATH=\$PATH:$htPath' >> ~/.zshrc"]); 199 | dbg("ht added to zsh PATH. $output"); 200 | } 201 | } else { 202 | dbg("creating ~/.zshrc"); 203 | try { 204 | File("$home/.zshrc").createSync(recursive: false); 205 | var output = Process.runSync( 206 | "zsh", ["-c", "echo 'export PATH=\$PATH:$htPath' >> ~/.zshrc"]); 207 | dbg("ht added to zsh PATH. $output"); 208 | } catch (e) { 209 | print("Error creating file: $e"); 210 | return false; 211 | } 212 | } 213 | 214 | return true; 215 | } 216 | -------------------------------------------------------------------------------- /lib/ter_print.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:ht/ansi_codes.dart'; 3 | import 'debug.dart'; 4 | 5 | var terminalWidth = 80; 6 | 7 | // decides which function to use 8 | void terPrint(String line) { 9 | line = line.replaceAll("\n", ""); 10 | 11 | dbg("terPrint line: $line"); 12 | 13 | if (line.contains("#DSCR:")) { 14 | terPrintCommandAndDescription(line); 15 | } else if (line.contains("#ERR:") || line.contains("#WRN:")) { 16 | terPrintErrorsAndWarnings(line); 17 | } else if (line.contains("#COR:")) { 18 | terPrintCorrectedVersion(line); 19 | } else if (line.contains("#SS:")) { 20 | terPrintSummary(line); 21 | } else { 22 | if (line.trim().isEmpty) { 23 | return; 24 | } 25 | terPrintLine(line); 26 | } 27 | return; 28 | } 29 | 30 | // returns a list of lines that fit into terminal width 31 | List fitIntoTerminalWidth(line, {int indentation = 0}) { 32 | terminalWidth = 80; 33 | if (stdout.hasTerminal) terminalWidth = stdout.terminalColumns; 34 | 35 | String spaces = " " * indentation; 36 | 37 | var lines = []; 38 | lines.add(spaces + line); 39 | 40 | for (var i = 0; i < lines.length; i++) { 41 | if ((lines[i].length) > terminalWidth) { 42 | int lastSpace = 0; 43 | lastSpace = lines[i].substring(0, terminalWidth).lastIndexOf(" "); 44 | dbg("TerminalWidth: $terminalWidth, lastSpace: $lastSpace, lineLength: ${lines[i].length}"); 45 | var line1 = lines[i].substring(0, lastSpace); 46 | var line2 = lines[i].substring(lastSpace + 1); 47 | line2 = line2; 48 | lines[i] = line1; 49 | lines.insert(i + 1, spaces + line2); 50 | } 51 | } 52 | // check if there are empty lines and remove them 53 | for (var i = 0; i < lines.length; i++) { 54 | lines[i] = lines[i].trimRight(); 55 | if (lines[i].isEmpty) { 56 | lines.removeAt(i); 57 | } 58 | } 59 | return lines; 60 | } 61 | 62 | // 63 | void terPrintErrorsAndWarnings(String line) { 64 | dbg("line: $line"); 65 | terminalWidth = 80; 66 | if (stdout.hasTerminal) terminalWidth = stdout.terminalColumns; 67 | String emoji = ""; 68 | 69 | if (line.contains("#ERR:")) { 70 | if (line.contains("none")) return; 71 | emoji = "❌"; 72 | } else if (line.contains("#WRN:")) { 73 | emoji = "$acBrightYellow⚠️$acReset"; 74 | } 75 | line = line.replaceAll("#ERR:", ""); 76 | line = line.replaceAll("#WRN:", ""); 77 | 78 | List lines = fitIntoTerminalWidth(line, indentation: 3); 79 | 80 | for (var i = 0; i < lines.length; i++) { 81 | if (i == 0) { 82 | lines[i] = lines[i].replaceAll(RegExp(r"^\s+"), "$emoji $acBold"); 83 | } 84 | lines[i] = lines[i].trimRight(); 85 | print(lines[i]); 86 | } 87 | print(acReset); 88 | return; 89 | } 90 | 91 | // 92 | void terPrintCorrectedVersion(line) { 93 | line = line.replaceAll("#COR:", ""); 94 | 95 | line = "$acBrightGreen✓ $acReset Corrected version:$acBold$line$acReset\n"; 96 | stdout.write(line); 97 | print(""); 98 | } 99 | 100 | // 101 | void terPrintCommandAndDescription(String line) { 102 | dbg(line); 103 | 104 | line = line.replaceAll("#CMD:", ""); 105 | line = line.replaceAll("#SB1:", ""); 106 | line = line.replaceAll("#SB2:", " └─"); 107 | line = line.replaceAll("#SB3:", " └─"); 108 | line = line.replaceAll("#OPR:", ""); 109 | 110 | var parts = line.split("#DSCR:"); 111 | var command = parts[0]; 112 | 113 | terminalWidth = 80; 114 | if (stdout.hasTerminal) terminalWidth = stdout.terminalColumns; 115 | dbg("terminalWidth: $terminalWidth, command: $command, commandLength: ${command.length}"); 116 | 117 | int fixedCommandWidth = 27; 118 | 119 | var lines = []; 120 | 121 | // if command is longer than 27 chars, write description to additional line 122 | if (command.length > fixedCommandWidth) { 123 | var parts = line.split("#DSCR:"); 124 | lines.insert(0, parts[0].trimRight()); 125 | // insert parts[1] as new line in Lines 126 | lines.insert(1, "#DSCR:${parts[1].trim()}"); 127 | } else { 128 | lines.insert(0, line.trimRight()); 129 | } 130 | 131 | // iterate through lines to add dots padding 132 | for (var i = 0; i < lines.length; i++) { 133 | // if line does not contain #DSCR: skip 134 | if (!lines[i].contains("#DSCR:")) { 135 | //continue; 136 | } 137 | 138 | // if line starts with #DSCR: 139 | if (lines[i].startsWith("#DSCR:")) { 140 | lines[i] = lines[i] 141 | .replaceAll("#DSCR:", "$acReset "); 142 | continue; 143 | } 144 | 145 | // if line contains #DSCR: 146 | if (lines[i].contains("#DSCR:")) { 147 | // fill space between command and #DSCR: with dots 148 | var parts = lines[i].split("#DSCR:"); 149 | var command = parts[0].trimRight(); 150 | var description = parts[1].trim(); 151 | int dotPaddingLength = fixedCommandWidth - command.length; 152 | String dotPadding = dotPaddingLength > 0 ? '.' * dotPaddingLength : ""; 153 | lines[i] = "$command $acGrey$dotPadding$acReset $description"; 154 | } 155 | } 156 | 157 | // ensure line fits into terminal width 158 | for (var i = 0; i < lines.length; i++) { 159 | // if line is longer than terminal width, -9 for ansi codes 160 | if ((lines[i].length - 9) > terminalWidth) { 161 | int lastSpace = 0; 162 | lastSpace = lines[i].substring(0, terminalWidth).lastIndexOf(" "); 163 | var line1 = lines[i].substring(0, lastSpace); 164 | var line2 = lines[i].substring(lastSpace + 1); 165 | line2 = " $line2"; 166 | lines[i] = line1; 167 | lines.insert(i + 1, line2); 168 | } 169 | } 170 | 171 | // replace tags 172 | for (var i = 0; i < lines.length; i++) { 173 | // if line starts with 29 * " " (for skipped description lines) 174 | if (!lines[i].startsWith(" ")) { 175 | lines[i] = acBold + lines[i]; 176 | } 177 | 178 | lines[i] = lines[i].replaceAll("└─", "$acGrey└─$acReset$acBold"); 179 | lines[i] = lines[i].replaceAll("#DSCR:", "$acReset ${" " * 19}$acBold"); 180 | lines[i] = lines[i].trimRight(); 181 | } 182 | 183 | // print all lines 184 | for (var i = 0; i < lines.length; i++) { 185 | print(lines[i]); 186 | } 187 | 188 | return; 189 | } 190 | 191 | //summary ───────────────────────────────────────────────────────────────────── 192 | void terPrintSummary(line) { 193 | dbg("terPrintSummary: $line"); 194 | 195 | // replacements for some edge cases 196 | 197 | line = line.replaceAll("#CMD:", ""); 198 | line = line.replaceAll("#SB1:", ""); 199 | line = line.replaceAll("#SB2:", ""); 200 | line = line.replaceAll("#SB3:", ""); 201 | line = line.replaceAll("#SS:", ""); 202 | line = line.replaceAll("#COR:", ""); 203 | line = line.replaceAll("#ERR:", ""); 204 | line = line.replaceAll("#WRN:", ""); 205 | line = line.replaceAll("\n", ""); 206 | 207 | line = line.replaceAll("#SS:", ""); 208 | line = line.trim(); 209 | 210 | List lines = fitIntoTerminalWidth(line, indentation: 1); 211 | 212 | // print lines 213 | for (var i = 0; i < lines.length; i++) { 214 | if (i == 0) lines[i] = "\n$acItalic${lines[i]}"; 215 | print(lines[i]); 216 | } 217 | print(""); 218 | } 219 | 220 | // 221 | void terPrintLine(String line, {int indentation = 0}) { 222 | List lines = fitIntoTerminalWidth(line, indentation: indentation); 223 | 224 | for (var i = 0; i < lines.length; i++) { 225 | print(lines[i]); 226 | } 227 | return; 228 | } 229 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | _fe_analyzer_shared: 5 | dependency: transitive 6 | description: 7 | name: _fe_analyzer_shared 8 | sha256: "36a321c3d2cbe01cbcb3540a87b8843846e0206df3e691fa7b23e19e78de6d49" 9 | url: "https://pub.dev" 10 | source: hosted 11 | version: "65.0.0" 12 | analyzer: 13 | dependency: transitive 14 | description: 15 | name: analyzer 16 | sha256: dfe03b90ec022450e22513b5e5ca1f01c0c01de9c3fba2f7fd233cb57a6b9a07 17 | url: "https://pub.dev" 18 | source: hosted 19 | version: "6.3.0" 20 | args: 21 | dependency: transitive 22 | description: 23 | name: args 24 | sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 25 | url: "https://pub.dev" 26 | source: hosted 27 | version: "2.4.2" 28 | async: 29 | dependency: transitive 30 | description: 31 | name: async 32 | sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" 33 | url: "https://pub.dev" 34 | source: hosted 35 | version: "2.11.0" 36 | boolean_selector: 37 | dependency: transitive 38 | description: 39 | name: boolean_selector 40 | sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" 41 | url: "https://pub.dev" 42 | source: hosted 43 | version: "2.1.1" 44 | collection: 45 | dependency: transitive 46 | description: 47 | name: collection 48 | sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a 49 | url: "https://pub.dev" 50 | source: hosted 51 | version: "1.18.0" 52 | compute: 53 | dependency: "direct main" 54 | description: 55 | name: compute 56 | sha256: b70190d59352a267a9765a77b97d0f619874c84d30c5193ae71050ee22ab2ef7 57 | url: "https://pub.dev" 58 | source: hosted 59 | version: "1.0.2" 60 | convert: 61 | dependency: transitive 62 | description: 63 | name: convert 64 | sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" 65 | url: "https://pub.dev" 66 | source: hosted 67 | version: "3.1.1" 68 | coverage: 69 | dependency: transitive 70 | description: 71 | name: coverage 72 | sha256: ac86d3abab0f165e4b8f561280ff4e066bceaac83c424dd19f1ae2c2fcd12ca9 73 | url: "https://pub.dev" 74 | source: hosted 75 | version: "1.7.1" 76 | crypto: 77 | dependency: transitive 78 | description: 79 | name: crypto 80 | sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab 81 | url: "https://pub.dev" 82 | source: hosted 83 | version: "3.0.3" 84 | file: 85 | dependency: transitive 86 | description: 87 | name: file 88 | sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" 89 | url: "https://pub.dev" 90 | source: hosted 91 | version: "7.0.0" 92 | frontend_server_client: 93 | dependency: transitive 94 | description: 95 | name: frontend_server_client 96 | sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" 97 | url: "https://pub.dev" 98 | source: hosted 99 | version: "3.2.0" 100 | glob: 101 | dependency: transitive 102 | description: 103 | name: glob 104 | sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" 105 | url: "https://pub.dev" 106 | source: hosted 107 | version: "2.1.2" 108 | http: 109 | dependency: "direct main" 110 | description: 111 | name: http 112 | sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" 113 | url: "https://pub.dev" 114 | source: hosted 115 | version: "1.1.0" 116 | http_multi_server: 117 | dependency: transitive 118 | description: 119 | name: http_multi_server 120 | sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" 121 | url: "https://pub.dev" 122 | source: hosted 123 | version: "3.2.1" 124 | http_parser: 125 | dependency: transitive 126 | description: 127 | name: http_parser 128 | sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" 129 | url: "https://pub.dev" 130 | source: hosted 131 | version: "4.0.2" 132 | io: 133 | dependency: transitive 134 | description: 135 | name: io 136 | sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" 137 | url: "https://pub.dev" 138 | source: hosted 139 | version: "1.0.4" 140 | js: 141 | dependency: transitive 142 | description: 143 | name: js 144 | sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 145 | url: "https://pub.dev" 146 | source: hosted 147 | version: "0.6.7" 148 | lints: 149 | dependency: "direct dev" 150 | description: 151 | name: lints 152 | sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" 153 | url: "https://pub.dev" 154 | source: hosted 155 | version: "2.1.1" 156 | logging: 157 | dependency: transitive 158 | description: 159 | name: logging 160 | sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" 161 | url: "https://pub.dev" 162 | source: hosted 163 | version: "1.2.0" 164 | matcher: 165 | dependency: transitive 166 | description: 167 | name: matcher 168 | sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" 169 | url: "https://pub.dev" 170 | source: hosted 171 | version: "0.12.16" 172 | meta: 173 | dependency: transitive 174 | description: 175 | name: meta 176 | sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 177 | url: "https://pub.dev" 178 | source: hosted 179 | version: "1.11.0" 180 | mime: 181 | dependency: transitive 182 | description: 183 | name: mime 184 | sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e 185 | url: "https://pub.dev" 186 | source: hosted 187 | version: "1.0.4" 188 | node_preamble: 189 | dependency: transitive 190 | description: 191 | name: node_preamble 192 | sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" 193 | url: "https://pub.dev" 194 | source: hosted 195 | version: "2.0.2" 196 | package_config: 197 | dependency: transitive 198 | description: 199 | name: package_config 200 | sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" 201 | url: "https://pub.dev" 202 | source: hosted 203 | version: "2.1.0" 204 | path: 205 | dependency: transitive 206 | description: 207 | name: path 208 | sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" 209 | url: "https://pub.dev" 210 | source: hosted 211 | version: "1.8.3" 212 | pool: 213 | dependency: transitive 214 | description: 215 | name: pool 216 | sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" 217 | url: "https://pub.dev" 218 | source: hosted 219 | version: "1.5.1" 220 | pub_semver: 221 | dependency: transitive 222 | description: 223 | name: pub_semver 224 | sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" 225 | url: "https://pub.dev" 226 | source: hosted 227 | version: "2.1.4" 228 | shelf: 229 | dependency: transitive 230 | description: 231 | name: shelf 232 | sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 233 | url: "https://pub.dev" 234 | source: hosted 235 | version: "1.4.1" 236 | shelf_packages_handler: 237 | dependency: transitive 238 | description: 239 | name: shelf_packages_handler 240 | sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" 241 | url: "https://pub.dev" 242 | source: hosted 243 | version: "3.0.2" 244 | shelf_static: 245 | dependency: transitive 246 | description: 247 | name: shelf_static 248 | sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e 249 | url: "https://pub.dev" 250 | source: hosted 251 | version: "1.1.2" 252 | shelf_web_socket: 253 | dependency: transitive 254 | description: 255 | name: shelf_web_socket 256 | sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" 257 | url: "https://pub.dev" 258 | source: hosted 259 | version: "1.0.4" 260 | source_map_stack_trace: 261 | dependency: transitive 262 | description: 263 | name: source_map_stack_trace 264 | sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae" 265 | url: "https://pub.dev" 266 | source: hosted 267 | version: "2.1.1" 268 | source_maps: 269 | dependency: transitive 270 | description: 271 | name: source_maps 272 | sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" 273 | url: "https://pub.dev" 274 | source: hosted 275 | version: "0.10.12" 276 | source_span: 277 | dependency: transitive 278 | description: 279 | name: source_span 280 | sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" 281 | url: "https://pub.dev" 282 | source: hosted 283 | version: "1.10.0" 284 | stack_trace: 285 | dependency: transitive 286 | description: 287 | name: stack_trace 288 | sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" 289 | url: "https://pub.dev" 290 | source: hosted 291 | version: "1.11.1" 292 | stream_channel: 293 | dependency: transitive 294 | description: 295 | name: stream_channel 296 | sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 297 | url: "https://pub.dev" 298 | source: hosted 299 | version: "2.1.2" 300 | string_scanner: 301 | dependency: transitive 302 | description: 303 | name: string_scanner 304 | sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" 305 | url: "https://pub.dev" 306 | source: hosted 307 | version: "1.2.0" 308 | term_glyph: 309 | dependency: transitive 310 | description: 311 | name: term_glyph 312 | sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 313 | url: "https://pub.dev" 314 | source: hosted 315 | version: "1.2.1" 316 | test: 317 | dependency: "direct dev" 318 | description: 319 | name: test 320 | sha256: a1f7595805820fcc05e5c52e3a231aedd0b72972cb333e8c738a8b1239448b6f 321 | url: "https://pub.dev" 322 | source: hosted 323 | version: "1.24.9" 324 | test_api: 325 | dependency: transitive 326 | description: 327 | name: test_api 328 | sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" 329 | url: "https://pub.dev" 330 | source: hosted 331 | version: "0.6.1" 332 | test_core: 333 | dependency: transitive 334 | description: 335 | name: test_core 336 | sha256: a757b14fc47507060a162cc2530d9a4a2f92f5100a952c7443b5cad5ef5b106a 337 | url: "https://pub.dev" 338 | source: hosted 339 | version: "0.5.9" 340 | typed_data: 341 | dependency: transitive 342 | description: 343 | name: typed_data 344 | sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c 345 | url: "https://pub.dev" 346 | source: hosted 347 | version: "1.3.2" 348 | vm_service: 349 | dependency: transitive 350 | description: 351 | name: vm_service 352 | sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 353 | url: "https://pub.dev" 354 | source: hosted 355 | version: "13.0.0" 356 | watcher: 357 | dependency: transitive 358 | description: 359 | name: watcher 360 | sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" 361 | url: "https://pub.dev" 362 | source: hosted 363 | version: "1.1.0" 364 | web_socket_channel: 365 | dependency: transitive 366 | description: 367 | name: web_socket_channel 368 | sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b 369 | url: "https://pub.dev" 370 | source: hosted 371 | version: "2.4.0" 372 | webkit_inspection_protocol: 373 | dependency: transitive 374 | description: 375 | name: webkit_inspection_protocol 376 | sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" 377 | url: "https://pub.dev" 378 | source: hosted 379 | version: "1.2.1" 380 | yaml: 381 | dependency: transitive 382 | description: 383 | name: yaml 384 | sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" 385 | url: "https://pub.dev" 386 | source: hosted 387 | version: "3.1.2" 388 | sdks: 389 | dart: ">=3.0.0 <4.0.0" 390 | --------------------------------------------------------------------------------