* rewrite: initial commit

* feat: add constants for live share

* feat: add first letter uppercase

#88

* chore: remove automatic reconnects

* chore: add troubleshooting to readme
This commit is contained in:
Crawl 2018-11-07 03:53:18 +01:00 committed by GitHub
parent 916c596478
commit 0195cde103
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 2073 additions and 558 deletions

2
.gitignore vendored
View file

@ -18,5 +18,5 @@ test/
!.vscode/tasks.json !.vscode/tasks.json
.vscode-test/ .vscode-test/
dist/ dist/
out/ typings/
package-lock.json package-lock.json

5
.vscode/launch.json vendored
View file

@ -1,4 +1,3 @@
// A launch configuration that compiles the extension and then opens it inside a new window
{ {
"version": "0.5.0", "version": "0.5.0",
"configurations": [ "configurations": [
@ -10,8 +9,8 @@
"args": ["--extensionDevelopmentPath=${workspaceRoot}" ], "args": ["--extensionDevelopmentPath=${workspaceRoot}" ],
"stopOnEntry": false, "stopOnEntry": false,
"sourceMaps": true, "sourceMaps": true,
"outFiles": [ "${workspaceRoot}/out/src/**/*.js" ], "outFiles": [ "${workspaceRoot}/dist/**/*.js" ],
"preLaunchTask": "npm" "preLaunchTask": "build"
} }
] ]
} }

41
.vscode/tasks.json vendored
View file

@ -1,30 +1,15 @@
// Available variables which can be used inside of strings.
// ${workspaceRoot}: the root folder of the team
// ${file}: the current opened file
// ${fileBasename}: the current opened file's basename
// ${fileDirname}: the current opened file's dirname
// ${fileExtname}: the current opened file's extension
// ${cwd}: the current working directory of the spawned process
// A task runner that calls a custom npm script that compiles the extension.
{ {
"version": "0.1.0", // See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
// we want to run npm "version": "2.0.0",
"command": "npm", "tasks": [
{
// the command is a shell script "label": "build",
"isShellCommand": true, "type": "gulp",
"task": "build",
// show the output window only if unrecognized errors occur. "problemMatcher": [
"showOutput": "silent", "$gulp-tsc"
]
// we run the custom script "compile" as defined in package.json }
"args": ["run", "compile", "--loglevel", "silent"], ]
// The tsc compiler is started in watching mode
"isBackground": true,
// use the standard tsc in watch mode problem matcher to find compile problems in the output.
"problemMatcher": "$tsc-watch"
} }

View file

@ -24,10 +24,11 @@
* Debug mode detection * Debug mode detection
* Easily manually reconnect to discord * Easily manually reconnect to discord
## The rich presence won't show after sleep / connection issues! ## Troubleshooting
It will only attempt to reconnect 20 times. ### Can't connect to Discord? Check those:
After it hit that threshold you will have to manually click the `Reconnect to Discord` button in the bottom left of the window. https://github.com/iCrawl/discord-vscode/issues/77#issuecomment-435622205
https://github.com/iCrawl/discord-vscode/issues/85#issuecomment-417895483
## Contributing ## Contributing

27
gulpfile.js Normal file
View file

@ -0,0 +1,27 @@
const gulp = require('gulp');
const fsn = require('fs-nextra');
const ts = require('gulp-typescript');
const sourcemaps = require('gulp-sourcemaps');
const merge = require('merge2');
const path = require('path');
const project = ts.createProject('tsconfig.json');
async function build() {
await Promise.all([
fsn.emptydir('dist'),
fsn.emptydir('typings')
]);
const result = project.src()
.pipe(sourcemaps.init())
.pipe(project());
await fsn.copy(path.join(__dirname, 'src', 'data'), path.join(__dirname, 'dist', 'data'))
return merge([
result.dts.pipe(gulp.dest('typings')),
result.js.pipe(sourcemaps.write('.', { sourceRoot: '../src' })).pipe(gulp.dest('dist'))
]);
}
gulp.task('default', build);
gulp.task('build', build);

View file

@ -1,7 +1,7 @@
{ {
"name": "discord-vscode", "name": "discord-vscode",
"displayName": "Discord Presence", "displayName": "Discord Presence",
"version": "2.12.1", "version": "3.0.0",
"description": "Update your discord status with the newly added rich presence.", "description": "Update your discord status with the newly added rich presence.",
"private": true, "private": true,
"author": { "author": {
@ -16,12 +16,10 @@
], ],
"publisher": "icrawl", "publisher": "icrawl",
"license": "MIT", "license": "MIT",
"main": "./out/src/extension", "main": "./dist/extension",
"scripts": { "scripts": {
"compile": "tsc -p ./", "build": "gulp",
"postinstall": "node ./node_modules/vscode/bin/install", "postinstall": "node ./node_modules/vscode/bin/install"
"test": "node ./node_modules/vscode/bin/test",
"lint": "tslint -p tsconfig.json -c tslint.json 'src/**/*.ts'"
}, },
"activationEvents": [ "activationEvents": [
"*" "*"
@ -54,11 +52,6 @@
"default": true, "default": true,
"description": "Controls if the Discord Presence should show across all workspaces" "description": "Controls if the Discord Presence should show across all workspaces"
}, },
"discord.reconnectThreshold": {
"type": "number",
"default": 20,
"description": "Decides how many reconnect attempts should be made before stopping"
},
"discord.detailsEditing": { "discord.detailsEditing": {
"type": "string", "type": "string",
"default": "Editing {filename}", "default": "Editing {filename}",
@ -152,6 +145,11 @@
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^10.12.2", "@types/node": "^10.12.2",
"fs-nextra": "^0.3.7",
"gulp": "^3.9.1",
"gulp-sourcemaps": "^2.6.4",
"gulp-typescript": "^5.0.0-alpha.3",
"merge2": "^1.2.3",
"tslint": "^5.11.0", "tslint": "^5.11.0",
"typescript": "^3.1.6", "typescript": "^3.1.6",
"vscode": "^1.1.21" "vscode": "^1.1.21"

42
src/client/RPCClient.ts Normal file
View file

@ -0,0 +1,42 @@
const { Client } = require('discord-rpc');
import {
Disposable,
workspace
} from 'vscode';
import Acivity from '../structures/Activity';
import Logger from '../structures/Logger';
export default class RPCClient implements Disposable {
private _rpc: any = new Client({ transport: 'ipc' });
private _activity = new Acivity();
private _config = workspace.getConfiguration('discord');
private _clientId: string;
public constructor(clientId: string) {
this._clientId = clientId;
}
public get client() {
return this._rpc;
}
public setActivity(workspaceElapsedTime: boolean = false) {
if (!this._rpc) return;
const activity = this._activity.generate(workspaceElapsedTime);
Logger.log('Sending activity to Discord.');
this._rpc.setActivity(activity);
}
public async login() {
Logger.log('Logging into RPC.');
return this._rpc.login({ clientId: this._clientId });
}
public async dispose() {
this._activity.dispose();
await this._rpc.destroy();
}
}

8
src/constants.ts Normal file
View file

@ -0,0 +1,8 @@
export const LIVE_SHARE_BASE_URL = 'insiders.liveshare.vsengsaas.visualstudio.com';
export const VSLS_EXTENSION_ID = 'ms-vsliveshare.vsliveshare';
export const LIVE_SHARE_COMMANDS = {
START: 'liveshare.start',
END: 'liveshare.end',
JOIN: 'liveshare.join'
}

View file

@ -1,401 +1,93 @@
// Import the required functions & object types from various packages. import RPCClient from './client/RPCClient';
import { Client } from 'discord-rpc'; import Logger from './structures/Logger';
import { basename, extname, parse, sep } from 'path';
import { setInterval, clearInterval } from 'timers';
import { import {
commands, commands,
debug,
env,
ExtensionContext, ExtensionContext,
StatusBarItem, StatusBarItem,
StatusBarAlignment, StatusBarAlignment,
window, window,
workspace, workspace
WorkspaceFolder
} from 'vscode'; } from 'vscode';
import { statSync } from 'fs'; import { setInterval, clearInterval } from 'timers';
const lang = require('./data/languages.json');
interface FileDetail {
size: string | null;
totalLines: string | null;
currentLine: string | null;
currentColumn: string | null;
}
interface Activity {
details: string;
state: string;
startTimestamp: number | null;
largeImageKey: string;
largeImageText: string;
smallImageKey: string;
smallImageText: string;
instance: boolean;
}
const knownExtentions: { [x: string]: { image: string } } = lang.knownExtentions;
const knownLanguages: string[] = lang.knownLanguages;
// Define the RPC variable and its type.
let rpc: Client;
// Define the config variable and its type.
let config;
// Define the reconnecting var and its type.
let reconnecting: boolean;
// Define the reconnect counter and its type.
let reconnectCounter = 0;
// Define the last known file and its type.
let lastKnownFile: string;
// Define the activity object.
let activity: Activity;
// Define the activity timer to not spam the API with requests.
let activityTimer: NodeJS.Timer; let activityTimer: NodeJS.Timer;
// Define the status bar icon
let statusBarIcon: StatusBarItem; let statusBarIcon: StatusBarItem;
// `Activate` is fired when the extension is enabled. This SHOULD only fire once. const config = workspace.getConfiguration('discord');
export function activate(context: ExtensionContext) { const rpc = new RPCClient(config.get<string>('clientID')!);
console.log('[Discord Presence]: Activated!');
// Get the workspace's configuration for "discord".
config = workspace.getConfiguration('discord');
// Obtain whether or not the extension is activated. export async function activate(context: ExtensionContext) {
if (config.get('enabled')) initRPC(config.get('clientID')); Logger.log('Discord Presence activated!');
// Register the `discord.enable` command, and set the `enabled` config option to true. rpc.client.once('ready', () => {
const enabler = commands.registerCommand('discord.enable', async () => { Logger.log('Successfully connected to Discord.');
if (rpc) await destroyRPC(); if (!config.get<boolean>('silent')) window.showInformationMessage('Successfully reconnected to Discord RPC');
await config.update('enabled', true);
config = workspace.getConfiguration('discord'); if (statusBarIcon) statusBarIcon.dispose();
initRPC(config.get('clientID')); if (activityTimer) clearInterval(activityTimer);
window.showInformationMessage('Enabled Discord Rich Presence for this workspace.'); rpc.setActivity();
rpc.client.transport.once('close', async () => {
if (!config.get<boolean>('enabled')) return;
await rpc.dispose();
await rpc.login();
if (!statusBarIcon) {
statusBarIcon = window.createStatusBarItem(StatusBarAlignment.Left);
statusBarIcon.text = '$(plug) Reconnect to Discord';
statusBarIcon.command = 'discord.reconnect';
statusBarIcon.show();
}
}); });
// Register the `discord.disable` command, and set the `enabled` config option to false.
const disabler = commands.registerCommand('discord.disable', async () => {
if (!rpc) return window.showWarningMessage('Discord Rich Presence is already disabled in this workspace.');
await config.update('enabled', false);
config = workspace.getConfiguration('discord');
await destroyRPC();
window.showInformationMessage('Disabled Discord Rich Presence for this workspace.');
});
// Register the `discord.reconnect` command
const reconnecter = commands.registerCommand('discord.reconnect', async () => {
if (rpc) try { await destroyRPC(); } catch {}
initRPC(config.get('clientID'), true);
if (!config.get('silent')) window.showInformationMessage('Reconnecting to Discord RPC');
if (statusBarIcon) statusBarIcon.text = '$(pulse) Reconnecting';
});
// Push the new commands into the subscriptions.
context.subscriptions.push(enabler, disabler, reconnecter);
}
// `Deactivate` is fired whenever the extension is deactivated.
export async function deactivate() {
// If there's an RPC Client initalized, destroy it.
await destroyRPC();
}
// Initalize the RPC systems.
function initRPC(clientID: string, loud?: boolean): void {
// Update the RPC variable with a new RPC Client.
rpc = new Client({ transport: 'ipc' });
// Once the RPC Client is ready, set the activity.
rpc.once('ready', () => {
console.log('[Discord Presence]: Successfully connected to Discord');
// Announce the reconnection
if (loud && !config.get('silent')) window.showInformationMessage('Successfully reconnected to Discord RPC');
// Remove icon if connected
if (statusBarIcon) {
statusBarIcon.dispose();
statusBarIcon = null;
}
// Stop from reconnecing.
reconnecting = false;
// This is purely for safety measures.
if (activityTimer) {
// Clear the activity interval.
clearInterval(activityTimer);
// Null activity variable.
activityTimer = null;
}
// Reset the reconnect counter to 0 on a successful reconnect.
reconnectCounter = 0;
setActivity();
// Set the activity once on ready
setTimeout(() => rpc.setActivity(activity).catch(err => console.error(`[Discord Presence]: ${err}`)), 500);
// Make sure to listen to the close event and dispose and destroy everything accordingly.
rpc.transport.once('close', async () => {
if (!config.get('enabled')) return;
await destroyRPC();
// Set the client to begin reconnecting
reconnecting = true;
initRPC(config.get('clientID'));
// Create reconnecting button
createButton(true);
});
// Update the user's activity to the `activity` variable.
activityTimer = setInterval(() => { activityTimer = setInterval(() => {
// Update the config before updating the activity rpc.setActivity(config.get<boolean>('workspaceElapsedTime'));
config = workspace.getConfiguration('discord'); }, 10000);
setActivity(Boolean(config.get('workspaceElapsedTime'))); })
rpc.setActivity(activity).catch(err => console.error(`[Discord Presence]: ${err}`));
}, 15000);
});
// Log in to the RPC Client, and check whether or not it errors. if (config.get<boolean>('enabled')) {
rpc.login({ clientId: clientID }).catch(async error => { try {
// Check if the client is reconnecting await rpc.login();
console.error(`[Discord Presence]: ${error}`); } catch (error) {
if (reconnecting) { Logger.log(`Encountered following error after trying to login:\n${error}`);
// Destroy and dispose of everything after the set reconnect attempts
if (reconnectCounter >= config.get('reconnectThreshold')) {
// Create reconnect button
createButton();
await destroyRPC();
} else {
// Increment the counter
reconnectCounter++;
// Create reconnecting button
createButton(true);
// Retry connection
initRPC(config.get('clientID'));
return;
}
}
// Announce failure
if (!config.get('silent')) { if (!config.get('silent')) {
if (error.message.includes('ENOENT')) window.showErrorMessage('No Discord Client detected!'); if (error.message.includes('ENOENT')) window.showErrorMessage('No Discord Client detected!');
else window.showErrorMessage(`Couldn't connect to Discord via RPC: ${error.toString()}`); else window.showErrorMessage(`Couldn't connect to Discord via RPC: ${error.toString()}`);
createButton();
}
});
}
// Create reconnect button
function createButton(isReconnecting?: boolean): void {
// Check if the button exists already
if (!statusBarIcon) { if (!statusBarIcon) {
// Create the icon
statusBarIcon = window.createStatusBarItem(StatusBarAlignment.Left); statusBarIcon = window.createStatusBarItem(StatusBarAlignment.Left);
// Check if the client is reconnecting
if (isReconnecting) {
// Show attempts left
const attempts = config.get('reconnectThreshold') - reconnectCounter;
statusBarIcon.text = `$(issue-reopened) Reconnecting: ${attempts} attempt${attempts === 1 ? '' : 's'} left`;
statusBarIcon.command = '';
} else {
// Show button to reconnect
statusBarIcon.text = '$(plug) Reconnect to Discord'; statusBarIcon.text = '$(plug) Reconnect to Discord';
statusBarIcon.command = 'discord.reconnect'; statusBarIcon.command = 'discord.reconnect';
}
// Show the button
statusBarIcon.show(); statusBarIcon.show();
} else { }
// Check if the client is reconnecting
if (isReconnecting) {
// Show attempts left
const attempts = config.get('reconnectThreshold') - reconnectCounter;
statusBarIcon.text = `$(issue-reopened) Reconnecting: ${attempts} attempt${attempts === 1 ? '' : 's'} left`;
statusBarIcon.command = '';
} else {
// Show button to reconnect
statusBarIcon.text = '$(plug) Reconnect to Discord';
statusBarIcon.command = 'discord.reconnect';
} }
} }
} }
// Cleanly destroy the RPC client (if it isn't already) && add icon to reconnect const enabler = commands.registerCommand('discord.enable', async () => {
async function destroyRPC(): Promise<void> { await rpc.dispose();
// Do not continue if RPC isn't initalized. await config.update('enabled', true);
if (!rpc) return; await rpc.login();
// Stop reconnecting. window.showInformationMessage('Enabled Discord Rich Presence for this workspace.');
reconnecting = false; });
// Clear the activity interval.
if (activityTimer) clearInterval(activityTimer); const disabler = commands.registerCommand('discord.disable', async () => {
// Null the activity timer. await config.update('enabled', false);
activityTimer = null; await rpc.dispose();
// If there's an RPC Client initalized, destroy it. window.showInformationMessage('Disabled Discord Rich Presence for this workspace.');
await rpc.destroy(); });
// Null the RPC variable.
rpc = null; const reconnecter = commands.registerCommand('discord.reconnect', async () => {
// Null the last known file. await rpc.dispose();
lastKnownFile = null; await rpc.login();
if (!config.get('silent')) window.showInformationMessage('Reconnecting to Discord RPC...');
if (statusBarIcon) statusBarIcon.text = '$(pulse) reconnecting...';
});
context.subscriptions.push(enabler, disabler, reconnecter);
} }
// This function updates the activity (The Client's Rich Presence status). export async function deactivate() {
function setActivity(workspaceElapsedTime: boolean = false): void { clearInterval(activityTimer);
// Do not continue if RPC isn't initalized. await rpc.dispose();
if (!rpc) return;
if (window.activeTextEditor && window.activeTextEditor.document.fileName === lastKnownFile) {
activity = {
...activity,
details: generateDetails('detailsDebugging', 'detailsEditing', 'detailsIdle', activity.largeImageKey),
state: generateDetails('lowerDetailsDebugging', 'lowerDetailsEditing', 'lowerDetailsIdle', activity.largeImageKey),
smallImageKey: debug.activeDebugSession
? 'debug'
: env.appName.includes('Insiders')
? 'vscode-insiders'
: 'vscode',
};
return;
}
lastKnownFile = window.activeTextEditor ? window.activeTextEditor.document.fileName : null;
const fileName: string = window.activeTextEditor ? basename(window.activeTextEditor.document.fileName) : null;
const largeImageKey: any = window.activeTextEditor
? knownExtentions[Object.keys(knownExtentions).find(key => {
if (key.startsWith('.') && fileName.endsWith(key)) return true;
const match = key.match(/^\/(.*)\/([mgiy]+)$/);
if (!match) return false;
const regex = new RegExp(match[1], match[2]);
return regex.test(fileName);
})] || (knownLanguages.includes(window.activeTextEditor.document.languageId) ? window.activeTextEditor.document.languageId : null)
: 'vscode-big';
// Get the previous activity start timestamp (if available) to preserve workspace elapsed time
let previousTimestamp = null;
if (activity) previousTimestamp = activity['startTimestamp'];
// Create a JSON Object with the user's activity information.
activity = {
details: generateDetails('detailsDebugging', 'detailsEditing', 'detailsIdle', largeImageKey),
state: generateDetails('lowerDetailsDebugging', 'lowerDetailsEditing', 'lowerDetailsIdle', largeImageKey),
startTimestamp: window.activeTextEditor && previousTimestamp && workspaceElapsedTime ? previousTimestamp : window.activeTextEditor ? new Date().getTime() : null,
largeImageKey: largeImageKey
? largeImageKey.image
|| largeImageKey
: 'txt',
largeImageText: window.activeTextEditor
? config.get('largeImage').replace('{lang}', largeImageKey ? largeImageKey.image || largeImageKey : 'txt').replace('{LANG}', largeImageKey ? (largeImageKey.image || largeImageKey).toUpperCase() : 'TXT')
|| window.activeTextEditor.document.languageId.padEnd(2, '\u200b')
: config.get('largeImageIdle'),
smallImageKey: debug.activeDebugSession
? 'debug'
: env.appName.includes('Insiders')
? 'vscode-insiders'
: 'vscode',
smallImageText: config.get('smallImage').replace('{appname}', env.appName),
instance: false
};
}
function generateDetails(debugging, editing, idling, largeImageKey): string {
const emptySpaces = '\u200b\u200b';
let string: string = config.get(idling).replace('{null}', emptySpaces);
const fileName: string = window.activeTextEditor ? basename(window.activeTextEditor.document.fileName) : null;
let dirName: string = null;
if (window.activeTextEditor) {
const { dir } = parse(window.activeTextEditor.document.fileName);
const split = dir.split(sep);
dirName = split[split.length - 1];
}
const checkState: boolean = window.activeTextEditor
? Boolean(workspace.getWorkspaceFolder(window.activeTextEditor.document.uri))
: false;
const workspaceFolder: WorkspaceFolder = checkState ? workspace.getWorkspaceFolder(window.activeTextEditor.document.uri) : null;
let fullDirName: string = null;
if (workspaceFolder) {
const { name } = workspaceFolder;
const relativePath = workspace.asRelativePath(window.activeTextEditor.document.fileName).split(sep);
relativePath.splice(-1, 1);
fullDirName = `${name}${sep}${relativePath.join(sep)}`;
}
if (window.activeTextEditor) {
if (debug.activeDebugSession) {
let rawString = config.get(debugging);
const { totalLines, size, currentLine, currentColumn } = getFileDetails(rawString);
rawString = rawString
.replace('{null}', emptySpaces)
.replace('{filename}', fileName)
.replace('{dirname}', dirName)
.replace('{fulldirname}', fullDirName)
.replace('{workspace}',
checkState ?
workspaceFolder.name :
config.get('lowerDetailsNotFound').replace('{null}', emptySpaces)
)
.replace('{lang}', largeImageKey ? largeImageKey.image || largeImageKey : 'txt')
.replace('{LANG}', largeImageKey ? (largeImageKey.image || largeImageKey).toUpperCase() : 'TXT');
if (totalLines) rawString = rawString.replace('{totallines}', totalLines);
if (size) rawString = rawString.replace('{filesize}', size);
if (currentLine) rawString = rawString.replace('{currentline}', currentLine);
if (currentColumn) rawString = rawString.replace('{currentcolumn}', currentColumn);
string = rawString;
} else {
let rawString = config.get(editing);
const { totalLines, size, currentLine, currentColumn } = getFileDetails(rawString);
rawString = rawString
.replace('{null}', emptySpaces)
.replace('{filename}', fileName)
.replace('{dirname}', dirName)
.replace('{fulldirname}', fullDirName)
.replace('{workspace}',
checkState ?
workspaceFolder.name :
config.get('lowerDetailsNotFound').replace('{null}', emptySpaces)
)
.replace('{lang}', largeImageKey ? largeImageKey.image || largeImageKey : 'txt')
.replace('{LANG}', largeImageKey ? (largeImageKey.image || largeImageKey).toUpperCase() : 'TXT');
if (totalLines) rawString = rawString.replace('{totallines}', totalLines);
if (size) rawString = rawString.replace('{filesize}', size);
if (currentLine) rawString = rawString.replace('{currentline}', currentLine);
if (currentColumn) rawString = rawString.replace('{currentcolumn}', currentColumn);
string = rawString;
}
}
return string;
}
function getFileDetails(rawString): FileDetail {
const obj = {
size: null,
totalLines: null,
currentLine: null,
currentColumn: null,
};
if (!rawString) return obj;
if (rawString.includes('{totallines}')) {
obj.totalLines = window.activeTextEditor.document.lineCount.toLocaleString();
}
if (rawString.includes('{currentline}')) {
obj.currentLine = (window.activeTextEditor.selection.active.line + 1).toLocaleString();
}
if (rawString.includes('{currentcolumn}')) {
obj.currentColumn = (window.activeTextEditor.selection.active.character + 1).toLocaleString();
}
if (rawString.includes('{filesize}')) {
const sizes = [' bytes', 'kb', 'mb', 'gb', 'tb'];
let currentDivision = 0;
let { size } = statSync(window.activeTextEditor.document.fileName);
const originalSize = size;
if (originalSize > 1000) {
size = size / 1000;
currentDivision++;
while (size > 1000) {
currentDivision++;
size = size / 1000;
}
}
obj.size = `${originalSize > 1000 ? size.toFixed(2) : size}${sizes[currentDivision]}`;
}
return obj;
} }
process.on('unhandledRejection', console.error); process.on('unhandledRejection', console.error);

174
src/structures/Activity.ts Normal file
View file

@ -0,0 +1,174 @@
import {
debug,
Disposable,
env,
window,
workspace
} from 'vscode';
import { basename, sep, parse } from 'path';
import { statSync } from 'fs';
const lang = require('../data/languages.json');
const knownExtentions: { [key: string]: { image: string } } = lang.knownExtentions;
const knownLanguages: string[] = lang.knownLanguages;
interface Activity {
details?: string;
state?: string;
startTimestamp?: number | null;
largeImageKey?: string;
largeImageText?: string;
smallImageKey?: string;
smallImageText?: string;
instance?: boolean;
}
interface FileDetail {
size?: string;
totalLines?: string;
currentLine?: string;
currentColumn?: string;
}
export default class Acivity implements Disposable {
private _state: Activity | null = null;
private _config = workspace.getConfiguration('discord');
private _lastKnownFile: string = '';
public get state() {
return this._state;
}
public generate(workspaceElapsedTime: boolean = false) {
let largeImageKey: any = 'vscode-big';
if (window.activeTextEditor) {
if (window.activeTextEditor.document.fileName === this._lastKnownFile) {
return this._state = {
...this._state,
details: this._generateDetails('detailsDebugging', 'detailsEditing', 'detailsIdle', this._state!.largeImageKey),
state: this._generateDetails('lowerDetailsDebugging', 'lowerDetailsEditing', 'lowerDetailsIdle', this._state!.largeImageKey),
smallImageKey: debug.activeDebugSession ? 'debug' : env.appName.includes('Insiders') ? 'vscode-insiders' : 'vscode'
};
}
this._lastKnownFile = window.activeTextEditor.document.fileName;
const filename = basename(window.activeTextEditor.document.fileName);
largeImageKey = knownExtentions[Object.keys(knownExtentions).find(key => {
if (key.startsWith('.') && filename.endsWith(key)) return true;
const match = key.match(/^\/(.*)\/([mgiy]+)$/);
if (!match) return false;
const regex = new RegExp(match[1], match[2]);
return regex.test(filename);
})!] || (knownLanguages.includes(window.activeTextEditor.document.languageId) ? window.activeTextEditor.document.languageId : null)
}
let previousTimestamp = null;
if (this.state && this.state.startTimestamp) previousTimestamp = this.state.startTimestamp;
this._state = {
details: this._generateDetails('detailsDebugging', 'detailsEditing', 'detailsIdle', largeImageKey),
state: this._generateDetails('lowerDetailsDebugging', 'lowerDetailsEditing', 'lowerDetailsIdle', largeImageKey),
startTimestamp: window.activeTextEditor && previousTimestamp && workspaceElapsedTime ? previousTimestamp : window.activeTextEditor ? new Date().getTime() : null,
largeImageKey: largeImageKey ? largeImageKey.image || largeImageKey : 'txt',
largeImageText: window.activeTextEditor
? this._config.get<string>('largeImage')!.replace('{lang}', largeImageKey ? largeImageKey.image || largeImageKey : 'txt').replace('{LANG}', largeImageKey ? (largeImageKey.image || largeImageKey).toUpperCase() : 'TXT')
|| window.activeTextEditor.document.languageId.padEnd(2, '\u200b')
: this._config.get<string>('largeImageIdle'),
smallImageKey: debug.activeDebugSession ? 'debug' : env.appName.includes('Insiders') ? 'vscode-insiders' : 'vscode',
smallImageText: this._config.get<string>('smallImage')!.replace('{appname}', env.appName),
instance: false
};
return this._state;
}
private _generateDetails(debugging: string, editing: string, idling: string, largeImageKey: any) {
const empty = '\u200b\u200b';
let raw = this._config.get<string>(idling);
let filename = null;
let dirname = null;
let checkState = false;
let workspaceFolder = null;
let fullDirname = null;
if (window.activeTextEditor) {
filename = basename(window.activeTextEditor.document.fileName);
const { dir } = parse(window.activeTextEditor.document.fileName);
const split = dir.split(sep);
dirname = split[split.length - 1];
checkState = Boolean(workspace.getWorkspaceFolder(window.activeTextEditor.document.uri));
workspaceFolder = checkState ? workspace.getWorkspaceFolder(window.activeTextEditor.document.uri) : null;
if (workspaceFolder) {
const { name } = workspaceFolder;
const relativePath = workspace.asRelativePath(window.activeTextEditor.document.fileName).split(sep);
relativePath.splice(-1, 1);
fullDirname = `${name}${sep}${relativePath.join(sep)}`;
}
if (debug.activeDebugSession) {
raw = this._config.get<string>(debugging);
} else {
raw = this._config.get<string>(editing);
}
const { totalLines, size, currentLine, currentColumn } = this._generateFileDetails(raw);
raw = raw!
.replace('{null}', empty)
.replace('{filename}', filename)
.replace('{dirname}', dirname)
.replace('{fulldirname}', fullDirname!)
.replace('{workspace}', checkState && workspaceFolder ? workspaceFolder.name : this._config.get<string>('lowerDetailsNotFound')!.replace('{null}', empty))
.replace('{lang}', largeImageKey ? largeImageKey.image || largeImageKey : 'txt')
.replace('{Lang}', largeImageKey ? (largeImageKey.image || largeImageKey).toLowerCase().replace(/^\w/, (c: string) => c.toUpperCase()) : 'Txt')
.replace('{LANG}', largeImageKey ? (largeImageKey.image || largeImageKey).toUpperCase() : 'TXT');
if (totalLines) raw = raw!.replace('{totalline}', totalLines);
if (size) raw = raw!.replace('{filsize}', size);
if (currentLine) raw = raw!.replace('{currentline}', currentLine);
if (currentColumn) raw = raw!.replace('{currentcolumn}', currentColumn);
}
return raw;
}
private _generateFileDetails(str?: string) {
const fileDetail: FileDetail = {};
if (!str) return fileDetail;
if (window.activeTextEditor) {
if (str.includes('{totallines}')) {
fileDetail.totalLines = window.activeTextEditor.document.lineCount.toLocaleString();
}
if (str.includes('{currentline}')) {
fileDetail.currentLine = (window.activeTextEditor.selection.active.line + 1).toLocaleString();
}
if (str.includes('{currentcolumn}')) {
fileDetail.currentColumn = (window.activeTextEditor.selection.active.character + 1).toLocaleString();
}
if (str.includes('{filesize}')) {
const sizes = [' bytes', 'kb', 'mb', 'gb', 'tb'];
let currentDivision = 0;
let { size } = statSync(window.activeTextEditor.document.fileName);
const originalSize = size;
if (originalSize > 1000) {
size /= 1000;
currentDivision++;
while (size > 1000) {
currentDivision++;
size /= 1000;
}
}
fileDetail.size = `${originalSize > 1000 ? size.toFixed(2) : size}${sizes[currentDivision]}`;
}
}
return fileDetail;
}
public dispose() {
this._state = null;
this._lastKnownFile = '';
}
}

14
src/structures/Logger.ts Normal file
View file

@ -0,0 +1,14 @@
import { OutputChannel, window } from 'vscode';
export default class Logger {
static output: OutputChannel;
static setup() {
this.output = this.output || window.createOutputChannel('Discord Presence');
}
static log(message: string) {
if (!this.output) this.setup();
this.output.appendLine(message);
}
}

View file

@ -1,21 +1,21 @@
{ {
"compilerOptions": { "compilerOptions": {
"strict": true,
"module": "commonjs", "module": "commonjs",
"target": "es6", "target": "es6",
"lib": [ "lib": [
"esnext", "esnext",
"esnext.asynciterable" "esnext.array",
"esnext.asynciterable",
"esnext.intl",
"esnext.symbol"
], ],
"outDir": "out",
"rootDir": ".",
"declaration": true, "declaration": true,
"sourceMap": true, "sourceMap": true,
"removeComments": false, "removeComments": false,
"experimentalDecorators": true "experimentalDecorators": true
}, },
"exclude": [ "include": [
"node_modules", "./src"
".vscode-test",
"out"
] ]
} }

View file

@ -1,111 +0,0 @@
{
"rules": {
"arrow-return-shorthand": true,
"callable-types": true,
"class-name": true,
"comment-format": [
true,
"check-space"
],
"curly": false,
"eofline": true,
"forin": true,
"import-blacklist": [
true,
"rxjs"
],
"import-spacing": true,
"indent": [
true,
"tabs"
],
"interface-over-type-literal": false,
"label-position": true,
"member-access": false,
"member-ordering": [
true,
{
"order": [
"static-field",
"instance-field",
"static-method",
"instance-method"
]
}
],
"no-arg": true,
"no-bitwise": true,
"no-console": [
true,
"debug",
"info",
"time",
"timeEnd",
"trace"
],
"no-construct": true,
"no-debugger": true,
"no-duplicate-super": true,
"no-empty": false,
"no-empty-interface": true,
"no-eval": true,
"no-inferrable-types": [
true,
"ignore-params"
],
"no-misused-new": true,
"no-non-null-assertion": true,
"no-shadowed-variable": true,
"no-string-literal": false,
"no-string-throw": true,
"no-switch-case-fall-through": true,
"no-trailing-whitespace": true,
"no-unnecessary-initializer": true,
"no-unused-expression": true,
"no-use-before-declare": true,
"no-var-keyword": true,
"object-literal-sort-keys": false,
"one-line": [
true,
"check-open-brace",
"check-catch",
"check-else",
"check-whitespace"
],
"prefer-const": true,
"quotemark": [
true,
"single"
],
"radix": true,
"semicolon": [
true,
"always"
],
"triple-equals": [
true,
"allow-null-check"
],
"typedef-whitespace": [
true,
{
"call-signature": "nospace",
"index-signature": "nospace",
"parameter": "nospace",
"property-declaration": "nospace",
"variable-declaration": "nospace"
}
],
"typeof-compare": true,
"unified-signatures": true,
"variable-name": false,
"whitespace": [
true,
"check-branch",
"check-decl",
"check-operator",
"check-separator",
"check-type"
]
}
}

1738
yarn.lock

File diff suppressed because it is too large Load diff