]> cgit.babelmonkeys.de Git - jubjub.git/commitdiff
Add a zxcv-like CLI frontend
authorFlorian Zeitz <florob@babelmonkeys.de>
Sun, 24 Feb 2013 01:13:49 +0000 (02:13 +0100)
committerFlorian Zeitz <florob@babelmonkeys.de>
Sun, 24 Feb 2013 01:13:49 +0000 (02:13 +0100)
15 files changed:
config.xml.example
src/Makefile
src/core/JubChatClient.m
src/core/JubConfig.h
src/core/JubConfig.m
src/core/Makefile
src/core/main.m
src/gui/Makefile
src/gui/cli/JubCLIChatUI.h [new file with mode: 0644]
src/gui/cli/JubCLIChatUI.m [new file with mode: 0644]
src/gui/cli/JubCLIColor.h [new file with mode: 0644]
src/gui/cli/JubCLICommand.h [new file with mode: 0644]
src/gui/cli/JubCLIUI.h [new file with mode: 0644]
src/gui/cli/JubCLIUI.m [new file with mode: 0644]
src/gui/cli/Makefile [new file with mode: 0644]

index 714054e6a28e894d96fc64d728fa2c7bc8931a6b..568b69e9bc8b19f14abc265e2eac6269cba44fa8 100644 (file)
@@ -1,4 +1,5 @@
 <config xmlns="http://babelmonkeys.de/jubjub/config">
+       <frontend>gtk</frontend>
        <domain>localhost</domain>
        <!--<server>127.0.0.1</server>-->
        <username>alice</username>
index 4a8896cf8a43278eafcabadc918195ba23bd2ff8..a76fa665b57c739040f7257323a9f64904831381 100644 (file)
@@ -1,7 +1,7 @@
 SUBDIRS = core gui
 
 PROG = jubjub${PROG_SUFFIX}
-OBJS_EXTRA = core/core.a gui/gtk/gtk.a
+OBJS_EXTRA = core/core.a gui/gtk/gtk.a gui/cli/cli.a
 
 include ../buildsys.mk
 
index 77a4791541806bff07e079e3ecf01b7f4f26a1ae..b46c14cf8fa3def40723d60a671770961a1e437f 100644 (file)
@@ -31,8 +31,6 @@
 
                _streamManagement = [[XMPPStreamManagement alloc]
                    initWithConnection: _connection];
-
-               [_connection asyncConnectAndHandle];
        } @catch (id e) {
                [self release];
                @throw e;
index ab4b0bb187a005188b9652ae7c63a6522cae9065..c88182906d090a9d3d54235c0c399cb649ee7a00 100644 (file)
@@ -7,10 +7,12 @@
        OFString *_domain, *_server;
        OFString *_username;
        OFString *_password;
+       OFString *_frontend;
 }
 @property (readonly) OFString *domain, *server;
 @property (readonly) OFString *username;
 @property (readonly) OFString *password;
+@property (readonly) OFString *frontend;
 
 - initWithFile: (OFString*)file;
 @end
index b8a5f5a3f3652d1ba489d45493c8ec261cc4ca3f..fd92233ce5ff22b8c0a6fac2889f5c055be46523 100644 (file)
@@ -5,6 +5,7 @@
 @synthesize server = _server;;
 @synthesize username = _username;
 @synthesize password = _password;
+@synthesize frontend = _frontend;
 
 - initWithFile: (OFString*)file
 {
                _password = [[[element
                    elementForName: @"password"
                         namespace: CONFIG_NS] stringValue] copy];
+               _frontend = [[[element
+                   elementForName: @"frontend"
+                        namespace: CONFIG_NS] stringValue] copy];
+
 
                [pool release];
        } @catch (id e) {
@@ -51,6 +56,7 @@
        [_server release];
        [_username release];
        [_password release];
+       [_frontend release];
 
        [super dealloc];
 }
index 34b97e15e41702caba4effdb27891c3b7f0ac6e5..9b1d30392af704820d911837080236014b6d22f7 100644 (file)
@@ -5,4 +5,4 @@ SRCS = main.m           \
 
 include ../../buildsys.mk
 
-CPPFLAGS += -I../gui/common -I../gui/gtk
+CPPFLAGS += -I../gui/common -I../gui/gtk -I../gui/cli
index 459a15c9a898f0ab126b50aad6a81019a97e8a46..fda8c5f96b5d59b229afed59b7f745fe9b75eb05 100644 (file)
@@ -2,6 +2,7 @@
 #import <ObjXMPP/ObjXMPP.h>
 
 #import "JubGtkUI.h"
+#import "JubCLIUI.h"
 #import "JubConfig.h"
 #import "JubChatClient.h"
 
@@ -22,11 +23,21 @@ OF_APPLICATION_DELEGATE(AppDelegate)
 
        _client = [[JubChatClient alloc] initWithConfig: config];
 
-       _ui = [[JubGtkUI alloc] initWithClient: _client];
+       if ([config.frontend isEqual: @"gtk"])
+               _ui = [[JubGtkUI alloc] initWithClient: _client];
+       else if ([config.frontend isEqual: @"cli"])
+               _ui = [[JubCLIUI alloc] initWithClient: _client];
+       else {
+               [of_stderr writeFormat: @"Unknown frontend '%@', known "
+                   @"frontends are 'gtk' and 'cli'\n", config.frontend];
+               [OFApplication terminate];
+       }
 
        _client.ui = _ui;
        [_client.connection addDelegate: self];
 
+       [_client.connection asyncConnectAndHandle];
+
        [_ui startUIThread];
 }
 
index a658a454ce8d3a7a0f9e9bc4144281b259d497a9..2d3e9e6948238c7c2ae47aa2d2fa5fe8cfd2246a 100644 (file)
@@ -1,3 +1,3 @@
-SUBDIRS = gtk
+SUBDIRS = gtk cli
 
 include ../../buildsys.mk
diff --git a/src/gui/cli/JubCLIChatUI.h b/src/gui/cli/JubCLIChatUI.h
new file mode 100644 (file)
index 0000000..c0f8874
--- /dev/null
@@ -0,0 +1,11 @@
+#import <ObjFW/ObjFW.h>
+
+#import "JubChatUI.h"
+
+@interface JubCLIChatUI: OFObject <JubChatUI>
+{
+       jub_send_block_t _sendBlock;
+}
+
+- (void)send: (OFString*)text;
+@end
diff --git a/src/gui/cli/JubCLIChatUI.m b/src/gui/cli/JubCLIChatUI.m
new file mode 100644 (file)
index 0000000..5803ba7
--- /dev/null
@@ -0,0 +1,37 @@
+#import "JubCLIChatUI.h"
+#import "JubCLIColor.h"
+
+@implementation JubCLIChatUI
+- initWithTitle: (OFString*)title
+     closeBlock: (jub_close_block_t)closeBlock
+      sendBlock: (jub_send_block_t)sendBlock
+{
+       self = [super init];
+
+       @try {
+               _sendBlock = [sendBlock copy];
+       } @catch (id e) {
+               [self release];
+               @throw e;
+       }
+
+       return self;
+}
+
+- (void)dealloc
+{
+       [_sendBlock release];
+       [super dealloc];
+}
+
+- (void)addMessage: (OFString*)text
+           sender: (OFString*)sender
+{
+       [of_stdout writeFormat: BOLD("%@:") @" %@\n", sender, text];
+}
+
+- (void)send: (OFString*)text
+{
+       _sendBlock(text);
+}
+@end
diff --git a/src/gui/cli/JubCLIColor.h b/src/gui/cli/JubCLIColor.h
new file mode 100644 (file)
index 0000000..1471969
--- /dev/null
@@ -0,0 +1,8 @@
+#define BOLD(text) @"\033[1m" text "\033[0m"
+
+#define COL_CHAT(text) @"\033[32;1m" text @"\033[0m"
+#define COL_ONLINE(text) @"\033[32m" text @"\033[0m"
+#define COL_AWAY(text) @"\033[33;1m" text @"\033[0m"
+#define COL_XA(text) @"\033[34;1m" text @"\033[0m"
+#define COL_DND(text) @"\033[35;1m" text @"\033[0m"
+#define COL_OFFLINE(text) @"\033[31m" text @"\033[0m"
diff --git a/src/gui/cli/JubCLICommand.h b/src/gui/cli/JubCLICommand.h
new file mode 100644 (file)
index 0000000..6f32796
--- /dev/null
@@ -0,0 +1,37 @@
+#import <ObjFW/ObjFW.h>
+
+@class JubCLIUI;
+
+@protocol JubCLICommand
+@property (readonly) OFString *command;
+@property (readonly) OFString *params;
+@property (readonly) OFString *help;
+
+- initWithCLIUI: (JubCLIUI*)ui;
+- (void)callWithParameters: (OFArray*)parameters;
+@end
+
+#define BEGINCLICOMMAND(name, com, paramsString, helpString) \
+    @interface name: OFObject<JubCLICommand> \
+    { \
+       JubCLIUI *_ui; \
+    } \
+    @end \
+    @implementation name \
+    - initWithCLIUI: (JubCLIUI*)ui \
+    { \
+       self = [super init]; \
+       _ui = [ui retain]; \
+       return self; \
+    } \
+    - (void)dealloc \
+    { \
+       [_ui release]; \
+       [super dealloc]; \
+    } \
+    - (OFString*)command { return com; } \
+    - (OFString*)params { return paramsString; } \
+    - (OFString*)help { return helpString; } \
+    - (void)callWithParameters: (OFArray*)parameters
+
+#define ENDCLICOMMAND @end
diff --git a/src/gui/cli/JubCLIUI.h b/src/gui/cli/JubCLIUI.h
new file mode 100644 (file)
index 0000000..b610d0a
--- /dev/null
@@ -0,0 +1,26 @@
+#import <ObjFW/ObjFW.h>
+#import <ObjXMPP/ObjXMPP.h>
+
+#import "JubUI.h"
+
+@class JubCLIChatUI;
+@class JubChatClient;
+@protocol JubCLICommand;
+
+@interface JubCLIUI: OFObject <JubUI, XMPPContactManagerDelegate>
+{
+       XMPPContact *_lastIn;
+       JubCLIChatUI *_sink;
+       JubChatClient *_client;
+       XMPPContactManager *_contactManager;
+       OFMutableDictionary *_commands;
+}
+@property (readonly) JubChatClient *client;
+@property (readonly) XMPPContact *lastIn;
+@property (retain) JubCLIChatUI *sink;
+
+- (BOOL)Jub_userInputWithStream: (OFStream*)stream
+                          line: (OFString*)line
+                     exception: (OFException*)exception;
+- (void)addCommand: (id<JubCLICommand>)command;
+@end
diff --git a/src/gui/cli/JubCLIUI.m b/src/gui/cli/JubCLIUI.m
new file mode 100644 (file)
index 0000000..e44a3f3
--- /dev/null
@@ -0,0 +1,319 @@
+#import "JubCLIChatUI.h"
+#import "JubChatClient.h"
+#import "JubCLIColor.h"
+#import "JubCLICommand.h"
+#import "JubCLIUI.h"
+
+BEGINCLICOMMAND(JubCLIReplyCommand, @":r", nil,
+    @"Sets the sender of the last incomming message as the default recipient")
+{
+       if (_ui.lastIn == nil) {
+               [of_stdout writeLine: @"No message has been received yet"];
+               return;
+       }
+
+       _ui.sink = (JubCLIChatUI*)[_ui.client chatForContact: _ui.lastIn];
+       [of_stdout writeFormat: @"Set sink to %@\n",
+           [_ui.lastIn.rosterItem.JID bareJID]];
+}
+ENDCLICOMMAND
+
+BEGINCLICOMMAND(JubCLISetSinkCommand, @":s", @"<who>",
+    @"Selects <who> as the default recipient")
+{
+       if ([parameters count] != 1) {
+               [of_stdout writeLine: @"Syntax: ':s <who>'"];
+               return;
+       }
+
+       OFString *param = parameters[0];
+       XMPPContact *contact = _ui.client.contactManager.contacts[param];
+
+       if (contact == nil) {
+               [of_stdout writeFormat: @"Contact '%@' not found in your "
+                   @"roster\n", param];
+               return;
+       }
+
+       _ui.sink = (JubCLIChatUI*)[_ui.client chatForContact: contact];
+
+       [of_stdout writeFormat: @"Set sink to %@\n", param];
+}
+ENDCLICOMMAND
+
+BEGINCLICOMMAND(JubCLIMessageCommand, @":m", @"<who> <message>",
+    @"Sends a single message to <who>")
+{
+       if ([parameters count] < 2) {
+               [of_stdout writeLine: @"Syntax: ':m <who> <message>'"];
+               return;
+       }
+
+       XMPPContact *contact =
+           _ui.client.contactManager.contacts[parameters[0]];
+
+       if (contact == nil) {
+               [of_stdout writeFormat: @"Contact %@ not found in your "
+                   @"roster\n", parameters[0]];
+               return;
+       }
+
+       JubCLIChatUI *chat =
+           (JubCLIChatUI*)[_ui.client chatForContact: contact];
+
+       OFArray *message =
+           [parameters arrayByRemovingObject: [parameters firstObject]];
+
+       [chat send: [message componentsJoinedByString: @" "]];
+}
+ENDCLICOMMAND
+
+BEGINCLICOMMAND(JubCLIPresenceCommand, @":t", @"<status> [<message>]",
+    @"Changes your presence")
+{
+       if ([parameters count] < 1) {
+               [of_stdout writeLine:
+                   @"Syntax: ':t <status> [<message>]'"];
+               return;
+       }
+
+       XMPPPresence *presence;
+       OFString *show = parameters[0];
+
+       if (![@[ @"available", @"away", @"dnd", @"xa", @"chat", @"unavailable" ]
+           containsObject: show]) {
+               [of_stdout writeLine: @"<status> must be one of:"
+                   @"available, away, dnd, xa, chat, unavailable"];
+               return;
+       }
+
+       if ([show isEqual: @"unavailable"])
+               presence = [XMPPPresence presenceWithType: show];
+       else
+               presence = [XMPPPresence presence];
+
+       if (![@[ @"available", @"unavailable" ] containsObject: show])
+               presence.show = show;
+
+       if ([parameters count] == 2) {
+               [_ui.client.connection sendStanza: presence];
+               return;
+       }
+
+       OFArray *message =
+           [parameters arrayByRemovingObject: [parameters firstObject]];
+       presence.status = [message componentsJoinedByString: @" "];
+
+       [_ui.client.connection sendStanza: presence];
+}
+ENDCLICOMMAND
+
+BEGINCLICOMMAND(JubCLIRosterCommand, @":roster", nil, @"Shows your roster")
+{
+       OFDictionary *contacts = _ui.client.contactManager.contacts;
+       for (OFString *key in contacts) {
+               XMPPContact *contact = contacts[key];
+               OFString *name = contact.rosterItem.name;
+               XMPPPresence *presence =
+                   [[[contact.presences allObjects] sortedArray] firstObject];
+
+               if (name != nil)
+                       [of_stdout writeFormat: @"%@ <%@> (", name, key];
+               else
+                       [of_stdout writeFormat: @"%@ (", key];
+
+               if (presence == nil)
+                       [of_stdout writeFormat: COL_OFFLINE(@"offline")];
+               else if ([presence.show isEqual: @"chat"])
+                       [of_stdout writeFormat: COL_CHAT(@"free for chat")];
+               else if ([presence.show isEqual: @"away"])
+                       [of_stdout writeFormat: COL_AWAY(@"away")];
+               else if ([presence.show isEqual: @"xa"])
+                       [of_stdout writeFormat: COL_XA(@"extended away")];
+               else if ([presence.show isEqual: @"dnd"])
+                       [of_stdout writeFormat: COL_DND(@"do not disturb")];
+               else
+                       [of_stdout writeFormat: COL_ONLINE(@"online")];
+
+               [of_stdout writeString: @")\n"];
+       }
+}
+ENDCLICOMMAND
+
+@implementation JubCLIUI
+@synthesize client = _client;
+@synthesize lastIn = _lastIn;
+@synthesize sink = _sink;
+
+- initWithClient: (JubChatClient*)client
+{
+       self = [super init];
+
+       @try {
+               _commands =  [OFMutableDictionary new];
+               _client = [client retain];
+               _contactManager = client.contactManager;
+               [_contactManager addDelegate: self];
+
+               [self addCommand: [[[JubCLIReplyCommand alloc]
+                                      initWithCLIUI: self] autorelease]];
+
+               [self addCommand: [[[JubCLISetSinkCommand alloc]
+                                      initWithCLIUI: self] autorelease]];
+
+               [self addCommand: [[[JubCLIMessageCommand alloc]
+                                      initWithCLIUI: self] autorelease]];
+
+               [self addCommand: [[[JubCLIPresenceCommand alloc]
+                                      initWithCLIUI: self] autorelease]];
+
+               [self addCommand: [[[JubCLIRosterCommand alloc]
+                                      initWithCLIUI: self] autorelease]];
+       } @catch (id e) {
+               [self release];
+               @throw e;
+       }
+
+       return self;
+}
+
+- (void)dealloc
+{
+       [_contactManager removeDelegate: self];
+       [_client release];
+       [_commands release];
+       [super dealloc];
+}
+
+- (void)startUIThread
+{
+       [of_stdin asyncReadLineWithTarget: self
+                                selector: @selector(Jub_userInputWithStream:
+                                                    line:exception:)];
+}
+
+-      (void)client: (JubChatClient*)client
+  didChangePresence: (XMPPPresence*)presence
+{
+}
+
+- (Class)chatUIClass
+{
+       return [JubCLIChatUI class];
+}
+
+- (void)addCommand: (id<JubCLICommand>)command
+{
+       [_commands setObject: command
+                     forKey: command.command];
+}
+
+- (BOOL)Jub_userInputWithStream: (OFStream*)stream
+                          line: (OFString*)line
+                     exception: (OFException*)exception
+{
+       if (line == nil)
+               [OFApplication terminate];
+
+       if ([line length] == 0)
+               return YES;
+
+       if ([line characterAtIndex: 0] != ':') {
+               if (_sink == nil)
+                       [of_stdout writeLine: @"No default sink selected, "
+                           @"type `:h` for help"];
+
+               [_sink send: line];
+
+               return YES;
+       }
+
+       line = [line stringByDeletingTrailingWhitespaces];
+
+       OFArray *input= [line componentsSeparatedByString: @" "];
+
+       if ([input[0] isEqual: @":h"]) {
+               __block size_t longest = 0;
+
+               [_commands enumerateKeysAndObjectsUsingBlock:
+                   ^(OFString *key, id<JubCLICommand> command, BOOL *stop) {
+                       size_t length = [command.command length] +
+                           (command.params == nil ? 0 :
+                               (1 + [command.params length]));
+
+                       if (length > longest)
+                               longest = length;
+               }];
+
+               for (OFString *key in [[_commands allKeys] sortedArray]) {
+                       id<JubCLICommand> command = _commands[key];
+                       size_t length = [command.command length] +
+                           (command.params == nil ? 0 :
+                               (1 + [command.params length]));
+
+                       if (command.params == nil)
+                               [of_stdout writeFormat: @"`%@`",
+                                   command.command];
+                       else
+                               [of_stdout writeFormat: @"`%@ %@`",
+                                   command.command, command.params];
+
+                       // This is NOT distributive due to integer arithmetic
+                       size_t padding = (longest + 2)/8 - (length + 2)/8;
+
+                       for (size_t i = 0; i <= padding; i++)
+                               [of_stdout writeString: @"\t"];
+
+                       [of_stdout writeFormat: @"- %@\n", command.help];
+               };
+
+               return YES;
+       }
+
+       id<JubCLICommand> command = _commands[input[0]];
+
+       if (command) {
+               [command callWithParameters:
+                   [input arrayByRemovingObject: [input firstObject]]];
+
+               return YES;
+       }
+
+       [of_stdout writeLine: @"Invalid command, type `:h` for help"];
+
+       return YES;
+}
+
+-  (void)contact: (XMPPContact*)contact
+  didSendMessage: (XMPPMessage*)message
+{
+       if (message.body == nil || ![message.type isEqual: @"chat"])
+               return;
+
+       _lastIn =  contact;
+}
+
+-   (void)contact: (XMPPContact*)contact
+  didSendPresence: (XMPPPresence*)presence
+{
+       [of_stdout writeFormat: BOLD("%@") @" is now in state ", presence.from];
+
+       if ([presence.type isEqual: @"unavailable"])
+               [of_stdout writeFormat: COL_OFFLINE(@"offline")];
+       else if ([presence.show isEqual: @"chat"])
+               [of_stdout writeFormat: COL_CHAT(@"free for chat")];
+       else if ([presence.show isEqual: @"away"])
+               [of_stdout writeFormat: COL_AWAY(@"away")];
+       else if ([presence.show isEqual: @"xa"])
+               [of_stdout writeFormat: COL_XA(@"extended away")];
+       else if ([presence.show isEqual: @"dnd"])
+               [of_stdout writeFormat: COL_DND(@"do not disturb")];
+       else
+               [of_stdout writeFormat: COL_ONLINE(@"online")];
+
+       if (presence.status != nil)
+               [of_stdout writeFormat: @": %@", presence.status];
+
+       [of_stdout writeString: @"\n"];
+}
+@end
diff --git a/src/gui/cli/Makefile b/src/gui/cli/Makefile
new file mode 100644 (file)
index 0000000..807a20a
--- /dev/null
@@ -0,0 +1,7 @@
+STATIC_LIB_NOINST = cli.a
+SRCS = JubCLIUI.m      \
+       JubCLIChatUI.m
+
+include ../../../buildsys.mk
+
+CPPFLAGS += -I../common -I../../core