{
93 | if (e) {
94 | postprocesManHTML(e, this.state.uniqueId);
95 | }
96 | }}
97 | />;
98 | }
99 | return
loading HTML man page...
;
100 | }
101 | }
102 |
103 | PluginManager.registerOutputDecorator({
104 | decorate: (job: Job): React.ReactElement
=> {
105 | const match = job.prompt.value.match(/^man\s+([a-zA-Z]\w*)\s*$/);
106 | return ;
107 | },
108 |
109 | isApplicable: (job: Job): boolean => {
110 | // Matches man page with a single arg that isn't a flag.
111 | return /^man\s+[a-zA-Z]\w*\s*$/.test(job.prompt.value);
112 | },
113 | });
114 |
--------------------------------------------------------------------------------
/src/shell/Command.ts:
--------------------------------------------------------------------------------
1 | import {Job} from "./Job";
2 | import {existsSync, statSync} from "fs";
3 | import {homeDirectory, pluralize, resolveDirectory, resolveFile, mapObject} from "../utils/Common";
4 | import {readFileSync} from "fs";
5 | import {EOL} from "os";
6 | import {Session} from "./Session";
7 | import {OrderedSet} from "../utils/OrderedSet";
8 | import {parseAlias} from "./Aliases";
9 |
10 | const executors: Dictionary<(i: Job, a: string[]) => void> = {
11 | cd: (job: Job, args: string[]): void => {
12 | let fullPath: string;
13 |
14 | if (!args.length) {
15 | fullPath = homeDirectory;
16 | } else {
17 | const enteredPath = args[0];
18 |
19 | if (isHistoricalDirectory(enteredPath)) {
20 | fullPath = expandHistoricalDirectory(enteredPath, job.session.historicalPresentDirectoriesStack);
21 | } else {
22 | fullPath = job.environment.cdpath
23 | .map(path => resolveDirectory(path, enteredPath))
24 | .filter(resolved => existsSync(resolved))
25 | .filter(resolved => statSync(resolved).isDirectory())[0];
26 |
27 | if (!fullPath) {
28 | throw new Error(`The directory "${enteredPath}" doesn't exist.`);
29 | }
30 | }
31 | }
32 |
33 | job.session.directory = fullPath;
34 | },
35 | clear: (job: Job, args: string[]): void => {
36 | setTimeout(() => job.session.clearJobs(), 0);
37 | },
38 | exit: (job: Job, args: string[]): void => {
39 | job.session.close();
40 | },
41 | export: (job: Job, args: string[]): void => {
42 | if (args.length === 0) {
43 | job.screenBuffer.writeMany(job.environment.map((key, value) => `${key}=${value}`).join("\r\n"));
44 | } else {
45 | args.forEach(argument => {
46 | const firstEqualIndex = argument.indexOf("=");
47 | const key = argument.slice(0, firstEqualIndex);
48 | const value = argument.slice(firstEqualIndex + 1);
49 |
50 | job.session.environment.set(key, value);
51 | });
52 | }
53 | },
54 | // FIXME: make the implementation more reliable.
55 | source: (job: Job, args: string[]): void => {
56 | sourceFile(job.session, args[0]);
57 | },
58 | alias: (job: Job, args: string[]): void => {
59 | if (args.length === 0) {
60 | job.screenBuffer.writeMany(mapObject(job.session.aliases.toObject(), (key, value) => `${key}=${value}`).join("\r\n"));
61 | } else if (args.length === 1) {
62 | const parsed = parseAlias(args[0]);
63 | job.session.aliases.add(parsed.name, parsed.value);
64 | } else {
65 | throw `Don't know what to do with ${args.length} arguments.`;
66 | }
67 | },
68 | unalias: (job: Job, args: string[]): void => {
69 | if (args.length === 1) {
70 | const name = args[0];
71 |
72 | if (job.session.aliases.has(name)) {
73 | job.session.aliases.remove(args[0]);
74 | } else {
75 | throw `There is such alias: ${name}.`;
76 | }
77 | } else {
78 | throw `Don't know what to do with ${args.length} arguments.`;
79 | }
80 | },
81 | show: (job: Job, args: string[]): void => {
82 | const imgs = args.map(argument => resolveFile(job.environment.pwd, argument));
83 | job.screenBuffer.writeMany(imgs.join(EOL));
84 | },
85 | };
86 |
87 | export function sourceFile(session: Session, fileName: string) {
88 | const content = readFileSync(resolveFile(session.directory, fileName)).toString();
89 |
90 | content.split(EOL).forEach(line => {
91 | if (line.startsWith("export ")) {
92 | const [key, value] = line.split(" ")[1].split("=");
93 | session.environment.set(key, value);
94 | }
95 | });
96 | }
97 |
98 | // A class representing built in commands
99 | export class Command {
100 | static allCommandNames = Object.keys(executors);
101 |
102 | static executor(command: string): (i: Job, args: string[]) => void {
103 | return executors[command];
104 | }
105 |
106 | static isBuiltIn(command: string): boolean {
107 | return this.allCommandNames.includes(command);
108 | }
109 | }
110 |
111 | export function expandHistoricalDirectory(alias: string, historicalDirectories: OrderedSet): string {
112 | if (alias === "-") {
113 | alias = "-1";
114 | }
115 | const index = historicalDirectories.size + parseInt(alias, 10);
116 |
117 | if (index < 0) {
118 | throw new Error(`Error: you only have ${historicalDirectories.size} ${pluralize("directory", historicalDirectories.size)} in the stack.`);
119 | } else {
120 | const directory = historicalDirectories.at(index);
121 |
122 | if (directory) {
123 | return directory;
124 | } else {
125 | throw `No directory with index ${index}`;
126 | }
127 | }
128 | }
129 |
130 | export function isHistoricalDirectory(directory: string): boolean {
131 | return /^-\d*$/.test(directory);
132 | }
133 |
--------------------------------------------------------------------------------
/src/plugins/autocompletion_providers/Top.ts:
--------------------------------------------------------------------------------
1 | import {PluginManager} from "../../PluginManager";
2 | import {shortFlag, mapSuggestions} from "../autocompletion_utils/Common";
3 | import combine from "../autocompletion_utils/Combine";
4 | import {mapObject} from "../../utils/Common";
5 |
6 | const options = combine(mapObject(
7 | {
8 | b: {
9 | short: "Batch mode operation",
10 | long: "Starts top in ’Batch mode’, which could be useful for sending\
11 | output from top to other programs or to a file. In this mode, top\
12 | will not accept input and runs until the iterations limit you’ve\
13 | set with the ’-n’ command-line option or until killed.",
14 | },
15 |
16 | c: {
17 | short: "Command line/Program name toggle",
18 | long: "Starts top with the last remembered ’c’ state reversed. Thus, if\
19 | top was displaying command lines, now that field will show program\
20 | names, and visa versa. See the ’c’ interactive command for\
21 | additional information.",
22 | },
23 |
24 | d: {
25 | short: "Delay time interval as: -d ss.tt (seconds.tenths)",
26 | long: "Specifies the delay between screen updates, and overrides the\
27 | corresponding value in one’s personal configuration file or the\
28 | startup default. Later this can be changed with the ’d’ or ’s’\
29 | interactive commands.\
30 | \
31 | Fractional seconds are honored, but a negative number is not\
32 | allowed. In all cases, however, such changes are prohibited if\
33 | top is running in ’Secure mode’, except for root (unless the ’s’\
34 | command-line option was used). For additional information on\
35 | ’Secure mode’ see topic 5a. SYSTEM Configuration File.",
36 | },
37 |
38 | h: {
39 | short: "Help",
40 | long: "Show library version and the usage prompt, then quit.",
41 | },
42 |
43 | H: {
44 | short: "Threads toggle",
45 | long: "Starts top with the last remembered ’H’ state reversed. When this\
46 | toggle is On, all individual threads will be displayed.\
47 | Otherwise, top displays a summation of all threads in a process.",
48 | },
49 |
50 | i: {
51 | short: "Idle Processes toggle", long: "Starts top with the last remembered ’i’ state reversed. When this\
52 | toggle is Off, tasks that are idled or zombied will not be\
53 | displayed.",
54 | },
55 |
56 | n: {
57 | short: "Number of iterations limit as: -n number",
58 | long: "Specifies the maximum number of iterations, or frames, top should\
59 | produce before ending.",
60 | },
61 |
62 | u: {
63 | short: "Monitor by user as: -u somebody",
64 | long: "Monitor only processes with an effective UID or user name matching\
65 | that given.",
66 | },
67 |
68 | U: {
69 | short: "Monitor by user as: -U somebody",
70 | long: "Monitor only processes with a UID or user name matching that\
71 | given. This matches real, effective, saved, and filesystem UIDs.",
72 | },
73 |
74 | p: {
75 | short: "Monitor PIDs as: -pN1 -pN2 ... or -pN1, N2 [,...]",
76 | long: "Monitor only processes with specified process IDs. This option\
77 | can be given up to 20 times, or you can provide a comma delimited\
78 | list with up to 20 pids. Co-mingling both approaches is\
79 | permitted.\
80 | \
81 | This is a command-line option only. And should you wish to return\
82 | to normal operation, it is not necessary to quit and and restart\
83 | top -- just issue the ’=’ interactive command.",
84 | },
85 |
86 | s: {
87 | short: "Secure mode operation",
88 | long: "Starts top with secure mode forced, even for root. This mode is\
89 | far better controlled through the system configuration file (see\
90 | topic 5. FILES).",
91 | },
92 |
93 | S: {
94 | short: "Cumulative time mode toggle",
95 | long: "Starts top with the last remembered ’S’ state reversed. When\
96 | ’Cumulative mode’ is On, each process is listed with the cpu time\
97 | that it and its dead children have used. See the ’S’ interactive\
98 | command for additional information regarding this mode.",
99 | },
100 |
101 | v: {
102 | short: "Version",
103 | long: "Show library version and the usage prompt, then quit.",
104 | },
105 | },
106 | (option, descriptions) => mapSuggestions(shortFlag(option), suggestion => suggestion.withSynopsis(descriptions.short).withDescription(descriptions.long))
107 | ));
108 |
109 |
110 | PluginManager.registerAutocompletionProvider("top", options);
111 |
--------------------------------------------------------------------------------
/src/utils/Git.ts:
--------------------------------------------------------------------------------
1 | import {linedOutputOf} from "../PTY";
2 | import * as Path from "path";
3 | import * as fs from "fs";
4 | import * as _ from "lodash";
5 |
6 | export class Branch {
7 | constructor(private refName: string, private _isCurrent: boolean) {
8 | }
9 |
10 | toString(): string {
11 | return _.last(this.refName.split("/"));
12 | }
13 |
14 | isCurrent(): boolean {
15 | return this._isCurrent;
16 | }
17 | }
18 |
19 | export interface ConfigVariable {
20 | name: string;
21 | value: string;
22 | }
23 |
24 | export type StatusCode =
25 | "Unmodified" |
26 |
27 | "UnstagedModified" |
28 | "UnstagedDeleted" |
29 | "StagedModified" |
30 | "StagedModifiedUnstagedModified" |
31 | "StagedModifiedUnstagedDeleted" |
32 | "StagedAdded" |
33 | "StagedAddedUnstagedModified" |
34 | "StagedAddedUnstagedDeleted" |
35 | "StagedDeleted" |
36 | "StagedDeletedUnstagedModified" |
37 | "StagedRenamed" |
38 | "StagedRenamedUnstagedModified" |
39 | "StagedRenamedUnstagedDeleted" |
40 | "StagedCopied" |
41 | "StagedCopiedUnstagedModified" |
42 | "StagedCopiedUnstagedDeleted" |
43 |
44 | "UnmergedBothDeleted" |
45 | "UnmergedAddedByUs" |
46 | "UnmergedDeletedByThem" |
47 | "UnmergedAddedByThem" |
48 | "UnmergedDeletedByUs" |
49 | "UnmergedBothAdded" |
50 | "UnmergedBothModified" |
51 |
52 | "Untracked" |
53 | "Ignored" |
54 |
55 | "Invalid"
56 |
57 | function lettersToStatusCode(letters: string): StatusCode {
58 | switch (letters) {
59 | case " ": return "Unmodified";
60 |
61 | case " M": return "UnstagedModified";
62 | case " D": return "UnstagedDeleted";
63 | case "M ": return "StagedModified";
64 | case "MM": return "StagedModifiedUnstagedModified";
65 | case "MD": return "StagedModifiedUnstagedDeleted";
66 | case "A ": return "StagedAdded";
67 | case "AM": return "StagedAddedUnstagedModified";
68 | case "AD": return "StagedAddedUnstagedDeleted";
69 | case "D ": return "StagedDeleted";
70 | case "DM": return "StagedDeletedUnstagedModified";
71 | case "R ": return "StagedRenamed";
72 | case "RM": return "StagedRenamedUnstagedModified";
73 | case "RD": return "StagedRenamedUnstagedDeleted";
74 | case "C ": return "StagedCopied";
75 | case "CM": return "StagedCopiedUnstagedModified";
76 | case "CD": return "StagedCopiedUnstagedDeleted";
77 |
78 | case "DD": return "UnmergedBothDeleted";
79 | case "AU": return "UnmergedAddedByUs";
80 | case "UD": return "UnmergedDeletedByThem";
81 | case "UA": return "UnmergedAddedByThem";
82 | case "DU": return "UnmergedDeletedByUs";
83 | case "AA": return "UnmergedBothAdded";
84 | case "UU": return "UnmergedBothModified";
85 |
86 | case "??": return "Untracked";
87 | case "!!": return "Ignored";
88 |
89 | default: return "Invalid";
90 | }
91 | }
92 |
93 | export class FileStatus {
94 | constructor(private _line: string) {
95 | }
96 |
97 | get value(): string {
98 | return this._line.slice(3).trim();
99 | }
100 |
101 | get code(): StatusCode {
102 | return lettersToStatusCode(this._line.slice(0, 2));
103 | }
104 | }
105 |
106 | type GitDirectoryPath = string & { __isGitDirectoryPath: boolean };
107 |
108 | export function isGitDirectory(directory: string): directory is GitDirectoryPath {
109 | return fs.existsSync(Path.join(directory, ".git") );
110 | }
111 |
112 | export async function branches(directory: GitDirectoryPath): Promise {
113 | let lines = await linedOutputOf(
114 | "git",
115 | ["for-each-ref", "refs/tags", "refs/heads", "refs/remotes", "--format='%(HEAD)%(refname:short)'"],
116 | directory
117 | );
118 | return lines.map(line => new Branch(line.slice(1), line[0] === "*"));
119 | }
120 |
121 | export async function configVariables(directory: string): Promise {
122 | const lines = await linedOutputOf(
123 | "git",
124 | ["config", "--list"],
125 | directory
126 | );
127 |
128 | return lines.map(line => {
129 | const parts = line.split("=");
130 |
131 | return {
132 | name: parts[0].trim(),
133 | value: parts[1] ? parts[1].trim() : "",
134 | };
135 | });
136 | }
137 |
138 | export async function aliases(directory: string): Promise {
139 | const variables = await configVariables(directory);
140 |
141 | return variables
142 | .filter(variable => variable.name.indexOf("alias.") === 0)
143 | .map(variable => {
144 | return {
145 | name: variable.name.replace("alias.", ""),
146 | value: variable.value,
147 | };
148 | });
149 | }
150 |
151 | export async function remotes(directory: GitDirectoryPath): Promise {
152 | return await linedOutputOf("git", ["remote"], directory);
153 | }
154 |
155 | export async function status(directory: GitDirectoryPath): Promise {
156 | let lines = await linedOutputOf("git", ["status", "--porcelain"], directory);
157 | return lines.map(line => new FileStatus(line));
158 | }
159 |
--------------------------------------------------------------------------------
/src/plugins/GitWatcher.ts:
--------------------------------------------------------------------------------
1 | import {Session} from "../shell/Session";
2 | import {PluginManager} from "../PluginManager";
3 | import {EnvironmentObserverPlugin} from "../Interfaces";
4 | import {watch, FSWatcher} from "fs";
5 | import * as Path from "path";
6 | import * as _ from "lodash";
7 | import {EventEmitter} from "events";
8 | import {executeCommand} from "../PTY";
9 | import {debounce} from "../Decorators";
10 | import * as Git from "../utils/Git";
11 |
12 | const GIT_WATCHER_EVENT_NAME = "git-data-changed";
13 |
14 | class GitWatcher extends EventEmitter {
15 | GIT_HEAD_FILE_NAME = Path.join(".git", "HEAD");
16 | GIT_HEADS_DIRECTORY_NAME = Path.join(".git", "refs", "heads");
17 |
18 | watcher: FSWatcher;
19 | gitDirectory: string;
20 |
21 | constructor(private directory: string) {
22 | super();
23 | this.gitDirectory = Path.join(this.directory, ".git");
24 | }
25 |
26 | stopWatching() {
27 | if (this.watcher) {
28 | this.watcher.close();
29 | }
30 | }
31 |
32 | watch() {
33 | if (Git.isGitDirectory(this.directory)) {
34 | this.updateGitData();
35 | this.watcher = watch(this.directory, {
36 | recursive: true,
37 | });
38 |
39 | this.watcher.on(
40 | "change",
41 | (type: string, fileName: string) => {
42 | if (!fileName.startsWith(".git") ||
43 | fileName === this.GIT_HEAD_FILE_NAME ||
44 | fileName.startsWith(this.GIT_HEADS_DIRECTORY_NAME)) {
45 | this.updateGitData();
46 | }
47 | }
48 | );
49 | } else {
50 | const data: VcsData = { kind: "not-repository" };
51 | this.emit(GIT_WATCHER_EVENT_NAME, data);
52 | }
53 | }
54 |
55 | @debounce(1000 / 60)
56 | private async updateGitData() {
57 |
58 | executeCommand("git", ["status", "-b", "--porcelain"], this.directory).then(changes => {
59 | const status: VcsStatus = changes.length ? "dirty" : "clean";
60 | let head: string = changes.split(" ")[1];
61 | let push: string = "0";
62 | let pull: string = "0";
63 |
64 | let secondSplit: Array = changes.split("[");
65 | if (secondSplit.length > 1) {
66 | let rawPushPull: string = secondSplit[1].slice(0, -2);
67 | let separatedPushPull: Array = rawPushPull.split(", ");
68 |
69 |
70 | if (separatedPushPull.length > 0) {
71 | for (let i in separatedPushPull) {
72 | if (separatedPushPull.hasOwnProperty(i)) {
73 | let splitAgain: Array = separatedPushPull[i].split(" ");
74 | switch (splitAgain[0]) {
75 | case "ahead":
76 | push = splitAgain[1];
77 | break;
78 | case "behind":
79 | pull = splitAgain[1];
80 | break;
81 | default:
82 | break;
83 | }
84 | }
85 | }
86 | }
87 | }
88 |
89 | const data: VcsData = {
90 | kind: "repository",
91 | branch: head,
92 | push: push,
93 | pull: pull,
94 | status: status,
95 | };
96 |
97 | this.emit(GIT_WATCHER_EVENT_NAME, data);
98 | });
99 | }
100 | }
101 |
102 | interface WatchesValue {
103 | listeners: Set;
104 | watcher: GitWatcher;
105 | data: VcsData;
106 | }
107 |
108 | class WatchManager implements EnvironmentObserverPlugin {
109 | directoryToDetails: Map = new Map();
110 |
111 | presentWorkingDirectoryWillChange(session: Session, newDirectory: string) {
112 | const oldDirectory = session.directory;
113 |
114 | if (!this.directoryToDetails.has(oldDirectory)) {
115 | return;
116 | }
117 |
118 | const details = this.directoryToDetails.get(oldDirectory)!;
119 | details.listeners.delete(session);
120 |
121 | if (details.listeners.size === 0) {
122 | details.watcher.stopWatching();
123 | this.directoryToDetails.delete(oldDirectory);
124 | }
125 | }
126 |
127 | presentWorkingDirectoryDidChange(session: Session, directory: string) {
128 | const existingDetails = this.directoryToDetails.get(directory);
129 |
130 | if (existingDetails) {
131 | existingDetails.listeners.add(session);
132 | } else {
133 | const watcher = new GitWatcher(directory);
134 |
135 | this.directoryToDetails.set(directory, {
136 | listeners: new Set([session]),
137 | watcher: watcher,
138 | data: { kind: "not-repository" },
139 | });
140 |
141 | watcher.watch();
142 |
143 | watcher.on(GIT_WATCHER_EVENT_NAME, (data: VcsData) => {
144 | const details = this.directoryToDetails.get(directory);
145 |
146 | if (details && !_.isEqual(data, details.data)) {
147 | details.data = data;
148 | details.listeners.forEach(listeningSession => listeningSession.emit("vcs-data"));
149 | }
150 | });
151 | }
152 | }
153 |
154 | vcsDataFor(directory: string): VcsData {
155 | const details = this.directoryToDetails.get(directory);
156 |
157 | if (details) {
158 | return details.data;
159 | } else {
160 | return { kind: "not-repository" };
161 | }
162 | }
163 | }
164 |
165 | export const watchManager = new WatchManager();
166 |
167 | PluginManager.registerEnvironmentObserver(watchManager);
168 |
--------------------------------------------------------------------------------
/src/shell/Scanner.ts:
--------------------------------------------------------------------------------
1 | import {Aliases} from "./Aliases";
2 |
3 | export abstract class Token {
4 | readonly raw: string;
5 | readonly fullStart: number;
6 |
7 | constructor(raw: string, fullStart: number) {
8 | this.raw = raw;
9 | this.fullStart = fullStart;
10 | }
11 |
12 | abstract get value(): string;
13 |
14 | /**
15 | * @deprecated
16 | */
17 | abstract get escapedValue(): EscapedShellWord;
18 | }
19 |
20 | export class Empty extends Token {
21 | constructor() {
22 | super("", 0);
23 | }
24 |
25 | get value() {
26 | return "";
27 | }
28 |
29 | get escapedValue() {
30 | return this.raw.trim();
31 | }
32 | }
33 |
34 | export class Word extends Token {
35 | get value() {
36 | return this.raw.trim().replace(/\\\s/g, " ");
37 | }
38 |
39 | get escapedValue() {
40 | return this.raw.trim();
41 | }
42 | }
43 |
44 | export class Pipe extends Token {
45 | get value() {
46 | return this.raw.trim();
47 | }
48 |
49 | get escapedValue(): EscapedShellWord {
50 | return this.value;
51 | }
52 | }
53 |
54 | export class Semicolon extends Token {
55 | get value() {
56 | return this.raw.trim();
57 | }
58 |
59 | get escapedValue(): EscapedShellWord {
60 | return this.value;
61 | }
62 | }
63 |
64 | export class And extends Token {
65 | get value() {
66 | return this.raw.trim();
67 | }
68 |
69 | get escapedValue(): EscapedShellWord {
70 | return this.value;
71 | }
72 | }
73 |
74 | export class Or extends Token {
75 | get value() {
76 | return this.raw.trim();
77 | }
78 |
79 | get escapedValue(): EscapedShellWord {
80 | return this.value;
81 | }
82 | }
83 |
84 | export class InputRedirectionSymbol extends Token {
85 | get value() {
86 | return this.raw.trim();
87 | }
88 |
89 | get escapedValue(): EscapedShellWord {
90 | return this.value;
91 | }
92 | }
93 |
94 | export class OutputRedirectionSymbol extends Token {
95 | get value() {
96 | return this.raw.trim();
97 | }
98 |
99 | get escapedValue(): EscapedShellWord {
100 | return this.value;
101 | }
102 | }
103 |
104 | export class AppendingOutputRedirectionSymbol extends Token {
105 | get value() {
106 | return this.raw.trim();
107 | }
108 |
109 | get escapedValue(): EscapedShellWord {
110 | return this.value;
111 | }
112 | }
113 |
114 | export abstract class StringLiteral extends Token {
115 | get value() {
116 | return this.raw.trim().slice(1, -1);
117 | }
118 | }
119 |
120 | export class SingleQuotedStringLiteral extends StringLiteral {
121 | get escapedValue(): EscapedShellWord {
122 | return `'${this.value}'`;
123 | }
124 | }
125 |
126 | export class DoubleQuotedStringLiteral extends StringLiteral {
127 | get escapedValue(): EscapedShellWord {
128 | return `"${this.value}"`;
129 | }
130 | }
131 |
132 | export class Invalid extends Token {
133 | get value() {
134 | return this.raw.trim();
135 | }
136 |
137 | get escapedValue(): EscapedShellWord {
138 | return this.value;
139 | }
140 | }
141 |
142 | const patterns = [
143 | {
144 | regularExpression: /^(\s*\|)/,
145 | tokenConstructor: Pipe,
146 | },
147 | {
148 | regularExpression: /^(\s*;)/,
149 | tokenConstructor: Semicolon,
150 | },
151 | {
152 | regularExpression: /^(\s*&&)/,
153 | tokenConstructor: And,
154 | },
155 | {
156 | regularExpression: /^(\s*\|\|)/,
157 | tokenConstructor: Or,
158 | },
159 | {
160 | regularExpression: /^(\s*>>)/,
161 | tokenConstructor: AppendingOutputRedirectionSymbol,
162 | },
163 | {
164 | regularExpression: /^(\s*<)/,
165 | tokenConstructor: InputRedirectionSymbol,
166 | },
167 | {
168 | regularExpression: /^(\s*>)/,
169 | tokenConstructor: OutputRedirectionSymbol,
170 | },
171 | {
172 | regularExpression: /^(\s*"(?:\\"|[^"])*")/,
173 | tokenConstructor: DoubleQuotedStringLiteral,
174 | },
175 | {
176 | regularExpression: /^(\s*'(?:\\'|[^'])*')/,
177 | tokenConstructor : SingleQuotedStringLiteral,
178 | },
179 | {
180 | regularExpression: /^(\s*(?:\\\s|[a-zA-Z0-9\u0080-\uFFFF+~!@#%^&*_=,.:/?\\-])+)/,
181 | tokenConstructor : Word,
182 | },
183 | ];
184 |
185 | export function scan(input: string): Token[] {
186 | const tokens: Token[] = [];
187 |
188 | let position = 0;
189 |
190 | while (true) {
191 | if (input.length === 0) {
192 | return tokens;
193 | }
194 |
195 | let foundMatch = false;
196 | for (const pattern of patterns) {
197 | const match = input.match(pattern.regularExpression);
198 |
199 | if (match) {
200 | const token = match[1];
201 | tokens.push(new pattern.tokenConstructor(token, position));
202 | position += token.length;
203 | input = input.slice(token.length);
204 | foundMatch = true;
205 | break;
206 | }
207 | }
208 |
209 | if (!foundMatch) {
210 | tokens.push(new Invalid(input, position));
211 | return tokens;
212 | }
213 | }
214 | }
215 |
216 | function concatTokens(left: Token[], right: Token[]): Token[] {
217 | return left.concat(right);
218 | }
219 |
220 | export function expandAliases(tokens: Token[], aliases: Aliases): Token[] {
221 | if (tokens.length === 0) {
222 | return [];
223 | }
224 |
225 | const commandWordToken = tokens[0];
226 | const argumentTokens = tokens.slice(1);
227 |
228 | if (aliases.has(commandWordToken.value)) {
229 | const alias = aliases.get(commandWordToken.value);
230 | const aliasTokens = scan(alias);
231 | const isRecursive = aliasTokens[0].value === commandWordToken.value;
232 |
233 | if (isRecursive) {
234 | return concatTokens(aliasTokens, argumentTokens);
235 | } else {
236 | return concatTokens(expandAliases(scan(alias), aliases), argumentTokens);
237 | }
238 | } else {
239 | return tokens;
240 | }
241 | }
242 |
--------------------------------------------------------------------------------
/test/shell/scanner_spec.ts:
--------------------------------------------------------------------------------
1 | import {expect} from "chai";
2 | import {
3 | scan, Word, DoubleQuotedStringLiteral, SingleQuotedStringLiteral,
4 | Pipe, OutputRedirectionSymbol, AppendingOutputRedirectionSymbol, InputRedirectionSymbol, Invalid, Semicolon,
5 | } from "../../src/shell/Scanner";
6 |
7 | describe("scan", () => {
8 | it("returns no tokens on empty input", () => {
9 | const tokens = scan("");
10 |
11 | expect(tokens.length).to.eq(0);
12 | });
13 |
14 | it("returns an invalid token on input that consists only of spaces", () => {
15 | const tokens = scan(" ");
16 |
17 | expect(tokens.length).to.eq(1);
18 | expect(tokens[0]).to.be.an.instanceof(Invalid);
19 | });
20 |
21 | it("splits on a space", () => {
22 | const tokens = scan("some words");
23 |
24 | expect(tokens.length).to.eq(2);
25 | expect(tokens[0]).to.be.an.instanceof(Word);
26 | expect(tokens[1]).to.be.an.instanceof(Word);
27 |
28 | expect(tokens.map(token => token.value)).to.eql(["some", "words"]);
29 | });
30 |
31 | it("doesn't split inside double quotes", () => {
32 | const tokens = scan('prefix "inside quotes"');
33 |
34 | expect(tokens.length).to.eq(2);
35 | expect(tokens[0]).to.be.an.instanceof(Word);
36 | expect(tokens[1]).to.be.an.instanceof(DoubleQuotedStringLiteral);
37 |
38 | expect(tokens.map(token => token.value)).to.eql(["prefix", "inside quotes"]);
39 | });
40 |
41 | it("doesn't split inside single quotes", () => {
42 | const tokens = scan("prefix 'inside quotes'");
43 |
44 | expect(tokens.length).to.eq(2);
45 | expect(tokens[0]).to.be.an.instanceof(Word);
46 | expect(tokens[1]).to.be.an.instanceof(SingleQuotedStringLiteral);
47 |
48 | expect(tokens.map(token => token.value)).to.eql(["prefix", "inside quotes"]);
49 | });
50 |
51 | it("doesn't split on an escaped space", () => {
52 | const tokens = scan("prefix single\\ token");
53 |
54 | expect(tokens.length).to.eq(2);
55 | expect(tokens[0]).to.be.an.instanceof(Word);
56 | expect(tokens[1]).to.be.an.instanceof(Word);
57 |
58 | expect(tokens.map(token => token.value)).to.eql(["prefix", "single token"]);
59 | });
60 |
61 | it("doesn't split on a colon", () => {
62 | const tokens = scan("curl http://www.example.com");
63 |
64 | expect(tokens.length).to.eq(2);
65 | expect(tokens[0]).to.be.an.instanceof(Word);
66 | expect(tokens[1]).to.be.an.instanceof(Word);
67 |
68 | expect(tokens.map(token => token.value)).to.eql(["curl", "http://www.example.com"]);
69 | });
70 |
71 | it("can handle special characters", () => {
72 | const tokens = scan("ls --color=tty -lh");
73 |
74 | expect(tokens.length).to.eq(3);
75 | expect(tokens[0]).to.be.an.instanceof(Word);
76 | expect(tokens[1]).to.be.an.instanceof(Word);
77 | expect(tokens[2]).to.be.an.instanceof(Word);
78 |
79 | expect(tokens.map(token => token.value)).to.eql(["ls", "--color=tty", "-lh"]);
80 | });
81 |
82 | it("recognizes a pipe", () => {
83 | const tokens = scan("cat file | grep word");
84 |
85 | expect(tokens.length).to.eq(5);
86 | expect(tokens[0]).to.be.an.instanceof(Word);
87 | expect(tokens[1]).to.be.an.instanceof(Word);
88 | expect(tokens[2]).to.be.an.instanceof(Pipe);
89 | expect(tokens[3]).to.be.an.instanceof(Word);
90 | expect(tokens[4]).to.be.an.instanceof(Word);
91 |
92 | expect(tokens.map(token => token.value)).to.eql(["cat", "file", "|", "grep", "word"]);
93 | });
94 |
95 | it("recognizes a semicolon", () => {
96 | const tokens = scan("cd directory; rm file");
97 |
98 | expect(tokens.length).to.eq(5);
99 | expect(tokens[0]).to.be.an.instanceof(Word);
100 | expect(tokens[1]).to.be.an.instanceof(Word);
101 | expect(tokens[2]).to.be.an.instanceof(Semicolon);
102 | expect(tokens[3]).to.be.an.instanceof(Word);
103 | expect(tokens[4]).to.be.an.instanceof(Word);
104 |
105 | expect(tokens.map(token => token.value)).to.eql(["cd", "directory", ";", "rm", "file"]);
106 | });
107 |
108 | it("recognizes input redirection", () => {
109 | const tokens = scan("cat < file");
110 |
111 | expect(tokens.length).to.eq(3);
112 | expect(tokens[0]).to.be.an.instanceof(Word);
113 | expect(tokens[1]).to.be.an.instanceof(InputRedirectionSymbol);
114 | expect(tokens[2]).to.be.an.instanceof(Word);
115 |
116 | expect(tokens.map(token => token.value)).to.eql(["cat", "<", "file"]);
117 | });
118 |
119 | it("recognizes output redirection", () => {
120 | const tokens = scan("cat file > another_file");
121 |
122 | expect(tokens.length).to.eq(4);
123 | expect(tokens[0]).to.be.an.instanceof(Word);
124 | expect(tokens[1]).to.be.an.instanceof(Word);
125 | expect(tokens[2]).to.be.an.instanceof(OutputRedirectionSymbol);
126 | expect(tokens[3]).to.be.an.instanceof(Word);
127 |
128 | expect(tokens.map(token => token.value)).to.eql(["cat", "file", ">", "another_file"]);
129 | });
130 |
131 | it("recognizes appending output redirection", () => {
132 | const tokens = scan("cat file >> another_file");
133 |
134 | expect(tokens.length).to.eq(4);
135 | expect(tokens[0]).to.be.an.instanceof(Word);
136 | expect(tokens[1]).to.be.an.instanceof(Word);
137 | expect(tokens[2]).to.be.an.instanceof(AppendingOutputRedirectionSymbol);
138 | expect(tokens[3]).to.be.an.instanceof(Word);
139 |
140 | expect(tokens.map(token => token.value)).to.eql(["cat", "file", ">>", "another_file"]);
141 | });
142 |
143 | it("can handle unicode é", () => {
144 | const tokens = scan("cd é/");
145 | expect(tokens.map(token => token.value)).to.eql(["cd", "é/"]);
146 | });
147 |
148 | it("can handle 'x+' (regression test for #753)", () => {
149 | const tokens = scan("cd x+");
150 | expect(tokens.map(token => token.value)).to.eql(["cd", "x+"]);
151 | });
152 |
153 | describe("invalid input", () => {
154 | it("adds an invalid token", async() => {
155 | const tokens = scan("cd '");
156 |
157 | expect(tokens.length).to.eq(2);
158 | expect(tokens[0]).to.be.an.instanceof(Word);
159 | expect(tokens[1]).to.be.an.instanceof(Invalid);
160 |
161 | expect(tokens.map(token => token.value)).to.eql(["cd", "'"]);
162 | });
163 | });
164 | });
165 |
--------------------------------------------------------------------------------
/src/plugins/autocompletion_providers/NPM.ts:
--------------------------------------------------------------------------------
1 | import * as Path from "path";
2 | import {Suggestion, styles} from "../autocompletion_utils/Common";
3 | import {exists, readFile, mapObject} from "../../utils/Common";
4 | import {PluginManager} from "../../PluginManager";
5 |
6 | const npmCommandConfig = [
7 | {
8 | name: "access",
9 | description: "Set access level on published packages",
10 | },
11 | {
12 | name: "adduser",
13 | description: "Add a registry user account",
14 | },
15 | {
16 | name: "bin",
17 | description: "Display npm bin folder",
18 | },
19 | {
20 | name: "bugs",
21 | description: "Bugs for a package in a web browser maybe",
22 | },
23 | {
24 | name: "build",
25 | description: "Build a package",
26 | },
27 | {
28 | name: "bundle",
29 | description: "REMOVED",
30 | },
31 | {
32 | name: "cache",
33 | description: "Manipulates packages cache",
34 | },
35 | {
36 | name: "completion",
37 | description: "Tab Completion for npm",
38 | },
39 | {
40 | name: "config",
41 | description: "Manage the npm configuration files",
42 | },
43 | {
44 | name: "dedupe",
45 | description: "Reduce duplication",
46 | },
47 | {
48 | name: "deprecate",
49 | description: "Deprecate a version of a package",
50 | },
51 | {
52 | name: "dist-tag",
53 | description: "Modify package distribution tags",
54 | },
55 | {
56 | name: "docs",
57 | description: "Docs for a package in a web browser maybe",
58 | },
59 | {
60 | name: "edit",
61 | description: "Edit an installed package",
62 | },
63 | {
64 | name: "explore",
65 | description: "Browse an installed package",
66 | },
67 | {
68 | name: "help",
69 | description: "Get help on npm",
70 | },
71 | {
72 | name: "help-search",
73 | description: "Search npm help documentation",
74 | },
75 | {
76 | name: "init",
77 | description: "Interactively create a package.json file",
78 | },
79 | {
80 | name: "install",
81 | description: "Install a package",
82 | },
83 | {
84 | name: "install-test",
85 | description: "",
86 | },
87 | {
88 | name: "link",
89 | description: "Symlink a package folder",
90 | },
91 | {
92 | name: "logout",
93 | description: "Log out of the registry",
94 | },
95 | {
96 | name: "ls",
97 | description: "List installed packages",
98 | },
99 | {
100 | name: "npm",
101 | description: "javascript package manager",
102 | },
103 | {
104 | name: "outdated",
105 | description: "Check for outdated packages",
106 | },
107 | {
108 | name: "owner",
109 | description: "Manage package owners",
110 | },
111 | {
112 | name: "pack",
113 | description: "Create a tarball from a package",
114 | },
115 | {
116 | name: "ping",
117 | description: "Ping npm registry",
118 | },
119 | {
120 | name: "prefix",
121 | description: "Display prefix",
122 | },
123 | {
124 | name: "prune",
125 | description: "Remove extraneous packages",
126 | },
127 | {
128 | name: "publish",
129 | description: "Publish a package",
130 | },
131 | {
132 | name: "rebuild",
133 | description: "Rebuild a package",
134 | },
135 | {
136 | name: "repo",
137 | description: "Open package repository page in the browser",
138 | },
139 | {
140 | name: "restart",
141 | description: "Restart a package",
142 | },
143 | {
144 | name: "root",
145 | description: "Display npm root",
146 | },
147 | {
148 | name: "run",
149 | description: "Run arbitrary package scripts",
150 | },
151 | {
152 | name: "search",
153 | description: "Search for packages",
154 | },
155 | {
156 | name: "shrinkwrap",
157 | description: "Lock down dependency versions",
158 | },
159 | {
160 | name: "star",
161 | description: "Mark your favorite packages",
162 | },
163 | {
164 | name: "stars",
165 | description: "View packages marked as favorites",
166 | },
167 | {
168 | name: "start",
169 | description: "Start a package",
170 | },
171 | {
172 | name: "stop",
173 | description: "Stop a package",
174 | },
175 | {
176 | name: "tag",
177 | description: "Tag a published version",
178 | },
179 | {
180 | name: "team",
181 | description: "Manage organization teams and team memberships",
182 | },
183 | {
184 | name: "test",
185 | description: "Test a package",
186 | },
187 | {
188 | name: "uninstall",
189 | description: "Remove a package",
190 | },
191 | {
192 | name: "unpublish",
193 | description: "Remove a package from the registry",
194 | },
195 | {
196 | name: "update",
197 | description: "Update a package",
198 | },
199 | {
200 | name: "version",
201 | description: "Bump a package version",
202 | },
203 | {
204 | name: "view",
205 | description: "View registry info",
206 | },
207 | {
208 | name: "whoami",
209 | description: "Display npm username",
210 | },
211 | ];
212 |
213 | const npmCommand = npmCommandConfig.map(config => new Suggestion({value: config.name, description: config.description, style: styles.command}));
214 |
215 | PluginManager.registerAutocompletionProvider("npm", async (context) => {
216 | if (context.argument.position === 1) {
217 | return npmCommand;
218 | } else if (context.argument.position === 2) {
219 | const firstArgument = context.argument.command.nthArgument(1);
220 |
221 | if (firstArgument && firstArgument.value === "run") {
222 | const packageFilePath = Path.join(context.environment.pwd, "package.json");
223 |
224 | if (await exists(packageFilePath)) {
225 | const parsed = JSON.parse(await readFile(packageFilePath)).scripts || {};
226 | return mapObject(parsed, (key: string, value: string) => new Suggestion({value: key, description: value, style: styles.command}));
227 | } else {
228 | return [];
229 | }
230 | } else {
231 | // TODO: handle npm sub commands other than "run" that can be
232 | // further auto-completed
233 | return [];
234 | }
235 | } else {
236 | return [];
237 | }
238 | });
239 |
--------------------------------------------------------------------------------
/src/shell/Job.ts:
--------------------------------------------------------------------------------
1 | import * as _ from "lodash";
2 | import * as i from "../Interfaces";
3 | import * as React from "react";
4 | import {Session} from "./Session";
5 | import {ANSIParser} from "../ANSIParser";
6 | import {Prompt} from "./Prompt";
7 | import {ScreenBuffer} from "../ScreenBuffer";
8 | import {CommandExecutor, NonZeroExitCodeError} from "./CommandExecutor";
9 | import {PTY} from "../PTY";
10 | import {PluginManager} from "../PluginManager";
11 | import {EmitterWithUniqueID} from "../EmitterWithUniqueID";
12 | import {Status} from "../Enums";
13 | import {Environment} from "./Environment";
14 | import {normalizeKey} from "../utils/Common";
15 | import {TerminalLikeDevice} from "../Interfaces";
16 | import {History} from "./History";
17 |
18 | function makeThrottledDataEmitter(timesPerSecond: number, subject: EmitterWithUniqueID) {
19 | return _.throttle(() => subject.emit("data"), 1000 / timesPerSecond);
20 | }
21 |
22 | export class Job extends EmitterWithUniqueID implements TerminalLikeDevice {
23 | public command: PTY;
24 | public status: Status = Status.NotStarted;
25 | public readonly parser: ANSIParser;
26 | public interceptionResult: React.ReactElement | undefined;
27 | private readonly _prompt: Prompt;
28 | private readonly _screenBuffer: ScreenBuffer;
29 | private readonly rareDataEmitter: Function;
30 | private readonly frequentDataEmitter: Function;
31 | private executedWithoutInterceptor: boolean = false;
32 |
33 | constructor(private _session: Session) {
34 | super();
35 |
36 | this._prompt = new Prompt(this);
37 | this._prompt.on("send", () => this.execute());
38 |
39 | this.rareDataEmitter = makeThrottledDataEmitter(1, this);
40 | this.frequentDataEmitter = makeThrottledDataEmitter(60, this);
41 |
42 | this._screenBuffer = new ScreenBuffer();
43 | this._screenBuffer.on("data", this.throttledDataEmitter);
44 | this.parser = new ANSIParser(this);
45 | }
46 |
47 | async executeWithoutInterceptor(): Promise {
48 | if (!this.executedWithoutInterceptor) {
49 | this.executedWithoutInterceptor = true;
50 | try {
51 | await CommandExecutor.execute(this);
52 |
53 | // Need to check the status here because it's
54 | // executed even after the process was interrupted.
55 | if (this.status === Status.InProgress) {
56 | this.setStatus(Status.Success);
57 | }
58 | this.emit("end");
59 | } catch (exception) {
60 | this.handleError(exception);
61 | }
62 | }
63 | }
64 |
65 | async execute({allowInterception = true} = {}): Promise {
66 | History.add(this.prompt.value);
67 |
68 | if (this.status === Status.NotStarted) {
69 | this.setStatus(Status.InProgress);
70 | }
71 |
72 | const commandWords: string[] = this.prompt.expandedTokens.map(token => token.escapedValue);
73 | const interceptorOptions = {
74 | command: commandWords,
75 | presentWorkingDirectory: this.environment.pwd,
76 | };
77 | const interceptor = PluginManager.commandInterceptorPlugins.find(
78 | potentialInterceptor => potentialInterceptor.isApplicable(interceptorOptions)
79 | );
80 |
81 | await Promise.all(PluginManager.preexecPlugins.map(plugin => plugin(this)));
82 | if (interceptor && allowInterception) {
83 | if (!this.interceptionResult) {
84 | try {
85 | this.interceptionResult = await interceptor.intercept(interceptorOptions);
86 | this.setStatus(Status.Success);
87 | } catch (e) {
88 | await this.executeWithoutInterceptor();
89 | }
90 | }
91 | } else {
92 | await this.executeWithoutInterceptor();
93 | }
94 | this.emit("end");
95 | }
96 |
97 | handleError(message: NonZeroExitCodeError | string): void {
98 | this.setStatus(Status.Failure);
99 | if (message) {
100 | if (message instanceof NonZeroExitCodeError) {
101 | // Do nothing.
102 | } else {
103 | this._screenBuffer.writeMany(message);
104 | }
105 | }
106 | this.emit("end");
107 | }
108 |
109 | // Writes to the process' STDIN.
110 | write(input: string|KeyboardEvent) {
111 | let text: string;
112 |
113 | if (typeof input === "string") {
114 | text = input;
115 | } else {
116 | text = input.ctrlKey ? String.fromCharCode(input.keyCode - 64) : normalizeKey(input.key, this.screenBuffer.cursorKeysMode);
117 | }
118 |
119 | this.command.write(text);
120 | }
121 |
122 | get session(): Session {
123 | return this._session;
124 | }
125 |
126 | get dimensions(): Dimensions {
127 | return this.session.dimensions;
128 | }
129 |
130 | set dimensions(dimensions: Dimensions) {
131 | this.session.dimensions = dimensions;
132 | this.winch();
133 | }
134 |
135 | hasOutput(): boolean {
136 | return !this._screenBuffer.isEmpty();
137 | }
138 |
139 | interrupt(): void {
140 | if (this.command && this.status === Status.InProgress) {
141 | this.command.kill("SIGINT");
142 | this.setStatus(Status.Interrupted);
143 | this.emit("end");
144 | }
145 | }
146 |
147 | winch(): void {
148 | if (this.command && this.status === Status.InProgress) {
149 | this.command.dimensions = this.dimensions;
150 | }
151 | }
152 |
153 | canBeDecorated(): boolean {
154 | return !!this.firstApplicableDecorator;
155 | }
156 |
157 | decorate(): React.ReactElement {
158 | if (this.firstApplicableDecorator) {
159 | return this.firstApplicableDecorator.decorate(this);
160 | } else {
161 | throw "No applicable decorator found.";
162 | }
163 | }
164 |
165 | get environment(): Environment {
166 | // TODO: implement inline environment variable setting.
167 | return this.session.environment;
168 | }
169 |
170 | private get decorators(): i.OutputDecorator[] {
171 | return PluginManager.outputDecorators.filter(decorator =>
172 | this.status === Status.InProgress ? decorator.shouldDecorateRunningPrograms : true
173 | );
174 | }
175 |
176 | private get firstApplicableDecorator(): i.OutputDecorator | undefined {
177 | return this.decorators.find(decorator => decorator.isApplicable(this));
178 | }
179 |
180 | get screenBuffer(): ScreenBuffer {
181 | return this._screenBuffer;
182 | }
183 |
184 | get prompt(): Prompt {
185 | return this._prompt;
186 | }
187 |
188 | setStatus(status: Status): void {
189 | this.status = status;
190 | this.emit("status", status);
191 | }
192 |
193 | private throttledDataEmitter = () =>
194 | this._screenBuffer.size < ScreenBuffer.hugeOutputThreshold ? this.frequentDataEmitter() : this.rareDataEmitter();
195 | }
196 |
--------------------------------------------------------------------------------
/src/views/1_ApplicationComponent.tsx:
--------------------------------------------------------------------------------
1 | import {SessionComponent} from "./2_SessionComponent";
2 | import {TabComponent, TabProps, Tab} from "./TabComponent";
3 | import * as React from "react";
4 | import * as _ from "lodash";
5 | import {ipcRenderer} from "electron";
6 | import {remote} from "electron";
7 | import * as css from "./css/main";
8 | import {saveWindowBounds} from "./ViewUtils";
9 | import {StatusBarComponent} from "./StatusBarComponent";
10 | import {PaneTree, Pane} from "../utils/PaneTree";
11 | import {SearchComponent} from "./SearchComponent";
12 |
13 | export class ApplicationComponent extends React.Component<{}, {}> {
14 | private tabs: Tab[] = [];
15 | private focusedTabIndex: number;
16 |
17 | constructor(props: {}) {
18 | super(props);
19 | const electronWindow = remote.BrowserWindow.getAllWindows()[0];
20 |
21 | this.addTab(false);
22 |
23 | electronWindow
24 | .on("move", () => saveWindowBounds(electronWindow))
25 | .on("resize", () => {
26 | saveWindowBounds(electronWindow);
27 | this.recalculateDimensions();
28 | })
29 | .webContents
30 | .on("devtools-opened", () => this.recalculateDimensions())
31 | .on("devtools-closed", () => this.recalculateDimensions());
32 |
33 | ipcRenderer.on("change-working-directory", (event: Electron.IpcRendererEvent, directory: string) =>
34 | this.focusedTab.focusedPane.session.directory = directory
35 | );
36 |
37 | window.onbeforeunload = () => {
38 | electronWindow
39 | .removeAllListeners()
40 | .webContents
41 | .removeAllListeners("devtools-opened")
42 | .removeAllListeners("devtools-closed")
43 | .removeAllListeners("found-in-page");
44 |
45 | this.closeAllTabs();
46 | };
47 |
48 | window.application = this;
49 | }
50 |
51 | addTab(forceUpdate = true): void {
52 | if (this.tabs.length < 9) {
53 | this.tabs.push(new Tab(this));
54 | this.focusedTabIndex = this.tabs.length - 1;
55 | if (forceUpdate) this.forceUpdate();
56 | } else {
57 | remote.shell.beep();
58 | }
59 |
60 | window.focusedTab = this.focusedTab;
61 | }
62 |
63 | focusTab(position: OneBasedPosition): void {
64 | const index = position === 9 ? this.tabs.length : position - 1;
65 |
66 | if (this.tabs.length > index) {
67 | this.focusedTabIndex = index;
68 | this.forceUpdate();
69 | } else {
70 | remote.shell.beep();
71 | }
72 |
73 | window.focusedTab = this.focusedTab;
74 | }
75 |
76 | closeFocusedTab() {
77 | this.closeTab(this.focusedTab);
78 |
79 | this.forceUpdate();
80 | }
81 |
82 | activatePreviousTab() {
83 | let newPosition = this.focusedTabIndex - 1;
84 |
85 | if (newPosition < 0) {
86 | newPosition = this.tabs.length - 1;
87 | }
88 |
89 | this.focusTab(newPosition + 1);
90 | }
91 |
92 | activateNextTab() {
93 | let newPosition = this.focusedTabIndex + 1;
94 |
95 | if (newPosition >= this.tabs.length) {
96 | newPosition = 0;
97 | }
98 |
99 | this.focusTab(newPosition + 1);
100 | }
101 |
102 | // FIXME: this method should be private.
103 | closeFocusedPane() {
104 | this.focusedTab.closeFocusedPane();
105 |
106 | if (this.focusedTab.panes.size === 0) {
107 | this.closeTab(this.focusedTab);
108 | }
109 |
110 | this.forceUpdate();
111 | }
112 |
113 | render() {
114 | let tabs: React.ReactElement[] | undefined;
115 |
116 | if (this.tabs.length > 1) {
117 | tabs = this.tabs.map((tab: Tab, index: number) =>
118 | {
122 | this.focusedTabIndex = index;
123 | this.forceUpdate();
124 | }}
125 | closeHandler={(event: React.MouseEvent) => {
126 | this.closeTab(this.tabs[index]);
127 | this.forceUpdate();
128 |
129 | event.stopPropagation();
130 | event.preventDefault();
131 | }}>
132 |
133 | );
134 | }
135 |
136 | return (
137 |
138 |
142 | {this.renderPanes(this.focusedTab.panes)}
143 |
144 |
145 | );
146 | }
147 |
148 | private renderPanes(tree: PaneTree): JSX.Element {
149 | if (tree instanceof Pane) {
150 | const pane = tree;
151 | const session = pane.session;
152 | const isFocused = pane === this.focusedTab.focusedPane;
153 |
154 | return (
155 | this.forceUpdate() : undefined}
159 | focus={() => {
160 | this.focusedTab.activatePane(pane);
161 | this.forceUpdate();
162 | }}>
163 |
164 | );
165 | } else {
166 | return {tree.children.map(child => this.renderPanes(child))}
;
167 | }
168 | }
169 |
170 | private recalculateDimensions() {
171 | for (const tab of this.tabs) {
172 | tab.updateAllPanesDimensions();
173 | }
174 | }
175 |
176 | private get focusedTab(): Tab {
177 | return this.tabs[this.focusedTabIndex];
178 | }
179 |
180 | private closeTab(tab: Tab, quit = true): void {
181 | tab.closeAllPanes();
182 | _.pull(this.tabs, tab);
183 |
184 | if (this.tabs.length === 0 && quit) {
185 | ipcRenderer.send("quit");
186 | } else if (this.tabs.length === this.focusedTabIndex) {
187 | this.focusedTabIndex -= 1;
188 | }
189 |
190 | window.focusedTab = this.focusedTab;
191 | }
192 |
193 | private closeAllTabs(): void {
194 | // Can't use forEach here because closeTab changes the array being iterated.
195 | while (this.tabs.length) {
196 | this.closeTab(this.tabs[0], false);
197 | }
198 | }
199 | }
200 |
--------------------------------------------------------------------------------
/src/views/UserEventsHander.ts:
--------------------------------------------------------------------------------
1 | import {ApplicationComponent} from "./1_ApplicationComponent";
2 | import {SessionComponent} from "./2_SessionComponent";
3 | import {PromptComponent} from "./4_PromptComponent";
4 | import {JobComponent} from "./3_JobComponent";
5 | import {Tab} from "./TabComponent";
6 | import {Status, KeyboardAction} from "../Enums";
7 | import {isModifierKey} from "./ViewUtils";
8 | import {SearchComponent} from "./SearchComponent";
9 | import {remote} from "electron";
10 | import {buildMenuTemplate} from "./menu/Menu";
11 | import {isKeybindingForEvent} from "./keyevents/Keybindings";
12 |
13 | export type UserEvent = KeyboardEvent | ClipboardEvent;
14 |
15 | export const handleUserEvent = (application: ApplicationComponent,
16 | tab: Tab,
17 | session: SessionComponent,
18 | job: JobComponent,
19 | prompt: PromptComponent,
20 | search: SearchComponent) => (event: UserEvent) => {
21 | if (event instanceof ClipboardEvent) {
22 | if (search.isFocused) {
23 | return;
24 | }
25 |
26 | if (!isInProgress(job)) {
27 | prompt.focus();
28 | return;
29 | }
30 |
31 | job.props.job.write(event.clipboardData.getData("text/plain"));
32 |
33 | event.stopPropagation();
34 | event.preventDefault();
35 |
36 | return;
37 | }
38 |
39 | // Close focused pane
40 | if (isKeybindingForEvent(event, KeyboardAction.paneClose) && !isInProgress(job)) {
41 | application.closeFocusedPane();
42 |
43 | application.forceUpdate();
44 |
45 | event.stopPropagation();
46 | event.preventDefault();
47 | return;
48 | }
49 |
50 | // Change tab action
51 | if (isKeybindingForEvent(event, KeyboardAction.tabFocus)) {
52 | const position = parseInt(event.key, 10);
53 | application.focusTab(position);
54 |
55 | event.stopPropagation();
56 | event.preventDefault();
57 | return;
58 | }
59 |
60 | // Enable debug mode
61 | if (isKeybindingForEvent(event, KeyboardAction.developerToggleDebugMode)) {
62 | window.DEBUG = !window.DEBUG;
63 |
64 | require("devtron").install();
65 | console.log(`Debugging mode has been ${window.DEBUG ? "enabled" : "disabled"}.`);
66 |
67 | application.forceUpdate();
68 |
69 | event.stopPropagation();
70 | event.preventDefault();
71 | return;
72 | }
73 |
74 | // Console clear
75 | if (isKeybindingForEvent(event, KeyboardAction.cliClearJobs) && !isInProgress(job)) {
76 | session.props.session.clearJobs();
77 |
78 | event.stopPropagation();
79 | event.preventDefault();
80 | return;
81 | }
82 |
83 | if (event.metaKey) {
84 | event.stopPropagation();
85 | // Don't prevent default to be able to open developer tools and such.
86 | return;
87 | }
88 |
89 | if (search.isFocused) {
90 | // Search close
91 | if (isKeybindingForEvent(event, KeyboardAction.editFindClose)) {
92 | search.clearSelection();
93 | setTimeout(() => prompt.focus(), 0);
94 |
95 | event.stopPropagation();
96 | event.preventDefault();
97 | return;
98 | }
99 |
100 | return;
101 | }
102 |
103 |
104 | if (isInProgress(job) && !isModifierKey(event)) {
105 | // CLI interrupt
106 | if (isKeybindingForEvent(event, KeyboardAction.cliInterrupt)) {
107 | job.props.job.interrupt();
108 | } else {
109 | job.props.job.write(event);
110 | }
111 |
112 | event.stopPropagation();
113 | event.preventDefault();
114 | return;
115 | }
116 |
117 | prompt.focus();
118 |
119 | // Append last argument to prompt
120 | if (isKeybindingForEvent(event, KeyboardAction.cliAppendLastArgumentOfPreviousCommand)) {
121 | prompt.appendLastLArgumentOfPreviousCommand();
122 |
123 | event.stopPropagation();
124 | event.preventDefault();
125 | return;
126 | }
127 |
128 | if (!isInProgress(job)) {
129 | // CLI Delete word
130 | if (isKeybindingForEvent(event, KeyboardAction.cliDeleteWord)) {
131 | prompt.deleteWord();
132 |
133 | event.stopPropagation();
134 | event.preventDefault();
135 | return;
136 | }
137 |
138 | // CLI execute command
139 | if (isKeybindingForEvent(event, KeyboardAction.cliRunCommand)) {
140 | prompt.execute((event.target as HTMLElement).innerText);
141 |
142 | event.stopPropagation();
143 | event.preventDefault();
144 | return;
145 | }
146 |
147 | // CLI clear
148 | if (isKeybindingForEvent(event, KeyboardAction.cliClearText)) {
149 | prompt.clear();
150 |
151 | event.stopPropagation();
152 | event.preventDefault();
153 | return;
154 | }
155 |
156 | if (prompt.isAutocompleteShown()) {
157 | if (isKeybindingForEvent(event, KeyboardAction.autocompleteInsertCompletion)) {
158 | prompt.applySuggestion();
159 |
160 | event.stopPropagation();
161 | event.preventDefault();
162 | return;
163 | }
164 |
165 | if (isKeybindingForEvent(event, KeyboardAction.autocompletePreviousSuggestion)) {
166 | prompt.focusPreviousSuggestion();
167 |
168 | event.stopPropagation();
169 | event.preventDefault();
170 | return;
171 | }
172 |
173 | if (isKeybindingForEvent(event, KeyboardAction.autocompleteNextSuggestion)) {
174 | prompt.focusNextSuggestion();
175 |
176 | event.stopPropagation();
177 | event.preventDefault();
178 | return;
179 | }
180 | } else {
181 | if (isKeybindingForEvent(event, KeyboardAction.cliHistoryPrevious)) {
182 | prompt.setPreviousHistoryItem();
183 |
184 | event.stopPropagation();
185 | event.preventDefault();
186 | return;
187 | }
188 |
189 | if (isKeybindingForEvent(event, KeyboardAction.cliHistoryNext)) {
190 | prompt.setNextHistoryItem();
191 |
192 | event.stopPropagation();
193 | event.preventDefault();
194 | return;
195 | }
196 | }
197 | }
198 |
199 | prompt.setPreviousKeyCode(event);
200 | };
201 |
202 | function isInProgress(job: JobComponent): boolean {
203 | return job.props.job.status === Status.InProgress;
204 | }
205 |
206 | const app = remote.app;
207 | const browserWindow = remote.BrowserWindow.getAllWindows()[0];
208 | const template = buildMenuTemplate(app, browserWindow);
209 |
210 | remote.Menu.setApplicationMenu(remote.Menu.buildFromTemplate(template));
211 |
--------------------------------------------------------------------------------
/src/plugins/autocompletion_providers/Executable.ts:
--------------------------------------------------------------------------------
1 | export const commandDescriptions: Dictionary = {
2 | admin: "Create and administer SCCS files",
3 | alias: "Define or display aliases",
4 | ar: "Create and maintain library archives",
5 | asa: "Interpret carriage-control characters",
6 | at: "Execute commands at a later time",
7 | awk: "Pattern scanning and processing language",
8 | basename: "Return non-directory portion of a pathname; see also dirname",
9 | batch: "Schedule commands to be executed in a batch queue",
10 | bc: "Arbitrary-precision arithmetic language",
11 | bg: "Run jobs in the background",
12 | cc: "Compile standard C programs",
13 | cal: "Print a calendar",
14 | cat: "Concatenate and print files",
15 | cflow: "Generate a C-language flowgraph",
16 | chgrp: "Change the file group ownership",
17 | chmod: "Change the file modes/attributes/permissions",
18 | chown: "Change the file ownership",
19 | cksum: "Write file checksums and sizes",
20 | cmp: "Compare two files; see also diff",
21 | comm: "Select or reject lines common to two files",
22 | command: "Execute a simple command",
23 | compress: "Compress data",
24 | cp: "Copy files",
25 | crontab: "Schedule periodic background work",
26 | csplit: "Split files based on context",
27 | ctags: "Create a tags file",
28 | cut: "Cut out selected fields of each line of a file",
29 | cxref: "Generate a C-language program cross-reference table",
30 | date: "Display the date and time",
31 | dd: "Convert and copy a file",
32 | delta: "Make a delta (change) to an SCCS file",
33 | df: "Report free disk space",
34 | diff: "Compare two files; see also cmp",
35 | dirname: "Return the directory portion of a pathname; see also basename",
36 | du: "Estimate file space usage",
37 | echo: "Write arguments to standard output",
38 | ed: "The standard text editor",
39 | env: "Set the environment for command job",
40 | ex: "Text editor",
41 | expand: "Convert tabs to spaces",
42 | expr: "Evaluate arguments as an expression",
43 | FALSE: "Return false value",
44 | fc: "Process the command history list",
45 | fg: "Run jobs in the foreground",
46 | file: "Determine file type",
47 | find: "Find files",
48 | fold: "Filter for folding lines",
49 | fort77: "FORTRAN compiler",
50 | fuser: "List process IDs of all processes that have one or more files open",
51 | gencat: "Generate a formatted message catalog",
52 | get: "Get a version of an SCCS file",
53 | getconf: "Get configuration values",
54 | getopts: "Parse utility options",
55 | grep: "Search text for a pattern",
56 | hash: "hash database access method",
57 | head: "Copy the first part of files",
58 | iconv: "Codeset conversion",
59 | id: "Return user identity",
60 | ipcrm: "Remove a message queue, semaphore set, or shared memory segment identifier",
61 | ipcs: "Report interprocess communication facilities status",
62 | jobs: "Display status of jobs in the current session",
63 | join: "Merges two sorted text files based on the presence of a common field",
64 | kill: "Terminate or signal processes",
65 | lex: "Generate programs for lexical tasks",
66 | link: "Create a hard link to a file",
67 | ln: "Link files",
68 | locale: "Get locale-specific information",
69 | localedef: "Define locale environment",
70 | logger: "Log messages",
71 | logname: "Return the user\"s login name",
72 | lp: "Send files to a printer",
73 | ls: "List directory contents",
74 | m4: "Macro processor",
75 | mailx: "Process messages",
76 | make: "Maintain, update, and regenerate groups of programs",
77 | man: "Display system documentation",
78 | mesg: "Permit or deny messages",
79 | mkdir: "Make directories",
80 | mkfifo: "Make FIFO special files",
81 | more: "Display files on a page-by-page basis",
82 | mv: "Move files",
83 | newgrp: "Change to a new group (functionaliy similar to sg[1])",
84 | nice: "Invoke a utility with an altered nice value",
85 | nl: "Line numbering filter",
86 | nm: "Write the name list of an object file",
87 | nohup: "Invoke a utility immune to hangups",
88 | od: "Dump files in various formats",
89 | paste: "Merge corresponding or subsequent lines of files",
90 | patch: "Apply changes to files",
91 | pathchk: "Check pathnames",
92 | pax: "Portable archive interchange",
93 | pr: "Print files",
94 | printf: "Write formatted output",
95 | prs: "Print an SCCS file",
96 | ps: "Report process status",
97 | pwd: "print working directory - Return working directory name",
98 | qalter: "Alter batch job",
99 | qdel: "Delete batch jobs",
100 | qhold: "Hold batch jobs",
101 | qmove: "Move batch jobs",
102 | qmsg: "Send message to batch jobs",
103 | qrerun: "Rerun batch jobs",
104 | qrls: "Release batch jobs",
105 | qselect: "Select batch jobs",
106 | qsig: "Signal batch jobs",
107 | qstat: "Show status of batch jobs",
108 | qsub: "Submit a script",
109 | read: "Read a line from standard input",
110 | renice: "Set nice values of running processes",
111 | rm: "Remove directory entries",
112 | rmdel: "Remove a delta from an SCCS file",
113 | rmdir: "Remove directories",
114 | sact: "Print current SCCS file-editing activity",
115 | sccs: "Front end for the SCCS subsystem",
116 | sed: "Stream editor",
117 | sh: "Shell, the standard command language interpreter",
118 | sleep: "Suspend execution for an interval",
119 | sort: "Sort, merge, or sequence check text files",
120 | split: "Split files into pieces",
121 | strings: "Find printable strings in files",
122 | strip: "Remove unnecessary information from executable files",
123 | stty: "Set the options for a terminal",
124 | tabs: "Set terminal tabs",
125 | tail: "Copy the last part of a file",
126 | talk: "Talk to another user",
127 | tee: "Duplicate the standard output",
128 | test: "Evaluate expression",
129 | time: "Time a simple command",
130 | touch: "Change file access and modification times",
131 | tput: "Change terminal characteristics",
132 | tr: "Translate characters",
133 | TRUE: "Return true value",
134 | tsort: "Topological sort",
135 | tty: "Return user\"s terminal name ",
136 | type: "Displays how a name would be interpreted if used as a command",
137 | ulimit: "Set or report file size limit",
138 | umask: "Get or set the file mode creation mask",
139 | unalias: "Remove alias definitions",
140 | uname: "Return system name",
141 | uncompress: "Expand compressed data",
142 | unexpand: "Convert spaces to tabs",
143 | unget: "Undo a previous get of an SCCS file",
144 | uniq: "Report or filter out repeated lines in a file",
145 | unlink: "Call the unlink function",
146 | uucp: "System-to-system copy",
147 | uudecode: "Decode a binary file",
148 | uuencode: "Encode a binary file",
149 | uustat: "uucp status inquiry and job control",
150 | uux: "Remote command execution",
151 | val: "Validate SCCS files",
152 | vi: "Screen-oriented (visual) display editor",
153 | wait: "Await process completion",
154 | wc: "Line, word and byte or character count",
155 | what: "Identify SCCS files",
156 | who: "Display who is on the system",
157 | write: "Write to another user\"s terminal",
158 | xargs: "Construct argument lists and invoke utility",
159 | yacc: "Yet another compiler compiler",
160 | zcat: "Expand and concatenate data",
161 | };
162 |
--------------------------------------------------------------------------------