From d4efe21c5972e934b33fd70d42a407124c8a746b Mon Sep 17 00:00:00 2001 From: Florian Zeitz Date: Sun, 24 Feb 2013 02:13:49 +0100 Subject: [PATCH] Add a zxcv-like CLI frontend --- config.xml.example | 1 + src/Makefile | 2 +- src/core/JubChatClient.m | 2 - src/core/JubConfig.h | 2 + src/core/JubConfig.m | 6 + src/core/Makefile | 2 +- src/core/main.m | 13 +- src/gui/Makefile | 2 +- src/gui/cli/JubCLIChatUI.h | 11 ++ src/gui/cli/JubCLIChatUI.m | 37 +++++ src/gui/cli/JubCLIColor.h | 8 + src/gui/cli/JubCLICommand.h | 37 +++++ src/gui/cli/JubCLIUI.h | 26 +++ src/gui/cli/JubCLIUI.m | 319 ++++++++++++++++++++++++++++++++++++ src/gui/cli/Makefile | 7 + 15 files changed, 469 insertions(+), 6 deletions(-) create mode 100644 src/gui/cli/JubCLIChatUI.h create mode 100644 src/gui/cli/JubCLIChatUI.m create mode 100644 src/gui/cli/JubCLIColor.h create mode 100644 src/gui/cli/JubCLICommand.h create mode 100644 src/gui/cli/JubCLIUI.h create mode 100644 src/gui/cli/JubCLIUI.m create mode 100644 src/gui/cli/Makefile diff --git a/config.xml.example b/config.xml.example index 714054e..568b69e 100644 --- a/config.xml.example +++ b/config.xml.example @@ -1,4 +1,5 @@ + gtk localhost alice diff --git a/src/Makefile b/src/Makefile index 4a8896c..a76fa66 100644 --- a/src/Makefile +++ b/src/Makefile @@ -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 diff --git a/src/core/JubChatClient.m b/src/core/JubChatClient.m index 77a4791..b46c14c 100644 --- a/src/core/JubChatClient.m +++ b/src/core/JubChatClient.m @@ -31,8 +31,6 @@ _streamManagement = [[XMPPStreamManagement alloc] initWithConnection: _connection]; - - [_connection asyncConnectAndHandle]; } @catch (id e) { [self release]; @throw e; diff --git a/src/core/JubConfig.h b/src/core/JubConfig.h index ab4b0bb..c881829 100644 --- a/src/core/JubConfig.h +++ b/src/core/JubConfig.h @@ -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 diff --git a/src/core/JubConfig.m b/src/core/JubConfig.m index b8a5f5a..fd92233 100644 --- a/src/core/JubConfig.m +++ b/src/core/JubConfig.m @@ -5,6 +5,7 @@ @synthesize server = _server;; @synthesize username = _username; @synthesize password = _password; +@synthesize frontend = _frontend; - initWithFile: (OFString*)file { @@ -35,6 +36,10 @@ _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]; } diff --git a/src/core/Makefile b/src/core/Makefile index 34b97e1..9b1d303 100644 --- a/src/core/Makefile +++ b/src/core/Makefile @@ -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 diff --git a/src/core/main.m b/src/core/main.m index 459a15c..fda8c5f 100644 --- a/src/core/main.m +++ b/src/core/main.m @@ -2,6 +2,7 @@ #import #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]; } diff --git a/src/gui/Makefile b/src/gui/Makefile index a658a45..2d3e9e6 100644 --- a/src/gui/Makefile +++ b/src/gui/Makefile @@ -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 index 0000000..c0f8874 --- /dev/null +++ b/src/gui/cli/JubCLIChatUI.h @@ -0,0 +1,11 @@ +#import + +#import "JubChatUI.h" + +@interface JubCLIChatUI: OFObject +{ + 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 index 0000000..5803ba7 --- /dev/null +++ b/src/gui/cli/JubCLIChatUI.m @@ -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 index 0000000..1471969 --- /dev/null +++ b/src/gui/cli/JubCLIColor.h @@ -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 index 0000000..6f32796 --- /dev/null +++ b/src/gui/cli/JubCLICommand.h @@ -0,0 +1,37 @@ +#import + +@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 \ + { \ + 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 index 0000000..b610d0a --- /dev/null +++ b/src/gui/cli/JubCLIUI.h @@ -0,0 +1,26 @@ +#import +#import + +#import "JubUI.h" + +@class JubCLIChatUI; +@class JubChatClient; +@protocol JubCLICommand; + +@interface JubCLIUI: OFObject +{ + 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)command; +@end diff --git a/src/gui/cli/JubCLIUI.m b/src/gui/cli/JubCLIUI.m new file mode 100644 index 0000000..e44a3f3 --- /dev/null +++ b/src/gui/cli/JubCLIUI.m @@ -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", @"", + @"Selects as the default recipient") +{ + if ([parameters count] != 1) { + [of_stdout writeLine: @"Syntax: ':s '"]; + 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", @" ", + @"Sends a single message to ") +{ + if ([parameters count] < 2) { + [of_stdout writeLine: @"Syntax: ':m '"]; + 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", @" []", + @"Changes your presence") +{ + if ([parameters count] < 1) { + [of_stdout writeLine: + @"Syntax: ':t []'"]; + return; + } + + XMPPPresence *presence; + OFString *show = parameters[0]; + + if (![@[ @"available", @"away", @"dnd", @"xa", @"chat", @"unavailable" ] + containsObject: show]) { + [of_stdout writeLine: @" 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)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 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 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 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 index 0000000..807a20a --- /dev/null +++ b/src/gui/cli/Makefile @@ -0,0 +1,7 @@ +STATIC_LIB_NOINST = cli.a +SRCS = JubCLIUI.m \ + JubCLIChatUI.m + +include ../../../buildsys.mk + +CPPFLAGS += -I../common -I../../core -- 2.39.5