From 2eb645572887b1a6ba0d0bf9a3c2077a7aa46246 Mon Sep 17 00:00:00 2001 From: rbraun <> Date: Mon, 24 Mar 2003 12:56:55 +0000 Subject: [PATCH] linemode command history --- macssh/source/Headers/Preferences.h | 3 + macssh/source/Linemode/linemode.c | 290 +++++++++++++++++++++--- macssh/source/Linemode/linemode.proto.h | 9 +- macssh/source/Screens/wind.h | 25 +- macssh/source/ae/ae.c | 2 +- macssh/source/config/configure.c | 15 +- macssh/source/main/Connections.c | 4 +- macssh/source/main/event.c | 23 +- macssh/source/parse/parse.c | 3 +- macssh/source/telnet.rsrc | Bin 165069 -> 165003 bytes 10 files changed, 331 insertions(+), 43 deletions(-) diff --git a/macssh/source/Headers/Preferences.h b/macssh/source/Headers/Preferences.h index 95a7539..0a368a8 100755 --- a/macssh/source/Headers/Preferences.h +++ b/macssh/source/Headers/Preferences.h @@ -278,6 +278,9 @@ typedef struct { cacheDelay; // delay in seconds before passphrase expires /* NONO */ + short + commandHistoryLines; // RAB MacSSH 2.2a1 + short padding[96]; // Memory is getting cheap too.... (used to be [96]) } ApplicationPrefs; diff --git a/macssh/source/Linemode/linemode.c b/macssh/source/Linemode/linemode.c index e80fd97..797ae5f 100755 --- a/macssh/source/Linemode/linemode.c +++ b/macssh/source/Linemode/linemode.c @@ -52,8 +52,12 @@ static unsigned char default_slc[] = static char munger[255]; #endif +#define ESCAPED(ascii) (!(ascii > 31 && ascii < 127)) + void LinemodeUnload(void) {} +/* every connection setup needs to call this now (2.2a1) since it's needed for plain echo + (aka kludge-linemode) and not just traditional linemode */ void initLinemode(struct WindRec *tw) { short temp; @@ -68,18 +72,170 @@ void initLinemode(struct WindRec *tw) tw->lmodeBits = 0; tw->litNext = 0; tw->lmode = 0; + /* RAB 2.2a1 - command line editing */ + tw->editPos = 0; + tw->commandHistory = 0; + tw->chEditPos = 0; + tw->commandHistoryLength = 0; } -void process_key(unsigned char ascii,struct WindRec *tw) +// of course there's a new function to dispose of the stuff... we don't like memory leaks! +void disposeHistory(WindRec *tw) { + CommandHistoryEntry *histptr = tw->commandHistory; + CommandHistoryEntry *tempptr; + + while (tw->commandHistoryLength) { + tempptr = histptr; + histptr = histptr->next; + DisposePtr((char *)tempptr); + tw->commandHistoryLength--; + } +} + +void addToHistory(WindRec *tw) +{ + CommandHistoryEntry *histptr; + short commandHistoryLines = gApplicationPrefs->commandHistoryLines; + + if (commandHistoryLines == -1) return; + if (!commandHistoryLines) + commandHistoryLines = DEFAULT_HISTORY_LINES; + + if (tw->commandHistoryLength >= commandHistoryLines) { // we're at the limit here... + histptr = tw->commandHistory; + histptr = histptr->next; // so re-use what we have + tw->commandHistory = histptr; + } else if (!tw->commandHistory) { // or maybe we haven't even started yet... + histptr = (CommandHistoryEntry *) myNewPtr(sizeof(CommandHistoryEntry)); + if (!histptr) return; // bad bad bad + histptr->next = histptr; + histptr->prev = histptr; + tw->commandHistory = histptr; + tw->commandHistoryLength = 1; + } else { + histptr = (CommandHistoryEntry *) myNewPtr(sizeof(CommandHistoryEntry)); + if (!histptr) return; // bad bad bad + histptr->next = tw->commandHistory->next; + histptr->prev = tw->commandHistory; + tw->commandHistory->next->prev = histptr; + tw->commandHistory->next = histptr; + tw->commandHistory = histptr; + tw->commandHistoryLength++; + } + + BlockMoveData(tw->kbbuf, tw->commandHistory->kbbuf, sizeof(tw->kbbuf)); + tw->commandHistory->kblen = tw->kblen; + tw->chEditPos = 0; +} + +void linemode_kbwrite(struct WindRec *tw, unsigned char ascii) { + if (tw->kblen < (MAXKB - 1)) { /* Add to buffer if not full */ + if (tw->editPos == tw->kblen + 1) tw->editPos = 0; + if (!tw->editPos) + tw->kbbuf[tw->kblen++] = ascii; + else { + BlockMoveData(&tw->kbbuf[tw->editPos-1], &tw->kbbuf[tw->editPos], + tw->kblen - tw->editPos + 1); + tw->kbbuf[tw->editPos-1] = ascii; + tw->kblen++; tw->editPos++; + } + } + else + { + kbflush(tw); + tw->kbbuf[0]=ascii; + tw->kblen=1; + } +} + +void delLinemodeChar(struct WindRec *tw) { + if (!tw->kblen) return; // sanity check + if (!tw->editPos) + tw->kblen--; + else { + if (tw->editPos == 1) return; // another sanity check + BlockMoveData(&tw->kbbuf[tw->editPos-1], &tw->kbbuf[tw->editPos-2], + tw->kblen - tw->editPos + 1); + tw->kblen--; tw->editPos--; + } +} + +void displayHistory(WindRec *tw) +{ + static char tempkbbuf[MAXKB*2+1]; + short kblen; + char *tempkbptr, *kbptr; + +// get rid of the old text first + while (tw->kblen >0) + { + if (tw->echo) { + if (tw->editPos && tw->editPos <= tw->kblen) { + parse(tw,(unsigned char *) "\033[P",3); + if (ESCAPED(tw->kbbuf[tw->editPos - 2])) + parse(tw,(unsigned char *) "\033[P",3); + } else { + parse(tw,(unsigned char *) "\010 \010",3); + if (ESCAPED(tw->kbbuf[tw->kblen - 1])) + parse(tw,(unsigned char *) "\010\033[P", 4); + } + } + tw->kblen--; + } + tw->editPos = 0; + +// now find the new text to put in + if (!tw->chEditPos) { + tw->kblen = tw->oldkblen; + BlockMove(tw->oldkbbuf, tw->kbbuf, sizeof(tw->kbbuf)); + } else { + BlockMove(tw->chEditPos->kbbuf, tw->kbbuf, sizeof(tw->kbbuf)); + tw->kblen = tw->chEditPos->kblen; + } + kblen = tw->kblen; + tempkbptr = tempkbbuf; + kbptr = tw->kbbuf; + +// now draw it - but first, escape controls + while (kblen) { + if (ESCAPED(*kbptr)) { + *tempkbptr = '^'; + tempkbptr++; + *tempkbptr = '@'+*kbptr; + tempkbptr++; + } else { + *tempkbptr = *kbptr; + tempkbptr++; + } + kbptr++; + kblen--; + } + parse(tw, (unsigned char *)tempkbbuf, tempkbptr-tempkbbuf); +} + +// NB: this function can now get called if linemode is OFF (for kludge-pseudo-linemode) +// so make sure no assumptions are made about whether linemode itself is on +// (don't send linemode subnegotations without checking tw->lmode, for example) +void process_key(unsigned char ascii, unsigned char keycode, struct WindRec *tw) +{ + if (!tw->lmode) tw->lmodeBits = L_EDIT; // edit, no trapsig if linemode is off +/* signals? what signals? if you are actually telnetted into a un*x box, you should + be using real linemode, or else. kludge-linemode is designed for other services. + we never did flush the keybuffer on ^Z, etc., and we never will, unless you're + using REAL linemode. So there. */ + if (tw->litNext) { //do no processing on next key tw->litNext = FALSE; - kbwrite(tw, &ascii, 1); + linemode_kbwrite(tw, ascii); + if (tw->editPos) tw->editPos++; if (tw->echo) { - if (ascii>31 && ascii <127) { + if (!ESCAPED(ascii)) { + if (tw->editPos) /* make some room if we're not at the end */ + parse(tw, (unsigned char*)"\033[@", 3); // insert char /* add these chars to buffer */ parse(tw, &ascii, 1); } @@ -87,6 +243,10 @@ void process_key(unsigned char ascii,struct WindRec *tw) } } + /* make sure editing keys don't get confused for control codes */ + if ((keycode == 0x75 || keycode == 0x7b || keycode == 0x7c) + && (tw->lmodeBits & L_EDIT)) ascii = 64; + if (tw->lmodeBits & 2) { // TRAPSIG mode active unsigned char toSend[2] = {IAC,0}; @@ -116,6 +276,7 @@ void process_key(unsigned char ascii,struct WindRec *tw) // if (tw->echo) // parse(tw, &ascii, 1); // echo if we should tw->kblen=0; //zero out the buffer + tw->editPos = 0; netpush(tw->port); netwrite(tw->port,toSend,2); //send IAC whatever if (tw->slcLevel[whichSignal] & SLC_FLUSHIN) @@ -139,12 +300,13 @@ void process_key(unsigned char ascii,struct WindRec *tw) if ((tw->lmodeBits & L_SOFT_TAB)&&(ascii == 0x09)) // SOFT_TAB mode active; expand tab into spaces { short numSpaces = VSIgetNextTabDistance(); - unsigned char spacechar = ' '; while (numSpaces-- > 0) { - kbwrite(tw, &spacechar, 1); + linemode_kbwrite(tw, ' '); } - if (tw->echo) + if (tw->echo) { + if (tw->editPos) parse(tw, (unsigned char*)"\033[@", 3); parse(tw, &ascii, 1); + } return; } @@ -152,10 +314,73 @@ void process_key(unsigned char ascii,struct WindRec *tw) if (tw->lmodeBits & L_EDIT) //handle editing functions { - + if (keycode == 0x7b) { // left arrow + if (tw->editPos == 1 || !tw->kblen) return; // blah + if (!tw->editPos) + tw->editPos = tw->kblen; + else tw->editPos--; + parse(tw, (unsigned char *)"\033[D", 3); + if (ESCAPED(tw->kbbuf[tw->editPos - 1])) + parse(tw, (unsigned char *)"\033[D", 3); + return; + } + + if (keycode == 0x7c) { // right arrow + short times = 1; + + if (!tw->editPos) return; // blah + if (ESCAPED(tw->kbbuf[tw->editPos - 1])) + times = 2; + tw->editPos++; + if (tw->editPos > tw->kblen) tw->editPos = 0; + parse(tw, (unsigned char *) "\033[C\033[C", times*3); + return; + } + if (keycode == 0x7e) { // up arrow + if (!tw->commandHistory) return; // blah + if (!tw->chEditPos) { + tw->chEditPos = tw->commandHistory; + tw->oldkblen = tw->kblen; + BlockMoveData(tw->kbbuf, tw->oldkbbuf, sizeof(tw->kbbuf)); + // save it + } else { + tw->chEditPos = tw->chEditPos->prev; + // we "insert" the entry defined by tw->chEditPos = 0 + // (ie, the current line) between the bottom and top of the chain + if (tw->chEditPos == tw->commandHistory) tw->chEditPos = 0; + } + displayHistory(tw); + return; + } + if (keycode == 0x7d) { // down arrow + if (!tw->commandHistory) return; + if (tw->chEditPos == tw->commandHistory) tw->chEditPos = 0; + else { + if (!tw->chEditPos) { + BlockMoveData(tw->kbbuf, tw->oldkbbuf, sizeof(tw->kbbuf)); // save it + tw->oldkblen = tw->kblen; + tw->chEditPos = tw->commandHistory; + } + tw->chEditPos = tw->chEditPos->next; + } + displayHistory(tw); + return; + } + if (keycode == 0x75) { // forward delete + if (!tw->editPos) return; // blah + tw->editPos++; + if (tw->editPos > tw->kblen) tw->editPos = 0; + parse(tw, (unsigned char *) "\033[P", 3); + if (ESCAPED(tw->kbbuf[tw->editPos ? tw->editPos - 2 : tw->kblen - 1])) + parse(tw,(unsigned char *) "\033[P",3); + delLinemodeChar(tw); + return; + } if (ascii == '\015') //CR { //since we are in edit, send the buffer and CR-LF + addToHistory(tw); /* since we're sending it, save it */ + tw->chEditPos = 0; kbflush(tw); netpush(tw->port); netwrite(tw->port,"\015\012",2); @@ -166,29 +391,34 @@ void process_key(unsigned char ascii,struct WindRec *tw) if (ascii == tw->slc[SLC_EC]) //kill the character { - if (tw->echo) - parse(tw,(unsigned char *) "\010 \010",3); - tw->kblen--; - return; - } - else if (ascii == tw->slc[SLC_AO]) //kill the line - { - while (tw->kblen >0) - { - if (tw->echo) - parse(tw,(unsigned char *) "\010 \010",3); - tw->kblen--; + if (tw->kblen && tw->editPos != 1) { + if (tw->echo) { + parse(tw,(unsigned char *) "\010\033[P", 4); + if (ESCAPED(tw->kbbuf[tw->editPos ? tw->editPos - 2 : tw->kblen - 1])) + parse(tw,(unsigned char *) "\010\033[P",4); + } + delLinemodeChar(tw); } return; } - else if (ascii == tw->slc[SLC_EL]) //kill the line + else if (ascii == tw->slc[SLC_AO] || ascii == tw->slc[SLC_EL]) //kill the line { while (tw->kblen >0) { - if (tw->echo) - parse(tw,(unsigned char *) "\010 \010",3); + if (tw->echo) { + if (tw->editPos && tw->editPos <= tw->kblen) { + parse(tw,(unsigned char *) "\033[P",3); + if (ESCAPED(tw->kbbuf[tw->editPos - 2])) + parse(tw,(unsigned char *) "\033[P",3); + } else { + parse(tw,(unsigned char *) "\010 \010",3); + if (ESCAPED(tw->kbbuf[tw->kblen - 1])) + parse(tw,(unsigned char *) "\010\033[P",4); + } + } tw->kblen--; } + tw->editPos = 0; return; } else if ((ascii == tw->slc[SLC_EOF]) && (tw->lmodeBits & 2)) @@ -224,8 +454,8 @@ void process_key(unsigned char ascii,struct WindRec *tw) while ((tw->kbbuf[tw->kblen-1] != 0x20)&&(tw->kblen >= 0)) //while its not a space { if (tw->echo) - parse(tw,(unsigned char *)"\010 \010",3); - tw->kblen--; + parse(tw,(unsigned char *)"\010\033[P",4); + delLinemodeChar(tw); } } else if (ascii == tw->slc[SLC_RP]) @@ -264,7 +494,7 @@ void process_key(unsigned char ascii,struct WindRec *tw) //ok, at this point, we are past all local editing functions. Now, add the character to the buffer. else { - kbwrite(tw, &ascii, 1); + linemode_kbwrite(tw, ascii); } } @@ -285,10 +515,10 @@ void process_key(unsigned char ascii,struct WindRec *tw) if (tw->echo) /* Handle local ECHOs */ { - if (ascii>31 && ascii <127) /* add these chars to buffer */ + if (!ESCAPED(ascii)) { /* add these chars to buffer */ parse(tw, &ascii, 1); - else /* not printable char */ - { + if (tw->editPos) parse(tw, (unsigned char *) "\033[@", 3); + } else { /* not printable char */ if (!(tw->lmodeBits & L_LIT_ECHO)) //don't echo if this is set { ascii='@'+ascii; @@ -595,8 +825,8 @@ void doLinemode(struct WindRec *tw) unsigned char subBeginSeq[4] = {IAC,TEL_SB,N_LINEMODE,L_SLC}; unsigned char subEndSeq[2] = {IAC,TEL_SE}; unsigned char toSend[3]; - - + + tw->editPos = 0; tw->lmodeBits = 0; tw->lmode = TRUE; tw->litNext = FALSE; diff --git a/macssh/source/Linemode/linemode.proto.h b/macssh/source/Linemode/linemode.proto.h index d7ee7aa..4089a1a 100755 --- a/macssh/source/Linemode/linemode.proto.h +++ b/macssh/source/Linemode/linemode.proto.h @@ -1,6 +1,11 @@ void linemode_suboption(struct WindRec *tw); void DemangleLineModeShort(char *s, short mode); void doLinemode(struct WindRec *tw); -void process_key(unsigned char ascii,struct WindRec *tw); +void process_key(unsigned char ascii, unsigned char keycode, struct WindRec *tw); void initLinemode(struct WindRec *tw); -void LinemodeUnload(void); \ No newline at end of file +void LinemodeUnload(void); +void disposeHistory(WindRec *tw); +void addToHistory(WindRec *tw); +void linemode_kbwrite(struct WindRec *tw, unsigned char ascii); +void delLinemodeChar(struct WindRec *tw); +void displayHistory(WindRec *tw); diff --git a/macssh/source/Screens/wind.h b/macssh/source/Screens/wind.h index a77841a..22d18d7 100755 --- a/macssh/source/Screens/wind.h +++ b/macssh/source/Screens/wind.h @@ -39,6 +39,17 @@ typedef struct sshinfo { void *context; } sshinfo; +typedef struct CommandHistoryEntry CommandHistoryEntry; + +struct CommandHistoryEntry { // a doubly linked list to make removing off the top easier + CommandHistoryEntry *prev; + CommandHistoryEntry *next; + char kbbuf[MAXKB]; + short kblen; +}; + +#define DEFAULT_HISTORY_LINES 10 + struct WindRec { short vs, // virtual screen number @@ -98,7 +109,8 @@ short realbold, ignoreBeeps, // ignore beeps: RAB BetterTelnet 1.0fc7 inversebold, // use inverse for bold: RAB BetterTelnet 1.0fc9 - kblen, // Pointer to next char in buffer to be used + kblen, // Pointer to next char in buffer to be used + oldkblen, // same thing if we're stuck in history clientflags, // BYU mod - boolean flags for ftp client jsNoFlush, // RAB BetterTelnet 2.0b4 - don't flush jump scroller at all realBlink; @@ -202,7 +214,10 @@ short lmodeBits, // Current linemode MODE. Currently support EDIT and TRAPSIG lmode, // linemode is active forwardMask, // should we forward on certain characters - numForwardKeys; // how many keys to forward on + numForwardKeys, // how many keys to forward on + editPos; /* if we're editing the text stream, location past the insertion pt; + otherwise, 0 */ + unsigned char slcLevel[SLC_ARRAY_SIZE+1], //levelBits forwardKeys[32],// which keys to forward on @@ -212,7 +227,8 @@ char TELstop, // Character for scrolling to stop TELgo, // Character for scrolling to go TELip, // Character for interrupt process - kbbuf[MAXKB]; // The keyboard buffer (echo mode ) + kbbuf[MAXKB], // The keyboard buffer (echo mode ) + oldkbbuf[MAXKB]; // the real keyboard buffer if we're stuck in history GrafPtr wind; @@ -239,6 +255,9 @@ Boolean NewMacroInfo sessmacros; +CommandHistoryEntry *commandHistory, *chEditPos; +short commandHistoryLength; + /* NONO */ short authentication, diff --git a/macssh/source/ae/ae.c b/macssh/source/ae/ae.c index 2dc326e..481b83d 100755 --- a/macssh/source/ae/ae.c +++ b/macssh/source/ae/ae.c @@ -189,7 +189,7 @@ pascal OSErr MyHandleSendCR (AppleEvent *theAppleEvent, AppleEvent* reply, long if (TelInfo->numwindows < 1) return noErr; if (screens[scrn].lmode) { - process_key('\015', &screens[scrn]); + process_key('\015', 0, &screens[scrn]); return noErr; } diff --git a/macssh/source/config/configure.c b/macssh/source/config/configure.c index 3fe5487..e812816 100755 --- a/macssh/source/config/configure.c +++ b/macssh/source/config/configure.c @@ -190,6 +190,14 @@ void Cenviron( void) NumToString(scratchlong,scratchPstring); SetTEText(dptr,PrefSendTime, scratchPstring); + if (gApplicationPrefs->commandHistoryLines != -1) { + if (!gApplicationPrefs->commandHistoryLines) + NumToString(DEFAULT_HISTORY_LINES, scratchPstring); + else + NumToString(gApplicationPrefs->commandHistoryLines, scratchPstring); + SetTEText(dptr, 44, scratchPstring); + } + ShowWindow(dptr); ditem=0; /* initially no hits */ while((ditem>2) || (ditem==0)) { @@ -308,7 +316,12 @@ void Cenviron( void) GetTEText(dptr,PrefSendTime, scratchPstring); StringToNum(scratchPstring, &scratchlong); gApplicationPrefs->SendTimeout = (short) scratchlong; - + + GetTEText(dptr, 44, scratchPstring); + StringToNum(scratchPstring, &scratchlong); + if (!scratchlong) gApplicationPrefs->commandHistoryLines = -1; + else gApplicationPrefs->commandHistoryLines = scratchlong; + GetTEText(dptr, PrefCaptTE, scratchPstring); BlockMoveData(&scratchPstring[1], &(gApplicationPrefs->CaptureFileCreator), sizeof(OSType)); GetTEText(dptr,PrefStaggerOffset, scratchPstring); diff --git a/macssh/source/main/Connections.c b/macssh/source/main/Connections.c index 7ee3673..462f385 100755 --- a/macssh/source/main/Connections.c +++ b/macssh/source/main/Connections.c @@ -802,7 +802,9 @@ Boolean CreateConnectionFromParams( ConnInitParams **Params) theScreen->lmode = 0; // RAB BetterTelnet 2.0b1 - fix for a really bizarre bug theScreen->lineAllow = SessPtr->linemode; - if (SessPtr->linemode) //we allow linemode + + /* Other stuff uses the linemode code now, so we need to set it up all the time... + if (SessPtr->linemode) //we allow linemode */ initLinemode(&screens[cur]); theScreen->launchurlesc = SessPtr->launchurlesc; diff --git a/macssh/source/main/event.c b/macssh/source/main/event.c index c2c503b..2eddb80 100755 --- a/macssh/source/main/event.c +++ b/macssh/source/main/event.c @@ -436,6 +436,14 @@ void HandleKeyDown(EventRecord theEvent,struct WindRec *tw) ObscureCursor(); +// command line editing for linemode is a special case here + if (((tw->lmode && (tw->lmodeBits & 1)) || + (tw->echo && !tw->halfdup)) && (code == 0x7b || code == 0x7c || code == 0x75 + || code == 0x7d || code == 0x7e)) { + process_key(ascii, code, tw); + return; + } + if ( tw->emacsmeta == 2 && optiondown ) { // option key as emacs meta key: keep shift and control translation ascii = translatekeycode( code, (theEvent.modifiers & (shiftKey|controlKey)) ); @@ -561,8 +569,11 @@ emacsHack: } if ( code == BScode ) { + // if linemode, give it what it wants + if ((tw->lmode || (tw->echo && !tw->halfdup)) && !optiondown) + ascii = tw->slc[10]; // handle mapping BS to DEL, flipping on option - if ( tw->bsdel ) { + else if ( tw->bsdel ) { if ( (optiondown && tw->emacsmeta != 2) || commanddown ) ascii = BS; else @@ -697,7 +708,7 @@ emacsHack: } else if ( tw->lmode ) { for (res = 0; res < trlen; ++res) { // Some form of linemode is active; we dont touch it after them - process_key( pbuf[res], tw ); + process_key( pbuf[res], 0, tw ); } return; } @@ -731,7 +742,8 @@ emacsHack: } if ( tw->echo && !tw->halfdup ) { - // Handle klude-linemode + // Handle kludge-linemode +#if 0 if ( ascii > 31 && ascii < 127 && code < KPlowest ) { // printable key kbwrite( tw, pbuf, trlen); @@ -758,7 +770,10 @@ emacsHack: parse( tw, &sendch, 1 ); } } - } //end if klude-linemode +#endif + process_key(ascii, code, tw); // let "linemode" handle this + return; + } //end if kludge-linemode if (ascii == '\015') { diff --git a/macssh/source/parse/parse.c b/macssh/source/parse/parse.c index ab54600..7ba8747 100755 --- a/macssh/source/parse/parse.c +++ b/macssh/source/parse/parse.c @@ -80,7 +80,8 @@ void kbflush(struct WindRec *tw) { if ( tw->kblen ) { netwrite( tw->port, tw->kbbuf, tw->kblen); - tw->kblen = 0; + tw->kblen = 0; + tw->editPos = 0; } } diff --git a/macssh/source/telnet.rsrc b/macssh/source/telnet.rsrc index f52a52f32e31bcb3135ef536c3aeeba5f89d9b00..2dfba35eecd2a64cfc8df74a34d7082dd39e73ed 100755 GIT binary patch delta 2676 zcmY+_dr(x@836F_eEYb2?=Fu8L}U?9SYSa(R3wpVUdT(zvaGvn5(JI7tYMYsE}|y3 zi;5_d#7CvvOl?LaHq%C}I?+HKYn>sc#2Tkqr?z9#Y1P(fJDu3^nKq^0IsTLGKi}_s z_ndRjp8MT%KkL`t?ALbBo&F>MsDQm|DRKY?fWacbRYlZ%w0E=0H@BT^V;E*>dzLsx zir27_O5DBxFgCo%Q|6|g zr%KDpD=2It|ZiCrpP>hptwAH~Y5C^6HJTIQ~N1;CVE=B}-Td{b^kt)~o(N@j(R z1{#%R6#;J)KpCtm^*BLMhO0_zJpdN0SybYq7+)iAU`ekj_bi3!mfV_PYZF-2oi)Ko zJAjtysqh5As?GFPl?K49o$~r9GHWc}8!2B6V9oW@%oGd#0UxIe&r@_liuEOb$lnIc z=JQ9J3ju6v0ySPYm~GtwpWjXS^MR1Bkm9wMt7yT@_H|>hYZX9TQe$vkgkol6s4GNq zW>DlUanA(9L7FdaI2egW0Aq2dn#6^)xIZLbxwB zR}=^XV|+GX5wHBt1;&7e;wD~xrLEliEO&`Sn$Jozgav@D?f zUh+Rcw^VbSmQ4N_gqr*wQ z3}cp)JE-M zLFp}`CUWQ;qu-#qhkQg@SBJ<~q=!`c$GOv_KTtm_A}igcFtEJs(4^lZ&d zu9d#4Ehn#&zNdY|F@L)BeXWV|?+Rm&)@el7Ir%TC5C%ExYY-Qx4sLPwR-g2wHI3>( zPNYm|J=y5`yT~UT(B7efvI|okjMG9N3Onj-85}+7(#tg3tvvBv0jh)b9Ou)wgxp}I zov44@XSrrNM4hVi7uMixHb-w?gQ=_Y=x_sYrEBAK=}l>eM!T9fBAuaKCx0Of-!;2A zYp$kOJO!%pB5llkGED6>XB*kh272U0CA*anps}7@Tr#9I0HQ*kqbIDzV@B$orjM<~ z$f~49F(G4YDnMVEG_q%mY>(U_W>D?`+CP+eqv01u*#0kJ1WSSulVyD%WA&7G$UL$} zTEVciLHWjrNt&*=pf7$FeH!Sib4e6p+>i7JE$C!U-O-9!xb=?yLMx``+FyM-x}JXztKuVG%Sb-^z z^jYorbZMP+j4DR%w__Q*#vyWDtez@nJ5|g|=?0sLDuzDxlwZ#N%^@l`!f{%;2^fDw zkF{gI@@wnwIeHiB@3mu53bhUbKKH&DqKsRA4p@F%cXXh0noFc$t93sho#5ivt1JLi z4(Zh$^a$QMtiRDgrI@TA>Odb3T+k;wa4MUjPwm8o%%uA}aSire(|^^8so2-2U+Kgz ziyd`h+iOmX*&Ox4h5wY6r;Wc`7zV`?Jh4ss44G}8Ir6&*E;PjWw{oe{6vYvGtY9}JVC&qTY(PuZlu z-Hiz@T#+Q2a`~)Ro@I{@%eieL?~==Co~@5{y;x_}Y8u0B+#3n4j_OBxk^c{vTY?t= delta 2834 zcmY+Ge^6A{701s#yX^b1yZjOqS=8(DW5pk!)J&{e69iErtYleh1T-$o8dibD1%I?- zx)>2cYmBYY*NIJ|jrNCW8f{F)snu3$Vy2A|<1}q*GukvNOlR6;Aa*9T^xT7gwENHb zyzf2l-22Y`-nZ{tedfD;=Kgt8VgO(OcE3hZ0tf&M*8(=y0!+_2u*;CU+TLsv7!^N0 zFqrnL71}3q&g_kfE1%eHGhybLcBXz6{wpOB{>jEkJ0AI@WG>jL7*zu3|Kmr8 z``<75K=J?n^l)>mwJ{orlj!V>w6sM!i{c&eu*&g-nwla_a&sioE;}MEk#J`;)+S@^ zD$wx%uMpf<_fLGBAC|2S)PjcR z%PZ-6rfq6@)$%6*lDy?rtCmAyQc=Y!e>vE-f{Fm$XV)4lf;Alg+V?BV{JEfMCo0QU z`2lpZdTD8Z;>2pMuY0SP`Bx`kie6M5Zfydmai}^Rr_P!M{tAB(oMuT)Wmym$=Ea%- zMd6%S6JNHG9=)iRDpOoo8w^O&D=6kdhI4&wq_z!&tFE@Axdgx!4_4Pyfy4EUV4${& z^lQOLpoHS^no3%*aAPRknP>!XXNSUDsj2RQP^2?Lu_PSsYy{oCBpeM>LHCI;wHeTT zu}SG!?%Pc*Pwjxzl=Nu0Wj;7kKBBk)9H}MIaBD6krS?#qPw@)H1(1|BF&gg31xMO? zij<%BbBdIo?udqqs9FflU@L^|YX%!?-xZSjobj)}$ygAwgka6Cc z8^HyO)rQ0Fq^|pOhZBEMk^p;h z9^iat0SkzCGP|IZ__xenSe^$qmttS`*ceTDklDk`b$mIB@2F|PH-))XF?8u(iIeU# z@6zW<%wn&?Gl{V=iIX#Vpqm&R$;^lJg`_{iZ0ReBFEAg`LlUzWF`v-m#K)Om*PoW? z;X?zb^%scuuzp@YO8g%4fc~b$99|q;PI|>IzA^_AG$L%!EFp<=M6!N~lGJeqk>>cJ z6umc@vz)#>oVlENGHj)MG8W}{^$#RYaWWU{cZlnmNA!EduP~3ApGqt|t{A)Y|JZTb zYQ?r8M^8eEo+7>&c3hXD?iJqqc4dFL6hGsrEt%{cDx(OLNSY;;9H>0(|M3O^WB(8omih zf0Ls0WlqX4N+ss<6ZoXm*{_s zxxx9F#3!=GnLL4WY7|44@imEw{2{*BWjrgfAcgrE<%xn>%!iC`6Z5LUGL8`MX8njk z6BJxxK4H8=e2e*Y<2v!U(M}sf5`B|dKW}_a{1EeiX%dGNi-ZmKNqFNFUnHB^H1ITY zn&T9;_xH?M=8Qb_jql}TI7}wZVBKpjru;$XVzY|)3+54Xox~Z_nMck0wD<2Ye{MFB zUa1(n%q@1Dv1Nn0AR`49h&hcrF+(yuM&@1i#K6$;)arFcSogII?9}3{*u;}#++?lCVrUNV^RYP z!^}D6$HaY#X?M3veE25wTIYbo$48hOTteb{W_Zj|B}MZ+rNc_RLVe5w|H5i-!9eotbfsz6lo69Ftg9`UE8eq4 zTF|?s`z195LcOdXy>j&|hY0NI| z68$4{{gsbu*FgN>eWkA~)oy@z?GeQ_xrWvU;vP>>V`?vexWR3#acM_D46)m-6>ZdJ zt=>Al z$1khgVb&j1p|20Bo<%y&I>78zHTuSLMid`pzl%|I?_1p}$2O$&9%LR?oZx*YXHm{z zOzD|Bt#59_6rX4=R+pZ7mDbHKH%|jGmSij4#}Cu|XY1G7aD72KpW_D(U8KezICL-b zxY@l{`F31RZQQ#Z`{#0D{L+gm!LwEs_BS*0Bx@eLqFp|~(BH*{AFVi3z5@rG*1vaS YQ%Z78Jhq8GEMsw59_kEPo4Zl|7qO9C{r~^~