├── .editorconfig ├── .gitignore ├── LICENSE.md ├── README.md ├── keymaps └── irc.cson ├── lib ├── commands.coffee ├── connector.coffee ├── irc-status-view.coffee ├── irc-view.coffee └── irc.coffee ├── menus └── irc.cson ├── package.json ├── screenshot.png ├── spec ├── irc-spec.coffee └── irc-view-spec.coffee └── styles └── irc.less /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | node_modules 4 | 5 | .zedstate 6 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Chris Saylor 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Atom.io IRC client 2 | 3 | IRC client that works inside of Atom.io editor. 4 | 5 | ![Screenshot](https://github.com/cjsaylor/atom-irc/blob/master/screenshot.png?raw=true) 6 | 7 | 8 | This package is a work in progress. Feel free to open issues or pull requests! 9 | 10 | ### Features 11 | 12 | * Pane view for large view. 13 | * Status bar with notifications of new messages. 14 | * Support for PMs from users. 15 | * Support for custom commands (i.e. `/msg `). 16 | * Customizable output (thanks @adamclerk). 17 | 18 | ### Roadmap 19 | 20 | * Support for list of users on the channel. 21 | * Easy Channel switching. 22 | * Popup private messages. 23 | -------------------------------------------------------------------------------- /keymaps/irc.cson: -------------------------------------------------------------------------------- 1 | 'atom-workspace': 2 | 'ctrl-alt-i': 'irc:toggle' 3 | -------------------------------------------------------------------------------- /lib/commands.coffee: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { key: 'msg', pattern: /^\/msg\s(\w*)\s(.*)/i } 3 | { key: 'whois', pattern: /^\/whois\s(\w*)$/i } 4 | ] 5 | -------------------------------------------------------------------------------- /lib/connector.coffee: -------------------------------------------------------------------------------- 1 | util = require 'util' 2 | Irc = require 'irc'; 3 | commands = require './commands' 4 | {EventEmitter} = require 'events' 5 | 6 | module.exports = 7 | class Connector 8 | 9 | retryCount = 3 10 | 11 | client: null 12 | emitter: null 13 | connected: false 14 | 15 | constructor: (options) -> 16 | @client = new Irc.Client(options.host, options.nickname, { 17 | channels: options.channels?.split(','), 18 | debug: options.debug, 19 | secure: options.secure, 20 | port: parseInt(options.port || 6697), 21 | password: options.serverPassword or null, 22 | selfSigned: true, 23 | autoConnect: false 24 | retryCount: retryCount 25 | }); 26 | @emitter = new EventEmitter() 27 | @client.on 'notice', (from, to, text) => 28 | @client.say('NickServ', 'identify ' + options.password) if from is 'NickServ' and text.indexOf('identify') >= 0 29 | 30 | on: (event, callback) => 31 | if event in ['disconnected', 'connected'] 32 | @emitter.on(event, callback) 33 | else 34 | @client.on(event, callback) 35 | @ 36 | 37 | sendMessage: (message) -> 38 | return unless message and @connected 39 | for command in commands 40 | if command.pattern.test message 41 | tokens = command.pattern.exec message 42 | return @senders()[command.key] tokens 43 | @senders().default message 44 | 45 | senders: => 46 | default: (message) => @client.say atom.config.get('irc.channels'), message 47 | msg: (tokens) => @client.say tokens[1], tokens[2] if tokens.length is 3 48 | whois: (tokens) => @client.whois tokens[1] if tokens.length is 2 49 | 50 | connect: => 51 | return if @connected 52 | @client.connect => 53 | @connected = true 54 | @emitter.emit 'connected' 55 | 56 | disconnect: => 57 | return if not @connected or @client.conn is null 58 | @client.disconnect => 59 | @connected = false 60 | @emitter.emit 'disconnected' 61 | 62 | clearEvents: => 63 | @client.removeAllListeners() 64 | @emitter.removeAllListeners() 65 | @ 66 | 67 | disband: => 68 | @clearEvents().disconnect() 69 | -------------------------------------------------------------------------------- /lib/irc-status-view.coffee: -------------------------------------------------------------------------------- 1 | {CompositeDisposable} = require 'atom' 2 | {$, View} = require 'atom-space-pen-views' 3 | 4 | module.exports = 5 | class IrcStatusView extends View 6 | 7 | constructor: -> 8 | @disposables = new CompositeDisposable 9 | super 10 | 11 | @content: -> 12 | @span id: 'irc-status', => 13 | @a href: '#', class: 'irc-status inline-block', tabindex: '-2', 'IRC' 14 | 15 | initialize: -> 16 | @click => 17 | @removeClass().addClass('connected') if @hasClass('notify') 18 | workspaceEl = atom.views.getView(atom.workspace) 19 | atom.commands.dispatch workspaceEl, 'irc:toggle' 20 | 21 | destroy: -> 22 | @disposables.dispose() 23 | @detach() 24 | -------------------------------------------------------------------------------- /lib/irc-view.coffee: -------------------------------------------------------------------------------- 1 | {$, ScrollView} = require 'atom-space-pen-views' 2 | util = require 'util' 3 | Autolinker = require 'autolinker' 4 | 5 | module.exports = 6 | class IrcView extends ScrollView 7 | 8 | @ircOutput: null 9 | @package: null 10 | 11 | @content: -> 12 | @div class: 'irc native-key-bindings', tabindex: -1, => 13 | @div class: 'input', => 14 | @div '', class: 'irc-output native-key-bindings' 15 | @input outlet: 'ircMessage', type: 'text', class: 'irc-input native-key-bindings', placeholder: 'Enter your message...' 16 | 17 | initialize: (@client) -> 18 | @sendCommand = atom.commands.add 'atom-workspace', 'irc:send', (e) => 19 | @client.sendMessage e.detail.message 20 | @autolinker = new Autolinker(newWindow: false, email: false, twitter: false, phone: false) 21 | 22 | attached: -> 23 | @ircOutput = @find('.irc-output') 24 | @ircMessage.on 'keydown', (e) => 25 | if e.keyCode is 13 and @ircMessage.val() 26 | workspaceEl = atom.views.getView(atom.workspace) 27 | atom.commands.dispatch workspaceEl, 'irc:send', message: @ircMessage.val() 28 | @addMessage atom.config.get('irc.nickname'), null, @ircMessage.val() 29 | @ircMessage.val '' 30 | 31 | getTitle: -> 32 | 'IRC ' + atom.config.get('irc.channels') 33 | 34 | destroy: -> 35 | @sendCommand.dispose() 36 | @detach() 37 | 38 | addMessage: (from, to, message) => 39 | ircOutput = @find('.irc-output') 40 | line = $('

') 41 | line.addClass 'pm' if to is atom.config.get 'irc.nickname' 42 | line.addClass 'whois' if from is 'WHOIS' 43 | line.addClass 'from-me' if from is atom.config.get 'irc.nickname' 44 | line.addClass 'connected' if from is 'CONNECTED' 45 | line.addClass 'disconnected' if from is 'DISCONNECTED' 46 | line.addClass 'joined' if from is 'JOINED' 47 | line.addClass 'quit' if from is 'QUIT' 48 | line.addClass "from-#{from}" 49 | 50 | ts = $('') 51 | ts.addClass 'ts' 52 | ts.text(util.format '%s', new Date().toLocaleTimeString()) 53 | 54 | un = $('') 55 | un.addClass 'un' 56 | un.text(util.format '%s', from) 57 | 58 | msg = $('') 59 | un.addClass 'msg' 60 | if atom.config.get('irc.evalHtml') 61 | msg.addClass 'html' 62 | msg.html(util.format '%s', @autolinker.link(message)) 63 | else 64 | msg.addClass 'txt' 65 | msg.text(util.format '%s', message) 66 | 67 | appendFunction = => ircOutput.append line.append ts, [un, msg] 68 | if ircOutput.prop('scrollHeight') is ircOutput.scrollTop() + ircOutput.outerHeight() 69 | appendFunction() 70 | ircOutput.scrollTop ircOutput.prop 'scrollHeight' 71 | else 72 | appendFunction() 73 | 74 | focusInput: => 75 | @find('.irc-input').focus() 76 | @ 77 | 78 | scrollToEnd: => 79 | ircOutput = @find '.irc-output' 80 | ircOutput.scrollTop ircOutput.prop 'scrollHeight' 81 | @ 82 | -------------------------------------------------------------------------------- /lib/irc.coffee: -------------------------------------------------------------------------------- 1 | {CompositeDisposable} = require 'atom' 2 | url = require 'url' 3 | IrcView = require './irc-view' 4 | IrcStatusView = null 5 | Client = require './connector' 6 | 7 | module.exports = 8 | ircView: null 9 | ircStatusView: null 10 | 11 | config: 12 | host: 13 | type: 'string' 14 | default: '' 15 | port: 16 | type: 'number' 17 | default: 6697 18 | secure: 19 | type: 'boolean' 20 | default: false 21 | nickname: 22 | type: 'string' 23 | default: '' 24 | password: 25 | type: 'string' 26 | default: '' 27 | serverPassword: 28 | type: 'string' 29 | default: '' 30 | channels: 31 | type: 'string' 32 | default: '' 33 | debug: 34 | type: 'boolean' 35 | default: false 36 | connectOnStartup: 37 | type: 'boolean' 38 | default: false 39 | evalHtml: 40 | type: 'boolean' 41 | default: false 42 | showJoinMessages: 43 | type: 'boolean' 44 | default: false 45 | 46 | activate: -> 47 | atom.workspace.addOpener (uriToOpen) => 48 | {protocol, host} = url.parse uriToOpen 49 | return unless protocol is 'irc:' 50 | @ircView if host is 'chat' 51 | 52 | @initializeIrc() 53 | 54 | @ircView = new IrcView(@client) 55 | 56 | @subscriptions = new CompositeDisposable 57 | @subscriptions.add atom.commands.add 'atom-workspace', 'irc:toggle', => 58 | pane = @findOpenPane() 59 | if pane 60 | pane.focus() 61 | pane.focusInput().scrollToEnd() 62 | else 63 | atom.workspace.open('irc://chat', split: 'right', searchAllPanes: true).then (ircView) -> 64 | ircView 65 | .focusInput() 66 | .scrollToEnd() 67 | @subscriptions.add atom.commands.add 'atom-workspace', 'irc:connect', => 68 | @client.connect() 69 | @subscriptions.add atom.commands.add 'atom-workspace', 'irc:disconnect', => 70 | @client.disconnect() 71 | @subscriptions.add atom.config.onDidChange 'irc', => 72 | @initializeIrc true 73 | 74 | findOpenPane: -> 75 | matched = false 76 | atom.workspace.getPaneItems().forEach (pane) -> 77 | if pane instanceof IrcView 78 | matched = pane 79 | matched 80 | 81 | deactivate: -> 82 | @ircStatusView.destroy() 83 | @ircView.destroy() 84 | @subscriptions.dispose() 85 | @client.disband() 86 | 87 | initializeIrc: (reinitialized)-> 88 | return if @client and not reinitialized 89 | @client?.disband() 90 | console.log 'Initializing IRC' if atom.config.get('irc.debug') 91 | @client = new Client atom.config.get('irc') 92 | @client 93 | .on 'connected', => 94 | @ircStatusView.removeClass().addClass('connected') 95 | @ircView?.addMessage 'CONNECTED', null, 'You have successfully connected!' 96 | .on 'disconnected', => 97 | @ircStatusView.removeClass() 98 | @ircView?.addMessage 'DISCONNECTED', null, 'You have been disconnected.' 99 | @bindIrcEvents() 100 | @client.connect() if atom.config.get('irc.connectOnStartup') 101 | 102 | bindIrcEvents: -> 103 | @client 104 | .on 'message', (from, to, message) => 105 | @ircStatusView?.removeClass().addClass 'notify' 106 | @ircView?.addMessage from, to, message 107 | .on 'error', @errorHandler.bind @ 108 | .on 'abort', @errorHandler.bind @ 109 | .on 'whois', (info) => @ircView.addMessage 'WHOIS', null, JSON.stringify info 110 | if atom.config.get 'irc.showJoinMessages' 111 | @client 112 | .on 'join', (channel, who) => 113 | @ircView?.addMessage 'JOINED', null, who + ' has joined ' + channel 114 | .on 'quit', (who, reason) => 115 | @ircView?.addMessage 'QUIT', null, who + ' has quit [' + reason + ']' 116 | 117 | errorHandler: (message) -> 118 | @ircStatusView.removeClass().addClass 'error' 119 | @client.disconnect() if @client 120 | console.error 'IRC Error: ' + message.args.join ' ' if message.args and atom.config.get('irc.debug') 121 | console.error 'IRC Error: ' + message if typeof message is String 122 | 123 | consumeStatusBar: (statusBar) -> 124 | IrcStatusView = require './irc-status-view' 125 | @ircStatusView ?= new IrcStatusView() 126 | statusBar.addLeftTile(item: @ircStatusView, priority: 150) 127 | -------------------------------------------------------------------------------- /menus/irc.cson: -------------------------------------------------------------------------------- 1 | 'menu': [ 2 | { 3 | 'label': 'Packages' 4 | 'submenu': [ 5 | 'label': 'Irc' 6 | 'submenu': [ 7 | { 'label': 'Open', 'command': 'irc:toggle' } 8 | { 'label': 'Connect', 'command': 'irc:connect' } 9 | { 'label': 'Disconnect', 'command': 'irc:disconnect' } 10 | ] 11 | ] 12 | } 13 | ] 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "irc", 3 | "main": "./lib/irc", 4 | "version": "0.4.0", 5 | "private": true, 6 | "description": "IRC client for Atom.io.", 7 | "repository": "https://github.com/cjsaylor/atom-irc", 8 | "license": "MIT", 9 | "engines": { 10 | "atom": ">=0.188.0" 11 | }, 12 | "dependencies": { 13 | "atom-space-pen-views": "^2.0.3", 14 | "irc": "0.3.7", 15 | "autolinker": "0.17.1" 16 | }, 17 | "consumedServices": { 18 | "status-bar": { 19 | "versions": { 20 | "^1.0.0": "consumeStatusBar" 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cjsaylor/atom-irc/bcb2d2a11e6cc6ad004ca0a8b0bf5b2610e4dec4/screenshot.png -------------------------------------------------------------------------------- /spec/irc-spec.coffee: -------------------------------------------------------------------------------- 1 | Irc = require '../lib/irc' 2 | 3 | # Use the command `window:run-package-specs` (cmd-alt-ctrl-p) to run specs. 4 | # 5 | # To run a specific `it` or `describe` block add an `f` to the front (e.g. `fit` 6 | # or `fdescribe`). Remove the `f` to unfocus the block. 7 | 8 | describe "Irc", -> 9 | activationPromise = null 10 | 11 | beforeEach -> 12 | atom.workspaceView = new WorkspaceView 13 | activationPromise = atom.packages.activatePackage('irc') 14 | 15 | describe "when the irc:toggle event is triggered", -> 16 | it "attaches and then detaches the view", -> 17 | expect(atom.workspaceView.find('.irc')).not.toExist() 18 | 19 | # This is an activation event, triggering it will cause the package to be 20 | # activated. 21 | atom.workspaceView.trigger 'irc:toggle' 22 | 23 | waitsForPromise -> 24 | activationPromise 25 | 26 | runs -> 27 | expect(atom.workspaceView.find('.irc')).toExist() 28 | atom.workspaceView.trigger 'irc:toggle' 29 | expect(atom.workspaceView.find('.irc')).not.toExist() 30 | -------------------------------------------------------------------------------- /spec/irc-view-spec.coffee: -------------------------------------------------------------------------------- 1 | IrcView = require '../lib/irc-view' 2 | {WorkspaceView} = require 'atom' 3 | 4 | describe "IrcView", -> 5 | it "has one valid test", -> 6 | expect("life").toBe "easy" 7 | -------------------------------------------------------------------------------- /styles/irc.less: -------------------------------------------------------------------------------- 1 | @import "ui-variables"; 2 | 3 | span#irc-status > a { color: @text-color-subtle; } 4 | span#irc-status.connected > a { color: @text-color; } 5 | span#irc-status.error > a { color: @text-color-error; } 6 | span#irc-status.notify > a { color: @text-color-warning; } 7 | 8 | .irc { 9 | color: @text-color; 10 | background-color: @app-background-color; 11 | .irc-output { 12 | p { margin-bottom: 2px; } 13 | p.pm { color: @text-color-success; } 14 | p.whois { color: @text-color-warning; } 15 | p.connected { color: @text-color-success; } 16 | p.disconnected { color: @text-color-error; } 17 | p.joined { color: @text-color-info; } 18 | p.quit { color: @text-color-error; } 19 | position: absolute; 20 | top: 0; 21 | bottom: 60px; 22 | padding: 5px 12px 0 12px; 23 | width: 100%; 24 | overflow: auto; 25 | 26 | .ts::before { 27 | content: "[" 28 | } 29 | .ts::after { 30 | content: "]" 31 | } 32 | .un::before { 33 | content: "<" 34 | } 35 | .un::after { 36 | content: ">" 37 | } 38 | } 39 | box-sizing: border-box; 40 | ::-webkit-input-placeholder { 41 | color: @text-color-subtle; 42 | } 43 | button { 44 | display: inline; 45 | margin-left: 15px; 46 | } 47 | input[type=text] { 48 | position: absolute; 49 | bottom: 10px; 50 | width: 96%; 51 | background-color: lighten(@input-background-color, 5%); 52 | border: 1px solid @input-border-color; 53 | color: @text-color; 54 | padding: @component-padding / 3 @component-padding / 2; 55 | margin: 10px; 56 | } 57 | } 58 | --------------------------------------------------------------------------------