This commit is contained in:
commit
e517b193e5
20 changed files with 1237 additions and 0 deletions
42
.forgejo/workflows/build.yml
Normal file
42
.forgejo/workflows/build.yml
Normal file
|
@ -0,0 +1,42 @@
|
|||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
paths:
|
||||
- "**.cs"
|
||||
- "**.csproj"
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
DOTNET_VERSION: "8.0.407"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: docker
|
||||
container:
|
||||
image: ghcr.io/catthehacker/ubuntu:act-24.04
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set Date
|
||||
run: echo "DATE=$(date +'%Y%m%dT%H%M%SZ')" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup .NET Core
|
||||
uses: https://github.com/actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: ${{ env.DOTNET_VERSION }}
|
||||
|
||||
- name: Install Dependencies
|
||||
run: dotnet restore
|
||||
|
||||
- name: Build
|
||||
run: dotnet build --configuration Release --no-restore
|
||||
|
||||
- name: Upload Build Artifact
|
||||
uses: https://git.devmarked.win/actions/upload-artifact@v4
|
||||
with:
|
||||
name: build-${{ env.DATE }}.zip
|
||||
path: src/bin/Release/net8.0
|
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
**/bin
|
||||
**/obj
|
7
.luaurc
Normal file
7
.luaurc
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"languageMode": "strict",
|
||||
"aliases": {
|
||||
"lune": "~/.lune/.typedefs/0.8.9",
|
||||
"pkg": "lune/pkg"
|
||||
}
|
||||
}
|
22
CSSharpTemplate.sln
Normal file
22
CSSharpTemplate.sln
Normal file
|
@ -0,0 +1,22 @@
|
|||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.0.31903.59
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CSSharpTemplate", "src\CSSharpTemplate.csproj", "{A50E1683-7ED3-4FE7-A752-DAFC22ABBDBD}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{A50E1683-7ED3-4FE7-A752-DAFC22ABBDBD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A50E1683-7ED3-4FE7-A752-DAFC22ABBDBD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A50E1683-7ED3-4FE7-A752-DAFC22ABBDBD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A50E1683-7ED3-4FE7-A752-DAFC22ABBDBD}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2025 marked
|
||||
|
||||
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:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
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.
|
9
PROJECT-SETUP.md
Normal file
9
PROJECT-SETUP.md
Normal file
|
@ -0,0 +1,9 @@
|
|||
## PROJECT SETUP
|
||||
|
||||
To set up the template:
|
||||
1. Make a repository using the template
|
||||
2. `git clone YOUR-REPOSITORY-URL MyPlugin`
|
||||
3. `cd MyPlugin`
|
||||
4. `lune run manager -- setup MyPlugin`
|
||||
|
||||
This file will be deleted afterwards.
|
53
README.md
Normal file
53
README.md
Normal file
|
@ -0,0 +1,53 @@
|
|||
<h1 align=center><code>CSSharpTemplate</code></h1>
|
||||
|
||||
<div align=center>
|
||||
<a href="https://git.devmarked.win/marked/CSSharpTemplate/actions?workflow=build.yml"><img src="https://git.devmarked.win/marked/CSSharpTemplate/badges/workflows/build.yml/badge.svg?style=for-the-badge&label=BUILD"></a>
|
||||
<a href="https://git.devmarked.win/marked/CSSharpTemplate/src/branch/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue?style=for-the-badge"></a>
|
||||
</div>
|
||||
<br>
|
||||
|
||||
Description
|
||||
|
||||
# Installation
|
||||
|
||||
1. Download the latest build artifact from [the build action](https://git.devmarked.win/marked/CSSharpTemplate/actions?workflow=build.yml)
|
||||
2. Extract it in `addons/counterstrikesharp/plugins`
|
||||
|
||||
# Development
|
||||
|
||||
## Setup
|
||||
|
||||
This project uses [Lune](https://github.com/lune-org/lune) to feature a cross-platform helper script.\
|
||||
To install it, fetch the latest binary from [GitHub releases](https://github.com/lune-org/lune/releases/tag/v0.8.9), or install it via cargo:
|
||||
```sh
|
||||
cargo install --locked lune
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
For a basic debug build, run:
|
||||
```sh
|
||||
lune run manager -- build
|
||||
```
|
||||
|
||||
To specify a configuration:
|
||||
```sh
|
||||
lune run manager -- build --configuration CONFIGURATION
|
||||
```
|
||||
|
||||
To replicate the built plugin to a docker container:
|
||||
```sh
|
||||
lune run manager -- build --replicator docker --remote-output container-id:/path/to/plugin
|
||||
```
|
||||
|
||||
To replicate the built plugin to a remote machine:
|
||||
```sh
|
||||
lune run manager -- build --replicator ssh --remote-output username@hostname:/path/to/plugin
|
||||
```
|
||||
|
||||
To replicate the built plugin to a docker container on a remote machine:
|
||||
```sh
|
||||
lune run manager -- build --replicator ssh-docker --remote-output username@hostname->container-id:/path/to/plugin
|
||||
```
|
||||
|
||||
You can also write a custom replicator in [Luau](https://github.com/luau-lang/luau) under `lune/manager/replicators`.
|
46
lune/manager/build.luau
Normal file
46
lune/manager/build.luau
Normal file
|
@ -0,0 +1,46 @@
|
|||
--!strict
|
||||
local common = require("./common")
|
||||
local progress_bar = require("@pkg/progress")
|
||||
local result = require("@pkg/result")
|
||||
|
||||
export type ReplicatorData = {
|
||||
remote_output: string,
|
||||
replicator: string,
|
||||
}
|
||||
local function build(configuration: string, replicator_data: ReplicatorData?)
|
||||
local progress = progress_bar.new():with_stage("init", "Initializing"):with_stage("build", "Building project")
|
||||
if replicator_data then
|
||||
progress:with_stage("replicate", "Replicating to remote server")
|
||||
end
|
||||
|
||||
progress:start() -- init
|
||||
|
||||
progress:next_stage() -- build
|
||||
|
||||
local build_result = common.spawn(`dotnet build -c {configuration}`)
|
||||
if not build_result.ok then
|
||||
progress:stop()
|
||||
return result(false, `Failed to build:\n{build_result.err}`)
|
||||
end
|
||||
|
||||
if replicator_data then
|
||||
progress:next_stage() -- replicate
|
||||
|
||||
progress:pause()
|
||||
local replicator: common.Replicator = (require)(`./replicators/{replicator_data.replicator}`)
|
||||
local replication_result = replicator(`src/bin/{configuration}/net8.0`, replicator_data.remote_output)
|
||||
if not replication_result.ok then
|
||||
progress:stop()
|
||||
return result(false, `Failed to replicate build:\n{replication_result.err}`)
|
||||
end
|
||||
progress:unpause()
|
||||
end
|
||||
|
||||
progress:stop() -- finish
|
||||
|
||||
print(build_result.val.stdout)
|
||||
|
||||
return result(true, nil)
|
||||
end
|
||||
|
||||
return build
|
46
lune/manager/common.luau
Normal file
46
lune/manager/common.luau
Normal file
|
@ -0,0 +1,46 @@
|
|||
--!strict
|
||||
local process = require("@lune/process")
|
||||
local stdio = require("@lune/stdio")
|
||||
|
||||
local result = require("@pkg/result")
|
||||
|
||||
local function c_print(msg: string)
|
||||
stdio.write(`{stdio.color("blue")}[info]{stdio.color("reset")} {msg}\n`)
|
||||
end
|
||||
|
||||
local function c_warn(msg: string)
|
||||
stdio.write(`{stdio.color("yellow")}[warn]{stdio.color("reset")} {msg}\n`)
|
||||
end
|
||||
|
||||
local function c_error(msg: string)
|
||||
stdio.ewrite(`{stdio.color("red")}[error]{stdio.color("reset")} {msg}\n`)
|
||||
end
|
||||
|
||||
local function c_spawn(command: string): result.Identity<process.SpawnResult>
|
||||
local args = string.split(command, " ")
|
||||
local program = table.remove(args, 1)
|
||||
if not program then
|
||||
return result(false, "Couldn't find program in command.")
|
||||
end
|
||||
|
||||
local spawn_result = process.spawn(program, args)
|
||||
if not spawn_result.ok then
|
||||
return result(false, spawn_result.stderr)
|
||||
end
|
||||
|
||||
return result(true, spawn_result)
|
||||
end
|
||||
|
||||
local common = {
|
||||
default_name = "CSSharpTemplate",
|
||||
|
||||
print = c_print,
|
||||
warn = c_warn,
|
||||
error = c_error,
|
||||
|
||||
spawn = c_spawn,
|
||||
}
|
||||
|
||||
export type Replicator = (dist_folder: string, remote_output: string) -> result.Identity<nil>
|
||||
|
||||
return common
|
67
lune/manager/init.luau
Normal file
67
lune/manager/init.luau
Normal file
|
@ -0,0 +1,67 @@
|
|||
--!strict
|
||||
local process = require("@lune/process")
|
||||
|
||||
local common = require("./common")
|
||||
local frkcli = require("@pkg/frkcli")
|
||||
|
||||
local cli = frkcli.new_subcommands("build_cli", "CLI for automatic basic C# tasks")
|
||||
|
||||
local command_build = cli:add_subcommand(
|
||||
"build",
|
||||
[[
|
||||
Build the project.
|
||||
|
||||
Additionally, you can use a replicator to replicate your built plugin to:
|
||||
1. A docker container:
|
||||
`lune run manager -- build --replicator docker --remote-output container-id:/path/to/plugin`
|
||||
2. A remote machine:
|
||||
`lune run manager -- build --replicator ssh --remote-output username@hostname:/path/to/plugin`
|
||||
3. A docker container on a remote machine:
|
||||
`lune run manager -- build --replicator docker-ssh --remote-output username@hostname->container-id:/path/to/plugin`
|
||||
|
||||
You can also use a custom replicator, as long as it's located inside `lune/manager/replicators`.
|
||||
]]
|
||||
)
|
||||
command_build:add_option(
|
||||
"configuration",
|
||||
{ help = "The configuration to use for building the project", aliases = { "c" }, default = "Debug" }
|
||||
)
|
||||
command_build:add_option(
|
||||
"remote-output",
|
||||
{ help = "Path on a remote machine to copy the built project to", aliases = { "o" }, default = "" }
|
||||
)
|
||||
command_build:add_option(
|
||||
"replicator",
|
||||
{ help = "Replicator to use for copying builds to a remote machine", aliases = { "r" }, default = "ssh" }
|
||||
)
|
||||
|
||||
local command_setup = cli:add_subcommand("setup", "Setup the project")
|
||||
command_setup:add_positional("name", { help = "Name of the project", default = nil })
|
||||
command_setup:add_flag("force", { help = "Force the setup?", aliases = { "f" } })
|
||||
|
||||
local parsed, err = cli:parse(process.args)
|
||||
if err ~= nil then
|
||||
error(err)
|
||||
end
|
||||
assert(parsed ~= nil)
|
||||
|
||||
local values = parsed.result.values
|
||||
local flags = parsed.result.flags
|
||||
if parsed.command == "build" then
|
||||
local replicator_data = nil
|
||||
if values.remote_output ~= "" then
|
||||
replicator_data = {
|
||||
remote_output = values.remote_output,
|
||||
replicator = values.replicator,
|
||||
}
|
||||
end
|
||||
|
||||
require("./build")(values.configuration, replicator_data)
|
||||
elseif parsed.command == "setup" then
|
||||
local setup = require("./setup")
|
||||
local result = setup(values.name, flags.force)
|
||||
|
||||
if not result.ok then
|
||||
common.error(result.err)
|
||||
end
|
||||
end
|
16
lune/manager/replicators/docker.luau
Normal file
16
lune/manager/replicators/docker.luau
Normal file
|
@ -0,0 +1,16 @@
|
|||
--!strict
|
||||
local process = require("@lune/process")
|
||||
|
||||
local common = require("../common")
|
||||
local result = require("@pkg/result")
|
||||
|
||||
local function docker_replicator(dist_folder: string, remote_output: string): result.Identity<nil>
|
||||
local copy_result = process.spawn("docker", { "cp", "-r", dist_folder, remote_output })
|
||||
if not copy_result.ok then
|
||||
return result(false, `Failed to copy files:\n{copy_result.stderr}`)
|
||||
end
|
||||
|
||||
return result(true, nil)
|
||||
end
|
||||
|
||||
return docker_replicator :: common.Replicator
|
41
lune/manager/replicators/ssh-docker.luau
Normal file
41
lune/manager/replicators/ssh-docker.luau
Normal file
|
@ -0,0 +1,41 @@
|
|||
--!strict
|
||||
local process = require("@lune/process")
|
||||
|
||||
local common = require("../common")
|
||||
local result = require("@pkg/result")
|
||||
|
||||
local function ssh_docker_replicator(dist_folder: string, remote_output: string): result.Identity<nil>
|
||||
local split = string.split(remote_output, "->")
|
||||
|
||||
local remote_machine = split[1]
|
||||
if not remote_machine then
|
||||
return result(false, "Couldn't get the remote machine to ssh into.")
|
||||
end
|
||||
|
||||
local docker_path = split[2]
|
||||
if not docker_path then
|
||||
return result(false, "Couldn't get the docker container path for the remote machine.")
|
||||
end
|
||||
|
||||
local ssh_copy_result = process.spawn(
|
||||
"scp",
|
||||
{ "-r", "-O", dist_folder, `{remote_machine}:/tmp/cssharp-replicate` },
|
||||
{ stdio = "forward" }
|
||||
)
|
||||
if not ssh_copy_result.ok then
|
||||
return result(false, `Failed to copy files to the remote machine:\n{ssh_copy_result.stderr}`)
|
||||
end
|
||||
|
||||
local docker_copy_result = process.spawn(
|
||||
"ssh",
|
||||
{ "-t", remote_machine, `"docker cp -r /tmp/cssharp-replicate {docker_path}"` },
|
||||
{ stdio = "forward" }
|
||||
)
|
||||
if not docker_copy_result.ok then
|
||||
return result(false, `Failed to copy files via docker on the remote machine:\n{docker_copy_result.stderr}`)
|
||||
end
|
||||
|
||||
return result(true, nil)
|
||||
end
|
||||
|
||||
return ssh_docker_replicator :: common.Replicator
|
16
lune/manager/replicators/ssh.luau
Normal file
16
lune/manager/replicators/ssh.luau
Normal file
|
@ -0,0 +1,16 @@
|
|||
--!strict
|
||||
local process = require("@lune/process")
|
||||
|
||||
local common = require("../common")
|
||||
local result = require("@pkg/result")
|
||||
|
||||
local function ssh_replicator(dist_folder: string, remote_output: string): result.Identity<nil>
|
||||
local copy_result = process.spawn("scp", { "-r", "-O", dist_folder, remote_output }, { stdio = "forward" })
|
||||
if not copy_result.ok then
|
||||
return result(false, `Failed to copy files:\n{copy_result.stderr}`)
|
||||
end
|
||||
|
||||
return result(true, nil)
|
||||
end
|
||||
|
||||
return ssh_replicator :: common.Replicator
|
69
lune/manager/setup.luau
Normal file
69
lune/manager/setup.luau
Normal file
|
@ -0,0 +1,69 @@
|
|||
--!strict
|
||||
local fs = require("@lune/fs")
|
||||
|
||||
local common = require("./common")
|
||||
local progress_bar = require("@pkg/progress")
|
||||
local result = require("@pkg/result")
|
||||
|
||||
local function setup(name: string, force: boolean): result.Identity<nil>
|
||||
local progress = progress_bar
|
||||
.new()
|
||||
:with_stage("init", "Initializing")
|
||||
:with_stage("prepare", "Cleaning up defaults")
|
||||
:with_stage("modify", "Replacing default names")
|
||||
:with_stage("generate", "Generating .sln")
|
||||
progress:start() -- init
|
||||
|
||||
if not fs.metadata(`{common.default_name}.sln`).exists and not force then
|
||||
progress:stop()
|
||||
return result(false, "Aborting -- the template is already set up.")
|
||||
end
|
||||
|
||||
progress:next_stage() -- prepare
|
||||
|
||||
fs.removeFile(`{common.default_name}.sln`)
|
||||
fs.removeFile(`PROJECT-SETUP.md`)
|
||||
|
||||
progress:next_stage() -- modify
|
||||
|
||||
fs.move(`src/{common.default_name}.cs`, `src/{name}.cs`)
|
||||
fs.move(`src/{common.default_name}.csproj`, `src/{name}.csproj`)
|
||||
|
||||
do
|
||||
local main_src = fs.readFile(`src/{name}.cs`)
|
||||
local edited_src = string.gsub(main_src, common.default_name, name)
|
||||
fs.writeFile(`src/{name}.cs`, edited_src)
|
||||
end
|
||||
|
||||
progress:next_stage() -- generate
|
||||
|
||||
do
|
||||
local res = common.spawn(`dotnet new sln --name {name}`)
|
||||
if not res.ok then
|
||||
return result(false, `Failed to create new sln:\n{res.err}`)
|
||||
end
|
||||
end
|
||||
|
||||
do
|
||||
local res = common.spawn("dotnet sln add src")
|
||||
if not res.ok then
|
||||
return result(false, `Failed to add src to sln:\n{res.err}`)
|
||||
end
|
||||
end
|
||||
|
||||
do
|
||||
local sln_src = fs.readFile(`{name}.sln`)
|
||||
local by_line = string.split(sln_src, "\n")
|
||||
for _ = 1, 3 do
|
||||
table.remove(by_line, 3)
|
||||
end
|
||||
local concated = table.concat(by_line, "\n")
|
||||
fs.writeFile(`{name}.sln`, concated)
|
||||
end
|
||||
|
||||
progress:stop() -- finish
|
||||
|
||||
return result(true, nil)
|
||||
end
|
||||
|
||||
return setup
|
585
lune/pkg/frkcli.luau
Normal file
585
lune/pkg/frkcli.luau
Normal file
|
@ -0,0 +1,585 @@
|
|||
--!nolint LocalShadow
|
||||
|
||||
local process = require("@lune/process")
|
||||
|
||||
local M = {}
|
||||
|
||||
type ArgKind = "POSITIONAL" | "FLAG" | "OPTION"
|
||||
type ArgOptions = {
|
||||
help: string?,
|
||||
aliases: { string }?,
|
||||
default: string?, -- if this is not nil, the arg will be optional
|
||||
}
|
||||
|
||||
-- this is what is stored, we guarante non nullity when args are added so this types makes Luau feel better
|
||||
type ArgOptionsSafe = {
|
||||
help: string,
|
||||
aliases: { string },
|
||||
default: string?,
|
||||
}
|
||||
|
||||
type ArgData = {
|
||||
name: string,
|
||||
kind: ArgKind,
|
||||
options: ArgOptionsSafe,
|
||||
}
|
||||
|
||||
type ParseResult = {
|
||||
values: { [string]: string },
|
||||
flags: { [string]: boolean },
|
||||
fwd_args: { string }, -- all args after `--`
|
||||
}
|
||||
|
||||
type SubcommandParseResult = {
|
||||
command: string,
|
||||
result: ParseResult,
|
||||
}
|
||||
|
||||
local DEFAULT_OPTIONS: ArgOptionsSafe = {
|
||||
help = "",
|
||||
default = nil,
|
||||
aliases = {},
|
||||
}
|
||||
|
||||
local DEFAULT_helpset = {
|
||||
["-h"] = true,
|
||||
["--help"] = true,
|
||||
}
|
||||
|
||||
local function validate_key_or_error(key: string)
|
||||
if (key:sub(1, 2) == "--" or key:sub(1, 1) == "-") and not key:find(" ") then
|
||||
return
|
||||
end
|
||||
error(`arg key {key} is invalid. Keys must start with either '-' or '--' and may not contain spaces`)
|
||||
end
|
||||
|
||||
local function validate_subcommand_name_or_error(name: string)
|
||||
if name:sub(1, 2) == "--" or name:sub(1, 1) == "-" or name:find(" ") then
|
||||
error(`subcommand name '{name}' is invalid. Name must not start with '-' or '--' and may not contain spaces`)
|
||||
end
|
||||
end
|
||||
|
||||
type HelpSection = { title: string, lines: { { string } | string }? }
|
||||
local function make_help(sections: { HelpSection }, indent: number?): string
|
||||
local function align_cols(rows: { { string } }, sep: string?): { { string } }
|
||||
local sep = if sep == nil then " " else sep
|
||||
local max_col_lengths: { number } = {}
|
||||
for _, row in rows do
|
||||
for i, s in row do
|
||||
if max_col_lengths[i] == nil or s:len() > max_col_lengths[i] then
|
||||
max_col_lengths[i] = s:len()
|
||||
end
|
||||
end
|
||||
end
|
||||
local aligned_rows: { { string } } = {}
|
||||
for _, row in rows do
|
||||
local line: { string } = {}
|
||||
for i, col in row do
|
||||
table.insert(line, col)
|
||||
if i < #row then
|
||||
local spacing = sep
|
||||
local diff = max_col_lengths[i] - col:len()
|
||||
if diff > 0 then
|
||||
spacing = string.rep(" ", diff) .. sep
|
||||
end
|
||||
table.insert(line, spacing)
|
||||
end
|
||||
end
|
||||
table.insert(aligned_rows, line)
|
||||
end
|
||||
return aligned_rows
|
||||
end
|
||||
|
||||
local function append_list<T>(dest: { T }, src: { T })
|
||||
for _, v in src do
|
||||
table.insert(dest, v)
|
||||
end
|
||||
end
|
||||
|
||||
local ind = " "
|
||||
if indent ~= nil then
|
||||
ind = string.rep(" ", indent)
|
||||
end
|
||||
|
||||
local help_lines: { { string } } = {}
|
||||
|
||||
for _, s in sections do
|
||||
table.insert(help_lines, { s.title })
|
||||
if s.lines ~= nil then
|
||||
local section_lines: { { string } } = {}
|
||||
for _, l in s.lines do
|
||||
if typeof(l) == "string" then
|
||||
table.insert(section_lines, { ind, l })
|
||||
else
|
||||
table.insert(section_lines, { ind, table.unpack(l) })
|
||||
end
|
||||
end
|
||||
append_list(help_lines, align_cols(section_lines))
|
||||
end
|
||||
table.insert(help_lines, {})
|
||||
end
|
||||
|
||||
-- remove last empty line
|
||||
table.remove(help_lines)
|
||||
|
||||
local help_text = ""
|
||||
for _, line in help_lines do
|
||||
help_text ..= table.concat(line, " ") .. "\n"
|
||||
end
|
||||
return help_text
|
||||
end
|
||||
-- subcommand cli, must be first positional arg
|
||||
function M.new_subcommands(name, description: string?, helpkeys: { string }?)
|
||||
-- args that trigger print help + abort
|
||||
local helpset: { [string]: boolean } = DEFAULT_helpset
|
||||
if helpkeys ~= nil then
|
||||
helpset = {}
|
||||
for _, k in helpkeys do
|
||||
helpset[k] = true
|
||||
end
|
||||
end
|
||||
|
||||
local cli = {}
|
||||
cli.name = name
|
||||
cli.description = description
|
||||
cli._helpkeys = helpkeys
|
||||
cli._helpset = helpset
|
||||
cli._subcommands = {}
|
||||
|
||||
function cli:add_subcommand(name: string, description: string?)
|
||||
validate_subcommand_name_or_error(name)
|
||||
local sc = M.new(name, description, cli._helpkeys)
|
||||
cli._subcommands[name] = sc
|
||||
return sc
|
||||
end
|
||||
|
||||
function cli:parse(args: { string }): (SubcommandParseResult?, string?)
|
||||
if #args == 0 then
|
||||
return nil, "insufficient arguments, required at least 1, got 0"
|
||||
end
|
||||
local command = args[1]
|
||||
assert(command ~= nil)
|
||||
|
||||
-- asking for help?
|
||||
if cli._helpset[command] ~= nil then
|
||||
print(cli:help())
|
||||
process.exit(0)
|
||||
end
|
||||
|
||||
local subcommand = cli._subcommands[command]
|
||||
if subcommand == nil then
|
||||
return nil, `'{command} is not a valid subcommand`
|
||||
end
|
||||
|
||||
local sub_args = { table.unpack(args, 2) }
|
||||
local res, err = subcommand:parse(sub_args)
|
||||
if err ~= nil then
|
||||
return nil, err
|
||||
end
|
||||
assert(res ~= nil)
|
||||
|
||||
return {
|
||||
command = command,
|
||||
result = res,
|
||||
}, nil
|
||||
end
|
||||
|
||||
function cli:help(indent: number?): string
|
||||
local help_sections: { HelpSection } = {}
|
||||
|
||||
table.insert(help_sections, { title = `usage: {cli.name} <command>` })
|
||||
|
||||
if cli.description ~= nil then
|
||||
table.insert(help_sections, {
|
||||
title = "description:",
|
||||
lines = { { cli.description } },
|
||||
})
|
||||
end
|
||||
|
||||
local subcommand_lines = {}
|
||||
for _, c in cli._subcommands do
|
||||
local desc = if c.description then c.description else ""
|
||||
table.insert(subcommand_lines, { c.name, desc })
|
||||
end
|
||||
|
||||
table.insert(help_sections, {
|
||||
title = "commands:",
|
||||
lines = subcommand_lines,
|
||||
})
|
||||
|
||||
return make_help(help_sections, indent)
|
||||
end
|
||||
|
||||
return cli
|
||||
end
|
||||
|
||||
function M.new(name: string, description: string?, helpkeys: { string }?)
|
||||
-- args that trigger print help + abort
|
||||
local helpset: { [string]: boolean } = DEFAULT_helpset
|
||||
if helpkeys ~= nil then
|
||||
helpset = {}
|
||||
for _, k in helpkeys do
|
||||
helpset[k] = true
|
||||
end
|
||||
end
|
||||
|
||||
-- I'm fairly certain this is not the right pattern for making objects, but it results in single definition + great LSP so I'm happy
|
||||
local cli = {}
|
||||
cli.name = name
|
||||
cli.description = description
|
||||
cli._helpset = helpset
|
||||
cli._positionals = {} :: { ArgData }
|
||||
cli._flags = {} :: { ArgData }
|
||||
cli._options = {} :: { ArgData }
|
||||
cli._argdata_set = {} :: { [string]: ArgData }
|
||||
cli._required_list = {} :: { string }
|
||||
cli._lookup = {} :: { [string]: ArgData }
|
||||
cli._required_positional_count = 0
|
||||
cli._default_result = { values = {}, flags = {} } :: ParseResult
|
||||
|
||||
local function add_arg_lookups(keys: { string }, arg_data: ArgData)
|
||||
for _, k in keys do
|
||||
if cli._helpset[k] ~= nil then
|
||||
error(`key '{k}' is already used as a help key.`)
|
||||
end
|
||||
validate_key_or_error(k)
|
||||
if cli._lookup[k] ~= nil then
|
||||
error(`key '{k}' already exists.`)
|
||||
end
|
||||
cli._lookup[k] = arg_data
|
||||
end
|
||||
end
|
||||
|
||||
local function add_name_error_on_duplicate(name: string, data: ArgData)
|
||||
if cli._argdata_set[name] ~= nil then
|
||||
error(`arg with name {name} already exists`)
|
||||
end
|
||||
cli._argdata_set[name] = data
|
||||
end
|
||||
|
||||
local function make_safe_options(options: ArgOptions?): ArgOptionsSafe
|
||||
if options == nil then
|
||||
return DEFAULT_OPTIONS
|
||||
end
|
||||
assert(options)
|
||||
|
||||
options.aliases = if options.aliases == nil then {} else options.aliases
|
||||
|
||||
return options :: ArgOptionsSafe
|
||||
end
|
||||
|
||||
-- return is guaranteed to start with at least one '-', if none, adds '--'
|
||||
local function to_lookup_name(name: string): string
|
||||
if name:sub(1, 1) == "-" then
|
||||
return name
|
||||
end
|
||||
return "--" .. name
|
||||
end
|
||||
|
||||
-- return is guaranted to not start with '-'
|
||||
local function to_result_name(name: string): string
|
||||
while name:sub(1, 1) == "-" do
|
||||
name = name:sub(2, -1)
|
||||
end
|
||||
return name
|
||||
end
|
||||
|
||||
function cli:add_positional(name: string, options: ArgOptions?)
|
||||
if name:sub(1, 1) == "-" then
|
||||
error(`invalid arg name '{name}', positional cannot start with dashes`)
|
||||
end
|
||||
|
||||
local options = make_safe_options(options)
|
||||
|
||||
local arg_data: ArgData = {
|
||||
name = name,
|
||||
kind = "POSITIONAL",
|
||||
options = options,
|
||||
}
|
||||
add_name_error_on_duplicate(name, arg_data)
|
||||
if options.default == nil then
|
||||
-- check positional required ordering, can't have a required pos after an optional one
|
||||
local last = cli._positionals[#cli._positionals]
|
||||
if last ~= nil and last.options.default ~= nil then
|
||||
error(
|
||||
`{name} is required, but {last.name} is optional. Cannot have required positional after optional positional`
|
||||
)
|
||||
end
|
||||
|
||||
table.insert(cli._required_list, name)
|
||||
else
|
||||
cli._default_result.values[name] = options.default
|
||||
end
|
||||
table.insert(cli._positionals, arg_data)
|
||||
end
|
||||
|
||||
function cli:add_flag(name: string, options: ArgOptions?)
|
||||
if options and options.default ~= nil then
|
||||
-- these are called constants :)
|
||||
error(`flag {name} has non nil default, default value is not supported for flags`)
|
||||
end
|
||||
|
||||
local options = make_safe_options(options)
|
||||
|
||||
local lookup_name = to_lookup_name(name)
|
||||
name = to_result_name(name)
|
||||
for i, v in options.aliases do
|
||||
options.aliases[i] = to_lookup_name(v)
|
||||
end
|
||||
|
||||
local arg_data: ArgData = {
|
||||
name = name,
|
||||
kind = "FLAG",
|
||||
options = options,
|
||||
}
|
||||
add_name_error_on_duplicate(name, arg_data)
|
||||
add_arg_lookups({ lookup_name, table.unpack(options.aliases) }, arg_data)
|
||||
|
||||
if options.default ~= nil then
|
||||
error(`flag {name} has non nil default value, defaults are not supported for flag args`)
|
||||
end
|
||||
|
||||
cli._default_result.flags[name] = false
|
||||
table.insert(cli._flags, arg_data)
|
||||
end
|
||||
|
||||
function cli:add_option(name: string, options: ArgOptions?)
|
||||
local options = make_safe_options(options)
|
||||
|
||||
local lookup_name = to_lookup_name(name)
|
||||
name = to_result_name(name)
|
||||
for i, v in options.aliases do
|
||||
options.aliases[i] = to_lookup_name(v)
|
||||
end
|
||||
|
||||
local arg_data: ArgData = {
|
||||
name = name,
|
||||
kind = "OPTION",
|
||||
options = options,
|
||||
}
|
||||
add_name_error_on_duplicate(name, arg_data)
|
||||
add_arg_lookups({ lookup_name, table.unpack(options.aliases) }, arg_data)
|
||||
if options.default == nil then
|
||||
table.insert(cli._required_list, name)
|
||||
else
|
||||
if options.default == nil then
|
||||
error(`optional arg {name} must have a default value`)
|
||||
end
|
||||
assert(options.default ~= nil)
|
||||
cli._default_result.values[name] = options.default
|
||||
end
|
||||
table.insert(cli._options, arg_data)
|
||||
end
|
||||
|
||||
-- return: data, err - where data is a table, and err is a string
|
||||
-- errors early, first encountered error ends the parse
|
||||
-- if a help key is found will print help text and exit the process
|
||||
function cli:parse(args: { string }): (ParseResult?, string?)
|
||||
local parsed: ParseResult = { values = {}, flags = {}, fwd_args = {} } :: ParseResult
|
||||
|
||||
local positional_idx = 1
|
||||
local skip_next = false -- used for options
|
||||
for i, arg in args do
|
||||
-- asking for help?
|
||||
if cli._helpset[arg] ~= nil then
|
||||
print(cli:help())
|
||||
process.exit(0)
|
||||
end
|
||||
|
||||
if skip_next then
|
||||
skip_next = false
|
||||
continue
|
||||
end
|
||||
|
||||
-- rest of args should be fwd args
|
||||
if arg == "--" then
|
||||
parsed.fwd_args = { table.unpack(args, i + 1) }
|
||||
break
|
||||
end
|
||||
|
||||
local has_dash = arg:sub(1, 1) == "-"
|
||||
local arg_data: ArgData? = nil
|
||||
local value: string? = nil
|
||||
|
||||
if not has_dash then -- positional
|
||||
if positional_idx > #cli._positionals then
|
||||
return nil, `too many positional arguments, expected {#cli._positionals}.`
|
||||
end
|
||||
arg_data = cli._positionals[positional_idx]
|
||||
positional_idx += 1
|
||||
value = arg
|
||||
else -- flag or option
|
||||
arg_data = cli._lookup[arg]
|
||||
end
|
||||
|
||||
if arg_data == nil then
|
||||
return nil, `unknown flag or option: '{arg}'`
|
||||
end
|
||||
assert(arg_data ~= nil)
|
||||
|
||||
-- handle flags early
|
||||
if arg_data.kind == "FLAG" then
|
||||
parsed.flags[arg_data.name] = true
|
||||
continue
|
||||
end
|
||||
|
||||
-- get option value
|
||||
if arg_data.kind == "OPTION" then
|
||||
if i < #args then
|
||||
value = args[i + 1]
|
||||
skip_next = true
|
||||
end
|
||||
end
|
||||
|
||||
-- resolve positionals and options
|
||||
if value == nil then
|
||||
return nil, `no value provided for option '{arg}'`
|
||||
end
|
||||
assert(value ~= nil)
|
||||
|
||||
parsed.values[arg_data.name] = value
|
||||
end
|
||||
|
||||
-- check all required options are provided
|
||||
for _, name in cli._required_list do
|
||||
if parsed.values[name] == nil and parsed.flags[name] == nil then
|
||||
local kind = cli._argdata_set[name].kind:lower()
|
||||
return nil, `required {kind} arg '{name}' was not found`
|
||||
end
|
||||
end
|
||||
|
||||
-- apply defaults for missing optional
|
||||
for k, v in cli._default_result.flags do
|
||||
if parsed.flags[k] == nil then
|
||||
parsed.flags[k] = v
|
||||
end
|
||||
end
|
||||
for k, v in cli._default_result.values do
|
||||
if parsed.values[k] == nil then
|
||||
parsed.values[k] = v
|
||||
end
|
||||
end
|
||||
|
||||
return parsed, nil
|
||||
end
|
||||
|
||||
function cli:help(indent: number?): string
|
||||
local function align_cols(rows: { { string } }, sep: string?): { { string } }
|
||||
local sep = if sep == nil then " " else sep
|
||||
local max_col_lengths: { number } = {}
|
||||
for _, row in rows do
|
||||
for i, s in row do
|
||||
if max_col_lengths[i] == nil or s:len() > max_col_lengths[i] then
|
||||
max_col_lengths[i] = s:len()
|
||||
end
|
||||
end
|
||||
end
|
||||
local aligned_rows: { { string } } = {}
|
||||
for _, row in rows do
|
||||
local line: { string } = {}
|
||||
for i, col in row do
|
||||
table.insert(line, col)
|
||||
if i < #row then
|
||||
local spacing = sep
|
||||
local diff = max_col_lengths[i] - col:len()
|
||||
if diff > 0 then
|
||||
spacing = string.rep(" ", diff) .. sep
|
||||
end
|
||||
table.insert(line, spacing)
|
||||
end
|
||||
end
|
||||
table.insert(aligned_rows, line)
|
||||
end
|
||||
return aligned_rows
|
||||
end
|
||||
|
||||
local help_sections: { HelpSection } = {}
|
||||
|
||||
-- usage
|
||||
local usage = `usage: {cli.name}`
|
||||
if #cli._flags > 0 or #cli._options > 0 then
|
||||
usage ..= " [options]"
|
||||
end
|
||||
for _, arg in cli._positionals do
|
||||
if arg.options.default == nil then
|
||||
usage ..= ` <{arg.name}>`
|
||||
else
|
||||
usage ..= ` [{arg.name}]`
|
||||
end
|
||||
end
|
||||
|
||||
table.insert(help_sections, { title = usage })
|
||||
|
||||
-- description
|
||||
if cli.description ~= nil then
|
||||
table.insert(help_sections, { title = "description:", lines = { cli.description } })
|
||||
end
|
||||
|
||||
local function make_arg_line(arg: ArgData): { string }
|
||||
local keys = arg.name
|
||||
if arg.kind ~= "POSITIONAL" then
|
||||
keys = `--{arg.name}`
|
||||
for _, a in arg.options.aliases do
|
||||
keys ..= `, {a}`
|
||||
end
|
||||
end
|
||||
|
||||
local reqdef = ""
|
||||
-- optional & default
|
||||
if arg.kind ~= "FLAG" then
|
||||
if arg.options.default == nil then
|
||||
reqdef ..= "[required]"
|
||||
else
|
||||
assert(arg.options.default)
|
||||
reqdef ..= `[default: '{arg.options.default}']`
|
||||
end
|
||||
end
|
||||
|
||||
local help = ""
|
||||
if arg.options.help then
|
||||
if help ~= "" then
|
||||
help ..= " "
|
||||
end
|
||||
help ..= arg.options.help
|
||||
end
|
||||
|
||||
return { keys, reqdef, help }
|
||||
end
|
||||
|
||||
-- positionals
|
||||
if #cli._positionals > 0 then
|
||||
local positional_lines: { { string } } = {}
|
||||
for _, arg in cli._positionals do
|
||||
table.insert(positional_lines, make_arg_line(arg))
|
||||
end
|
||||
table.insert(help_sections, { title = "positional arguments:", lines = positional_lines })
|
||||
end
|
||||
|
||||
-- flags
|
||||
if #cli._flags > 0 then
|
||||
local flag_lines: { { string } } = {}
|
||||
for _, arg in cli._flags do
|
||||
table.insert(flag_lines, make_arg_line(arg))
|
||||
end
|
||||
table.insert(help_sections, { title = "flags:", lines = flag_lines })
|
||||
end
|
||||
|
||||
-- options
|
||||
if #cli._options > 0 then
|
||||
local option_lines: { { string } } = {}
|
||||
for _, arg in cli._options do
|
||||
table.insert(option_lines, make_arg_line(arg))
|
||||
end
|
||||
table.insert(help_sections, { title = "options:", lines = option_lines })
|
||||
end
|
||||
|
||||
return make_help(help_sections, indent)
|
||||
end
|
||||
|
||||
return cli
|
||||
end
|
||||
|
||||
export type Cli = typeof(M.new(""))
|
||||
export type CliSubcommands = typeof(M.new_subcommands(""))
|
||||
|
||||
return M
|
135
lune/pkg/progress.luau
Normal file
135
lune/pkg/progress.luau
Normal file
|
@ -0,0 +1,135 @@
|
|||
--!strict
|
||||
--> Inspired by Rokit's progress bar: https://github.com/rojo-rbx/rokit/blob/a303faf/src/util/progress.rs
|
||||
-- Original: https://github.com/pesde-pkg/tooling/blob/main/toolchainlib/src/utils/progress.luau
|
||||
--[[
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 pesde-pkg
|
||||
|
||||
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:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
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.
|
||||
]]
|
||||
local stdio = require("@lune/stdio")
|
||||
local task = require("@lune/task")
|
||||
|
||||
local result = require("./result")
|
||||
|
||||
-- FORMAT: {SPINNER} {MESSAGE} {BAR} {STAGE}
|
||||
local SPINNERS = { "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏" }
|
||||
local BAR_COMPONENT = "▇"
|
||||
local MAX_BAR_LENGTH = 30
|
||||
|
||||
local ProgressBar = {}
|
||||
type ProgressBar = {
|
||||
stages: { { tag: string, message: string } },
|
||||
current_stage_index: number,
|
||||
finished: boolean,
|
||||
paused: boolean,
|
||||
thread: thread?,
|
||||
}
|
||||
export type ProgressBarImpl = typeof(setmetatable({} :: ProgressBar, { __index = ProgressBar }))
|
||||
|
||||
function ProgressBar.new(): ProgressBarImpl
|
||||
return setmetatable(
|
||||
{
|
||||
stages = {},
|
||||
current_stage_index = 1,
|
||||
finished = false,
|
||||
} :: ProgressBar,
|
||||
{
|
||||
__index = ProgressBar,
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
function ProgressBar.with_stage(self: ProgressBarImpl, tag: string, msg: string): ProgressBarImpl
|
||||
table.insert(self.stages, { tag = tag, message = msg })
|
||||
return self
|
||||
end
|
||||
|
||||
function ProgressBar.start(self: ProgressBarImpl)
|
||||
local BAR_LENGTH = MAX_BAR_LENGTH // #self.stages
|
||||
local TOTAL_BAR_LENGTH = BAR_LENGTH * #self.stages
|
||||
local BAR = string.rep(BAR_COMPONENT, BAR_LENGTH)
|
||||
local MAX_MESSAGE_LENGTH = 0
|
||||
for _, stage in self.stages do
|
||||
local len = #stage.message
|
||||
if len > MAX_MESSAGE_LENGTH then
|
||||
MAX_MESSAGE_LENGTH = len
|
||||
end
|
||||
end
|
||||
|
||||
self.thread = task.spawn(function()
|
||||
while true do
|
||||
if self.paused then
|
||||
task.wait(0.1)
|
||||
continue
|
||||
end
|
||||
|
||||
for _, spinner in SPINNERS do
|
||||
if self.finished then
|
||||
return
|
||||
end
|
||||
|
||||
local stage = self.stages[self.current_stage_index]
|
||||
stdio.ewrite(
|
||||
`\x1b[2K\x1b[0G{stdio.color("cyan")}{spinner} {stage.message}{stdio.color("reset")}{string.rep(
|
||||
" ",
|
||||
MAX_MESSAGE_LENGTH - #stage.message
|
||||
)} [{stdio.style("dim")}{string.rep(BAR, self.current_stage_index)}{string.rep(
|
||||
" ",
|
||||
TOTAL_BAR_LENGTH - (BAR_LENGTH * self.current_stage_index)
|
||||
)}{stdio.style("reset")}] {stdio.style("bold")}{self.current_stage_index} / {#self.stages}{stdio.style(
|
||||
"reset"
|
||||
)}`
|
||||
)
|
||||
|
||||
task.wait(0.1)
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function ProgressBar.stop(self: ProgressBarImpl)
|
||||
-- Trigger upvalue, kill thread and clean progress bar remnant
|
||||
self.finished = true
|
||||
stdio.ewrite("\x1b[2K\x1b[0G")
|
||||
end
|
||||
|
||||
function ProgressBar.pause(self: ProgressBarImpl)
|
||||
self.paused = true
|
||||
stdio.ewrite("\x1b[2K\x1b[0G")
|
||||
end
|
||||
|
||||
function ProgressBar.unpause(self: ProgressBarImpl)
|
||||
self.paused = false
|
||||
end
|
||||
|
||||
function ProgressBar.next_stage(self: ProgressBarImpl): result.Identity<nil>
|
||||
local inc = self.current_stage_index + 1
|
||||
if inc > #self.stages then
|
||||
-- TODO: Make this a result
|
||||
self.finished = true
|
||||
return result(false, "OutOfBounds - Attempted to advance past last stage")
|
||||
end
|
||||
|
||||
self.current_stage_index = inc
|
||||
return result(true, nil)
|
||||
end
|
||||
|
||||
return ProgressBar
|
24
lune/pkg/result.luau
Normal file
24
lune/pkg/result.luau
Normal file
|
@ -0,0 +1,24 @@
|
|||
--!strict
|
||||
export type Identity<T> = {
|
||||
ok: true,
|
||||
val: T,
|
||||
} | {
|
||||
ok: false,
|
||||
err: string,
|
||||
}
|
||||
|
||||
local function construct<T>(ok: boolean, value: T & string): Identity<T>
|
||||
if ok then
|
||||
return {
|
||||
ok = true,
|
||||
val = value,
|
||||
}
|
||||
else
|
||||
return {
|
||||
ok = false,
|
||||
err = value,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
return (construct :: any) :: (<T>(ok: true, value: T) -> Identity<T>) & (<T>(ok: false, value: string) -> Identity<T>)
|
13
src/CSSharpTemplate.cs
Normal file
13
src/CSSharpTemplate.cs
Normal file
|
@ -0,0 +1,13 @@
|
|||
using CounterStrikeSharp.API.Core;
|
||||
|
||||
namespace SimleAdmin;
|
||||
public class CSSharpTemplate : BasePlugin
|
||||
{
|
||||
public override string ModuleName => "CSSharpTemplate";
|
||||
public override string ModuleVersion => "0.1.0";
|
||||
|
||||
public override void Load(bool hotReload)
|
||||
{
|
||||
Console.WriteLine("Hello World!");
|
||||
}
|
||||
}
|
13
src/CSSharpTemplate.csproj
Normal file
13
src/CSSharpTemplate.csproj
Normal file
|
@ -0,0 +1,13 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CounterStrikeSharp.API" Version="1.0.316" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
10
stylua.toml
Normal file
10
stylua.toml
Normal file
|
@ -0,0 +1,10 @@
|
|||
column_width = 120
|
||||
line_endings = "Unix"
|
||||
indent_type = "Spaces"
|
||||
indent_width = 4
|
||||
quote_style = "AutoPreferDouble"
|
||||
call_parentheses = "Input"
|
||||
collapse_simple_statement = "Never"
|
||||
|
||||
[sort_requires]
|
||||
enabled = true
|
Loading…
Reference in a new issue