Extract common interfaces out of runtime-specific adapters
This commit is contained in:
parent
8155f4dc73
commit
9cca55b101
@ -1,5 +1,4 @@
|
|||||||
{
|
{
|
||||||
"exclude": ["src/bun/**/*.ts"],
|
|
||||||
"lint": {
|
"lint": {
|
||||||
"include": ["src/"],
|
"include": ["src/"],
|
||||||
"rules": {
|
"rules": {
|
||||||
|
16
justfile
16
justfile
@ -3,12 +3,7 @@ default:
|
|||||||
@just --list
|
@just --list
|
||||||
|
|
||||||
# Typescript checking
|
# Typescript checking
|
||||||
check: lint
|
check: deno-check bun-check
|
||||||
deno check --unstable --all -c deno.jsonc ./src/deno/*.ts ./src/common/*.ts
|
|
||||||
|
|
||||||
# Code linting
|
|
||||||
lint:
|
|
||||||
deno lint
|
|
||||||
|
|
||||||
# Reformat the code
|
# Reformat the code
|
||||||
fmt:
|
fmt:
|
||||||
@ -27,6 +22,10 @@ clean:
|
|||||||
# Bun-specific commands
|
# Bun-specific commands
|
||||||
########################################################################################################################
|
########################################################################################################################
|
||||||
|
|
||||||
|
# Check code with actual Typescript compiler
|
||||||
|
bun-check:
|
||||||
|
bunx tsc
|
||||||
|
|
||||||
# Test with bun
|
# Test with bun
|
||||||
bun-test:
|
bun-test:
|
||||||
bun test --coverage
|
bun test --coverage
|
||||||
@ -39,6 +38,11 @@ bun-run:
|
|||||||
# Deno-specific commands
|
# Deno-specific commands
|
||||||
########################################################################################################################
|
########################################################################################################################
|
||||||
|
|
||||||
|
# Lint code and check types
|
||||||
|
deno-check:
|
||||||
|
deno lint
|
||||||
|
deno check --unstable --all -c deno.jsonc ./src/deno/*.ts ./src/common/*.ts
|
||||||
|
|
||||||
# Test with deno
|
# Test with deno
|
||||||
deno-test:
|
deno-test:
|
||||||
deno test --allow-all
|
deno test --allow-all
|
||||||
|
6
package.json
Normal file
6
package.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"dependencies": {},
|
||||||
|
"devDependencies": {
|
||||||
|
"bun-types": "^1.0.11"
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,7 @@
|
|||||||
* This is all the nasty ffi setup for the bun runtime
|
* This is all the nasty ffi setup for the bun runtime
|
||||||
*/
|
*/
|
||||||
import { dlopen, ptr, suffix } from 'bun:ffi';
|
import { dlopen, ptr, suffix } from 'bun:ffi';
|
||||||
|
import { IFFI } from '../common/types.ts';
|
||||||
|
|
||||||
const getLib = (name: string) => {
|
const getLib = (name: string) => {
|
||||||
return dlopen(
|
return dlopen(
|
||||||
@ -23,7 +24,7 @@ const getLib = (name: string) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
let cStdLib = { symbols: {} };
|
let cStdLib: any = { symbols: {} };
|
||||||
|
|
||||||
try {
|
try {
|
||||||
cStdLib = getLib(`libc.${suffix}`);
|
cStdLib = getLib(`libc.${suffix}`);
|
||||||
@ -35,5 +36,12 @@ try {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const { tcgetattr, tcsetattr, cfmakeraw } = cStdLib.symbols;
|
const { tcgetattr, tcsetattr, cfmakeraw } = cStdLib.symbols;
|
||||||
export const getPointer = ptr;
|
const BunFFI: IFFI = {
|
||||||
|
tcgetattr,
|
||||||
|
tcsetattr,
|
||||||
|
cfmakeraw,
|
||||||
|
getPointer: ptr,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BunFFI;
|
||||||
|
@ -2,16 +2,8 @@
|
|||||||
* The main entrypoint when using Bun as the runtime
|
* The main entrypoint when using Bun as the runtime
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { getTermios } from "../common/mod.ts";
|
|
||||||
export * from './terminal_io.ts';
|
export * from './terminal_io.ts';
|
||||||
|
|
||||||
export async function init() {
|
export const onExit = (cb: () => void): void => {
|
||||||
const t = await getTermios();
|
process.on('beforeExit', cb);
|
||||||
|
};
|
||||||
t.enableRawMode();
|
|
||||||
|
|
||||||
process.on('exit', () => {
|
|
||||||
console.info('Disabling raw mode');
|
|
||||||
t.disableRawMode();
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* Wrap the runtime-specific hook into stdin
|
* Wrap the runtime-specific hook into stdin
|
||||||
*/
|
*/
|
||||||
|
import { ITerminalIO } from '../common/mod.ts';
|
||||||
|
|
||||||
export async function* inputLoop() {
|
export async function* inputLoop() {
|
||||||
for await (const chunk of Bun.stdin.stream()) {
|
for await (const chunk of Bun.stdin.stream()) {
|
||||||
yield chunk;
|
yield chunk;
|
||||||
@ -12,3 +14,10 @@ export async function write(s: string): Promise<void> {
|
|||||||
|
|
||||||
await Bun.write(Bun.stdout, buffer);
|
await Bun.write(Bun.stdout, buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const BunTerminalIO: ITerminalIO = {
|
||||||
|
inputLoop,
|
||||||
|
write,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BunTerminalIO;
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* Adapt the bun test interface to the shared testing interface
|
* Adapt the bun test interface to the shared testing interface
|
||||||
*/
|
*/
|
||||||
import {test as btest, expect } from 'bun:test';
|
import { expect, test as btest } from 'bun:test';
|
||||||
import {ITestBase} from "../common/mod";
|
import { ITestBase } from '../common/mod';
|
||||||
|
|
||||||
class TestBase implements ITestBase {
|
class TestBase implements ITestBase {
|
||||||
test(name: string, fn: () => void) {
|
test(name: string, fn: () => void) {
|
||||||
@ -32,6 +32,10 @@ class TestBase implements ITestBase {
|
|||||||
assertTrue(actual: boolean): void {
|
assertTrue(actual: boolean): void {
|
||||||
return expect(actual).toBe(true);
|
return expect(actual).toBe(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assertStrictEquals(actual: unknown, expected: unknown): void {
|
||||||
|
return expect(actual).toBe(expected);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const testBase = new TestBase();
|
const testBase = new TestBase();
|
||||||
|
@ -1,18 +1,23 @@
|
|||||||
import { importForRuntime } from './runtime.ts';
|
import { importForRuntime } from './runtime.ts';
|
||||||
import { Editor } from './editor.ts';
|
import { Editor } from './editor.ts';
|
||||||
|
import { getTermios } from './termios.ts';
|
||||||
|
|
||||||
export * from './runtime.ts';
|
export * from './runtime.ts';
|
||||||
export * from './strings.ts';
|
export * from './strings.ts';
|
||||||
export * from './termios.ts';
|
export type * from './types.ts';
|
||||||
export type { ITestBase } from './test_base.ts';
|
|
||||||
|
|
||||||
const decoder = new TextDecoder();
|
const decoder = new TextDecoder();
|
||||||
|
|
||||||
export async function main() {
|
export async function main() {
|
||||||
const { inputLoop, init } = await importForRuntime('mod.ts');
|
const { inputLoop, onExit } = await importForRuntime('mod.ts');
|
||||||
|
|
||||||
// Set up handlers to enable/disable raw mode for each runtime
|
// Setup raw mode, and tear down on error or normal exit
|
||||||
await init();
|
const t = await getTermios();
|
||||||
|
t.enableRawMode();
|
||||||
|
onExit(() => {
|
||||||
|
console.info('Exit handler called, disabling raw mode');
|
||||||
|
t.disableRawMode();
|
||||||
|
});
|
||||||
|
|
||||||
// Create the editor itself
|
// Create the editor itself
|
||||||
const editor = new Editor();
|
const editor = new Editor();
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { chars, importDefaultForRuntime, is_ascii, ITestBase } from './mod.ts';
|
import { importDefaultForRuntime, ITestBase } from './mod.ts';
|
||||||
|
import { chars, is_ascii } from './strings.ts';
|
||||||
|
|
||||||
const t: ITestBase = await importDefaultForRuntime('test_base');
|
const t: ITestBase = await importDefaultForRuntime('test_base');
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { die, importForRuntime } from './mod.ts';
|
import { die, IFFI, importDefaultForRuntime } from './mod.ts';
|
||||||
|
|
||||||
export const STDIN_FILENO = 0;
|
export const STDIN_FILENO = 0;
|
||||||
export const STOUT_FILENO = 1;
|
export const STOUT_FILENO = 1;
|
||||||
@ -8,40 +8,15 @@ export const TCSAFLUSH = 2;
|
|||||||
export const TERMIOS_SIZE = 60;
|
export const TERMIOS_SIZE = 60;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Common interface for setting Termios properties
|
|
||||||
*/
|
|
||||||
export interface ITermios {
|
|
||||||
/**
|
|
||||||
* Are we currently in raw mode?
|
|
||||||
*/
|
|
||||||
inRawMode: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggles on raw mode
|
|
||||||
*/
|
|
||||||
enableRawMode(): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Restores canonical mode
|
|
||||||
*/
|
|
||||||
disableRawMode(): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
let termiosSingleton: ITermios | null = null;
|
|
||||||
|
|
||||||
export const getTermios = async () => {
|
|
||||||
if (termiosSingleton !== null) {
|
|
||||||
return termiosSingleton;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the runtime-specific ffi wrappers
|
|
||||||
const { tcgetattr, tcsetattr, cfmakeraw, getPointer } =
|
|
||||||
await importForRuntime('ffi');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implementation to toggle raw mode
|
* Implementation to toggle raw mode
|
||||||
*/
|
*/
|
||||||
class Termios implements ITermios {
|
class Termios {
|
||||||
|
/**
|
||||||
|
* The ffi implementation for the current runtime
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
#ffi: IFFI;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Are we in raw mode?
|
* Are we in raw mode?
|
||||||
* @private
|
* @private
|
||||||
@ -66,7 +41,8 @@ export const getTermios = async () => {
|
|||||||
*/
|
*/
|
||||||
readonly #ptr: any;
|
readonly #ptr: any;
|
||||||
|
|
||||||
constructor() {
|
constructor(ffi: IFFI) {
|
||||||
|
this.#ffi = ffi;
|
||||||
this.#inRawMode = false;
|
this.#inRawMode = false;
|
||||||
|
|
||||||
// These are the TypedArrays linked to the raw pointer data
|
// These are the TypedArrays linked to the raw pointer data
|
||||||
@ -74,7 +50,7 @@ export const getTermios = async () => {
|
|||||||
this.#termios = new Uint8Array(TERMIOS_SIZE);
|
this.#termios = new Uint8Array(TERMIOS_SIZE);
|
||||||
|
|
||||||
// The current pointer for C
|
// The current pointer for C
|
||||||
this.#ptr = getPointer(this.#termios);
|
this.#ptr = ffi.getPointer(this.#termios);
|
||||||
}
|
}
|
||||||
|
|
||||||
get inRawMode() {
|
get inRawMode() {
|
||||||
@ -87,7 +63,7 @@ export const getTermios = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get the current termios settings
|
// Get the current termios settings
|
||||||
let res = tcgetattr(STDIN_FILENO, this.#ptr);
|
let res = this.#ffi.tcgetattr(STDIN_FILENO, this.#ptr);
|
||||||
if (res === -1) {
|
if (res === -1) {
|
||||||
die('Failed to get terminal settings');
|
die('Failed to get terminal settings');
|
||||||
}
|
}
|
||||||
@ -95,18 +71,16 @@ export const getTermios = async () => {
|
|||||||
// The #ptr property is pointing to the #termios TypedArray. As the pointer
|
// The #ptr property is pointing to the #termios TypedArray. As the pointer
|
||||||
// is manipulated, the TypedArray is as well. We will use this to save
|
// is manipulated, the TypedArray is as well. We will use this to save
|
||||||
// the original canonical/cooked terminal settings for disabling raw mode later.
|
// the original canonical/cooked terminal settings for disabling raw mode later.
|
||||||
|
// @ts-ignore: bad type definition
|
||||||
this.#cookedTermios = new Uint8Array(this.#termios, 0, 60);
|
this.#cookedTermios = new Uint8Array(this.#termios, 0, 60);
|
||||||
|
|
||||||
// Update termios struct with (most of the) raw settings
|
// Update termios struct with (most of the) raw settings
|
||||||
res = cfmakeraw(this.#ptr);
|
this.#ffi.cfmakeraw(this.#ptr);
|
||||||
if (res === -1) {
|
|
||||||
die('Failed to call cfmakeraw');
|
|
||||||
}
|
|
||||||
|
|
||||||
// @TODO: Tweak a few more terminal settings
|
// @TODO: Tweak a few more terminal settings
|
||||||
|
|
||||||
// Actually set the new termios settings
|
// Actually set the new termios settings
|
||||||
res = tcsetattr(STDIN_FILENO, TCSANOW, this.#ptr);
|
res = this.#ffi.tcsetattr(STDIN_FILENO, TCSANOW, this.#ptr);
|
||||||
if (res === -1) {
|
if (res === -1) {
|
||||||
die('Failed to update terminal settings. Can\'t enter raw mode');
|
die('Failed to update terminal settings. Can\'t enter raw mode');
|
||||||
}
|
}
|
||||||
@ -121,17 +95,26 @@ export const getTermios = async () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const oldTermiosPtr = getPointer(this.#cookedTermios);
|
const oldTermiosPtr = this.#ffi.getPointer(this.#cookedTermios);
|
||||||
const res = tcsetattr(STDIN_FILENO, TCSANOW, oldTermiosPtr);
|
const res = this.#ffi.tcsetattr(STDIN_FILENO, TCSANOW, oldTermiosPtr);
|
||||||
if (res === -1) {
|
if (res === -1) {
|
||||||
die('Failed to restore canonical mode.');
|
die('Failed to restore canonical mode.');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#inRawMode = false;
|
this.#inRawMode = false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let termiosSingleton: Termios | null = null;
|
||||||
|
|
||||||
|
export const getTermios = async () => {
|
||||||
|
if (termiosSingleton !== null) {
|
||||||
|
return termiosSingleton;
|
||||||
}
|
}
|
||||||
|
|
||||||
termiosSingleton = new Termios();
|
// Get the runtime-specific ffi wrappers
|
||||||
|
const ffi: IFFI = await importDefaultForRuntime('ffi');
|
||||||
|
termiosSingleton = new Termios(ffi);
|
||||||
|
|
||||||
return termiosSingleton;
|
return termiosSingleton;
|
||||||
};
|
};
|
||||||
|
@ -1,13 +0,0 @@
|
|||||||
/**
|
|
||||||
* The shared test interface, so tests can be run by both runtimes
|
|
||||||
*/
|
|
||||||
export interface ITestBase {
|
|
||||||
test(name: string, fn: () => void): void;
|
|
||||||
assertEquals(actual: unknown, expected: unknown): void;
|
|
||||||
assertNotEquals(actual: unknown, expected: unknown): void;
|
|
||||||
assertStrictEquals(actual: unknown, expected: unknown): void;
|
|
||||||
assertExists(actual: unknown): void;
|
|
||||||
assertInstanceOf(actual: unknown, expectedType: any): void;
|
|
||||||
assertTrue(actual: boolean): void;
|
|
||||||
assertFalse(actual: boolean): void;
|
|
||||||
}
|
|
61
src/common/types.ts
Normal file
61
src/common/types.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
/**
|
||||||
|
* The shared test interface, so tests can be run by both runtimes
|
||||||
|
*/
|
||||||
|
export interface ITestBase {
|
||||||
|
test(name: string, fn: () => void): void;
|
||||||
|
assertEquals(actual: unknown, expected: unknown): void;
|
||||||
|
assertNotEquals(actual: unknown, expected: unknown): void;
|
||||||
|
assertStrictEquals(actual: unknown, expected: unknown): void;
|
||||||
|
assertExists(actual: unknown): void;
|
||||||
|
assertInstanceOf(actual: unknown, expectedType: any): void;
|
||||||
|
assertTrue(actual: boolean): void;
|
||||||
|
assertFalse(actual: boolean): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The native functions for terminal settings
|
||||||
|
*/
|
||||||
|
export interface IFFI {
|
||||||
|
/**
|
||||||
|
* Get the existing termios settings (for canonical mode)
|
||||||
|
*/
|
||||||
|
tcgetattr(fd: number, termiosPtr: unknown): number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the termios settings
|
||||||
|
*/
|
||||||
|
tcsetattr(fd: number, act: number, termiosPtr: unknown): number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the termios pointer with raw mode settings
|
||||||
|
*/
|
||||||
|
cfmakeraw(termiosPtr: unknown): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a TypedArray to an opaque pointer for ffi calls
|
||||||
|
*/
|
||||||
|
getPointer(ta: any): unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runtime-specific IO streams
|
||||||
|
*/
|
||||||
|
export interface ITerminalIO {
|
||||||
|
/**
|
||||||
|
* The generator function returning chunks of input from the stdin stream
|
||||||
|
*/
|
||||||
|
inputLoop(): AsyncGenerator<Uint8Array, void, unknown>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pipe a string to stdout
|
||||||
|
*/
|
||||||
|
write(s: string): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IRuntime {
|
||||||
|
/**
|
||||||
|
* Set a beforeExit/beforeUnload event handler for the runtime
|
||||||
|
* @param cb - The event handler
|
||||||
|
*/
|
||||||
|
onExit(cb: () => void): void;
|
||||||
|
}
|
@ -1,8 +1,5 @@
|
|||||||
// Deno-specific ffi code
|
// Deno-specific ffi code
|
||||||
|
import { IFFI } from '../common/types.ts';
|
||||||
// Determine library extension based on
|
|
||||||
// your OS.
|
|
||||||
// import { termiosStruct } from "../common/termios.ts";
|
|
||||||
|
|
||||||
let libSuffix = '';
|
let libSuffix = '';
|
||||||
switch (Deno.build.os) {
|
switch (Deno.build.os) {
|
||||||
@ -36,6 +33,12 @@ const cStdLib = Deno.dlopen(
|
|||||||
} as const,
|
} as const,
|
||||||
);
|
);
|
||||||
|
|
||||||
export const { tcgetattr, tcsetattr, cfmakeraw } = cStdLib.symbols;
|
const { tcgetattr, tcsetattr, cfmakeraw } = cStdLib.symbols;
|
||||||
|
const DenoFFI: IFFI = {
|
||||||
|
tcgetattr,
|
||||||
|
tcsetattr,
|
||||||
|
cfmakeraw,
|
||||||
|
getPointer: Deno.UnsafePointer.of,
|
||||||
|
};
|
||||||
|
|
||||||
export const getPointer = Deno.UnsafePointer.of;
|
export default DenoFFI;
|
||||||
|
@ -1,17 +1,16 @@
|
|||||||
/**
|
/**
|
||||||
* The main entrypoint when using Deno as the runtime
|
* The main entrypoint when using Deno as the runtime
|
||||||
*/
|
*/
|
||||||
import { getTermios } from '../common/mod.ts';
|
import { IRuntime } from '../common/types.ts';
|
||||||
|
|
||||||
export * from './terminal_io.ts';
|
export * from './terminal_io.ts';
|
||||||
|
|
||||||
export async function init() {
|
export const onExit = (cb: () => void): void => {
|
||||||
const t = await getTermios();
|
globalThis.addEventListener('onbeforeunload', cb);
|
||||||
|
};
|
||||||
|
|
||||||
t.enableRawMode();
|
const DenoRuntime: IRuntime = {
|
||||||
|
onExit,
|
||||||
|
};
|
||||||
|
|
||||||
globalThis.onbeforeunload = (): void => {
|
export default DenoRuntime;
|
||||||
console.info('Disabling raw mode');
|
|
||||||
t.disableRawMode();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { ITerminalIO } from '../common/types.ts';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wrap the runtime-specific hook into stdin
|
* Wrap the runtime-specific hook into stdin
|
||||||
*/
|
*/
|
||||||
@ -14,3 +16,10 @@ export async function write(s: string): Promise<void> {
|
|||||||
await stdout.write(buffer);
|
await stdout.write(buffer);
|
||||||
stdout.releaseLock();
|
stdout.releaseLock();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DenoTerminalIO: ITerminalIO = {
|
||||||
|
inputLoop,
|
||||||
|
write,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DenoTerminalIO;
|
||||||
|
@ -47,5 +47,5 @@ class TestBase implements ITestBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const testBase = new TestBase();
|
const testBase = new TestBase();
|
||||||
export default testBase;
|
export default testBase;
|
||||||
|
19
tsconfig.json
Normal file
19
tsconfig.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"target": "esnext",
|
||||||
|
"types": ["bun-types"],
|
||||||
|
"lib": ["ESNext"],
|
||||||
|
"noEmit": true,
|
||||||
|
"strict": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"composite": true,
|
||||||
|
"downlevelIteration": true,
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"exclude": ["src/deno"]
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user