Subject: choosenews I

From: [email-masked] (Brooks Smith)
Date: 5 Mar 1993 15:16:26 GMT

I noticed recently that Scott Yanoff has posted choose news II here and
there.  I thought some of you might be interested in seeing choosenews I.
It grew out of an expressed need of our novice users when confronted with
the large number of sometimes obscurely named newsgroups available.

#!/usr/bin/perl
#
# Choosenews -- written by Brooks David Smith       sometime in 1991
#                          [email-masked]
#
#     Comments are gladly accepted; flames will not be fanned.
#
# Choose News is an interactive program to aid readers of the news,
# especially beginners, in choosing which news groups to subscribe to.
#
# A file of one-line newsgroup descriptions is merged with the user's
# .newsrc file and displayed one-by-one with options to subscribe or
# unsubscribe.  Other commands aid navigation through the rather large
# list of newsgroups.

# If you wish to use this script on a system other than at UWM, note
# the global variable $GroupDescFile.  This points to the file of
# one-line descriptions of the newsgroups that your net news administrator
# can point you to.
#
# Also note that system() calls are used for "stty" and for the news
# reader "vn".  You may want to change the newsreader at your site.
# If you do so, also change the comment in the sub MissingNewsrc.
# This assumes a format for .newsrc files like that used by vn, rn,
# tin, etc.
#
# termcap.pl is required also.

#
# First major update 17 Dec 92
#
# Reorganized field locations on top of screen because of suggestions
# by Len Levine and John Dobnick.
#
# Run in cbreak mode so user doesn't have to press return after entering
# a command character.  Due to Len Levine.
#
# Provided a "browse" mode where the user can enter vn with the current
# newsgroup to see examples of the traffic.  Has some drawbacks.
#
# Made the (f)ind command just do a substring search instead of a regular
# expression search.  (Search strings like "C++" would crash out.)
# (F)ind was put in for those that like regular expression searches.
#

# Setup for screen painting routines
require "sys/ioctl.ph";
ioctl(TTY, &TIOCGETP, $params);
($ispeed, $ospeed) = (9600, 9600); # unpack('cc', $params);

require "termcap.pl";
&Tgetent($ENV{'TERM'});

# force flushing on print
$| = 1;

#
# Globals
#

# These items constitute the main data structure
# that's navigated by this program.  GroupName
# and GroupIndex are the current pointers to
# the newsgroup being shown.
$GroupIndex = 0;
$GroupName = "";
$GroupCount = -1;
# @Groups[]
# @SortedGroups[]
# %NewsGroups{}

# counters that keep track of the totals of subscribed
# and unsubscribed newsgroups
$Subscribed = 0;
$UnSubscribed = 0;

# Set if the screen is to be completely repainted
$Repaint = 1;

# keeps state for search so repeated searched can be done
$SearchStr = "";
$HoldIndex = 0;

# File names
$GroupDescFile = "/usr/local/lib/news/newsgroups";
$HomeDir = $ENV{'HOME'};
$NewsrcFile = $HomeDir . "/.newsrc";
$OldNewsrcFile = $HomeDir . "/.oldnewsrc";
$NewsPlaceFile = $HomeDir . "/.newsplace";

#
# Keyboard input control
#

sub CbreakOn {
	system("stty cbreak");
}

sub CbreakOff {
	system("stty -cbreak");
}

#
# Screen painting primitives
#

sub Cursor {
	local($Row, $Column) = @_;

	&Tputs(&Tgoto($TC{'cm'}, $Column, $Row), O, STDOUT);
}

sub Clear {
	&Tputs($TC{'cl'}, $affcnt, STDOUT);
}

sub Paint {
	local($Row, $Column, $Text, $Width) = @_;

	$NumberArgs = @_;
	if ($NumberArgs == 3) {
		$Width = length($Text);
	}elsif($NumberArgs == 2) {
		$Width = 0;
		$Text = "";
	}

	&Cursor($Row, $Column);
	print " " x $Width;
	&Cursor($Row, $Column);
	print $Text;
}

sub BrightPaint {
	local($Row, $Column, $Text, $Width) = @_;

	$NumberArgs = @_;
	if ($NumberArgs == 3) {
		$Width = length($Text);
	}elsif($NumberArgs == 2) {
		$Width = 0;
		$Text = "";
	}

	&Paint($Row, $Column, "", $Width);
	&Tputs($TC{'so'}, $affcnt, STDOUT);
	&Paint($Row, $Column, $Text);
	&Tputs($TC{'se'}, $affcnt, STDOUT);
}

sub ClearBuf {

	&Paint(7, 0, "", 40);
	&Paint(7, 0, ">");
}

sub ClearPrompt {

	&Paint(7, 40, "", 40);
#	&Paint(7, 0);
}

sub Prompt {
	local($Message) = @_;

	&ClearPrompt;
	&BrightPaint(7, 40, $Message);
#	&Paint(7, 0);
}

#
# File handling
#

sub GetNewsrc {

	open(NEWSRC, $NewsrcFile) || &MissingNewsrc;

	while (<NEWSRC>) {
		($GroupName, $Range) = split;
		$Subscription = chop($GroupName);
		if ($Subscription eq ":")
			{$Subscribed++;}
		else
			{$UnSubscribed++;}
		$GroupCount++;
		$Groups[$GroupCount] = $GroupName;
		$NewsGroups{RANGE . $GroupName} = $Range;
		$NewsGroups{SUBSCRIBE . $GroupName} = $Subscription;
	}

	close(NEWSRC);

	@SortedGroups = sort(@Groups);
}

sub GetDesc {

	open(NEWSDESC, $GroupDescFile) || die "Missing newsgroups file";

	while (<NEWSDESC>) {
		chop;
		@InRec = split(' ');
		$GroupName = shift(@InRec);
		if (scalar(@InRec) != 0) {
			$NewsGroups{DESCRIPT . $GroupName} = join(" ",@InRec);
		}
	}

	close(NEWSDESC);
}

sub PutNewsrc {
	local($Group);

	rename($NewsrcFile, $OldNewsrcFile);
	open(OUTFILE, ">" . $NewsrcFile);
	for ($GroupIndex = 0; $GroupIndex < @Groups; $GroupIndex++) {
		$Group = $Groups[$GroupIndex];
		print(OUTFILE $Group, $NewsGroups{SUBSCRIBE . $Group}, " ",
			$NewsGroups{RANGE . $Group}, "\n");
	}
	close OUTFILE;
}

sub GetPlace {
	local($LastGroup);

	if ( -e $NewsPlaceFile) {
		open(NEWSPLACE, $NewsPlaceFile);
	} else {
		return 0;
	}
	$LastGroup = <NEWSPLACE>;
	chop($LastGroup);

	close NEWSPLACE;
	return($LastGroup);
}

sub PutPlace {
	local($LastGroup) = @_;

	open (NEWSPLACE, ">" . $NewsPlaceFile);
	print(NEWSPLACE $LastGroup, "\n");
	close NEWSPLACE;
}

sub InitData {

	&GetNewsrc;
	&GetDesc;
	$GroupName = &GetPlace;
	if ($GroupName eq 0) {
		$GroupName = $SortedGroups[0];
		$GroupIndex = 0;
	} else {
		for($GroupIndex = 0; $GroupIndex <= $GroupCount; $GroupIndex++) {
			if ($SortedGroups[$GroupIndex] eq $GroupName) {
				last;
			}
		}
	}
}

#
# Display handling
#

sub OpeningScreen {
	&Clear;
	&BrightPaint(9, 33, "CHOOSE NEWS");
	&BrightPaint(14, 12, "Please wait while the newsgroup lists are being read.");
	&Paint(23, 0);
}

sub MissingNewsrc {

	&Clear;
	&BrightPaint(10, 10, "You don't have a newsgroup control file.");
	&BrightPaint(12, 10, "Run vn to create one and, then, try again.");
	&AbnormalExit;
}

sub Instructions {

	if ($Repaint == 1) {
		&Paint(8, 0, "\
   n   show Next newsgroup               f   Find newsgroup with name\
                                                 containing entered string\
   N   move to Next news category\
                                         x   eXit and revert to previous\
   p   show Previous newsgroup                   subscription status\
\
   P   move to Previous news category    e   Exit and keep changes you've\
                                                 made. Your current place in\
   s   Subscribe to this newsgroup               newsgroup list will be noted\
\
   S   Subscribe to entire category      r   Repaint screen\
\
   u   Unsubscribe to this newsgroup     b   Browse this newsgroup\
\
   U   Unsubscribe to entire category    ?   notes of explanation");
	}
}

sub NoteScreen  {

	&Clear;
	&Paint(0, 0, "\
    Newsgroups are organized into categories by name.  For example, the news-\
group \"comp.lang.fortran\" is in the major category \"comp\" for computing re-\
lated issues; \"comp\" is qualified by \"lang\" a subcategory for programming\
languages; a final qualifier of \"fortran\" indicates the subject under dis-\
cussion.  The commands that deal with categories -- P, N, S and U -- refer to\
the major category, the part of the newsgroup name up to the first period.\
\
    You may notice that the newsgroups are presented in alphabetical order.\
This may not be the order you see them with your newsreader.  Alphabetical\
order is used so newsgroups can be treated in logical blocks by major cate-\
gory.  The order in which a news reader presents the newsgroups will not be\
affected.\
\
    The (n)ext newsgroup command is the default: just press return to move to\
the next newsgroup.\
\
    (f)ind will use the last search word entered if you press return when\
asked for the search word.\
\
                     <press return key to continue>\n");
	$Dummy = getc(STDIN);
#	$Dummy = <STDIN>;
	&InitScreen;
}

sub ShowGroup {

	if ($Repaint == 1) {
		&Paint(0, 0, "Newsgroup");
	}
	&BrightPaint(0, 11, $GroupName, 48);
}

sub ShowStatus {

	if ($Repaint == 1) {
		&Paint(2, 0, "Status");
	}
	if ($NewsGroups{SUBSCRIBE . $GroupName} eq ":") {
		&BrightPaint(2, 11, "subscribed", 12);
	} else {
		&BrightPaint(2, 11, "unsubscribed", 12);
	}
}

sub ShowDescription {

	if ($Repaint == 1) {
		&Paint(4, 0, "Description");
	}
	if (defined($NewsGroups{DESCRIPT . $GroupName})) {
		&BrightPaint(5, 3, $NewsGroups{DESCRIPT . $GroupName}, 75);
	} else {
		&BrightPaint(5, 3, "*Description of newsgroup not available*", 75);
	}
}

sub ShowTotals {

	if ($Repaint == 1) {
		&Paint(2, 54, "Total subscribed");
		&Paint(4, 54, "Total unsubscribed");
	}
	&BrightPaint(2, 74, $Subscribed, 5);
	&BrightPaint(4, 74, $UnSubscribed, 5);
}

sub InitScreen {

	$Repaint = 1;
	&Clear;
	&ShowGroup;
	&ShowStatus;
	&ShowDescription;
	&Instructions;
	&ShowTotals;
	$Repaint = 0;
	&ClearBuf;
	&CbreakOn;
}

#
# Command processing
#

# setup repaint for returning from control-Z
$SIG{'CONT'} = 'InitScreen';
# treat control-c as abnormal exit -- "x"
# This doesn't work quite correctly if "n" is answered
# to the exit question. ??
$SIG{'INT'} = 'AbnormalExit';

sub NormalExit {

	&Prompt("Exit normally and keep changes?");
	&ClearBuf;
	$_ = getc(STDIN);
	if ($_ =~ /[Yy]/) {
		&Prompt("Good bye");
		&PutPlace($GroupName);
		&PutNewsrc;
		&Paint(23, 0, "", 70);
		&CbreakOff;
		exit 0;
	} else {
		&Prompt("Continue processing");
		return;
	}
}

sub AbnormalExit {

	&Prompt("Exit abnormally and ignore changes?");
	&ClearBuf;
	$_ = getc(STDIN);
	if ($_ =~ /[Yy]/) {
		&Prompt("Bail out");
		&Paint(23, 0, "", 70);
		&CbreakOff;
		exit 1;
	} else {
		&Prompt("Continue processing");
		&ClearBuf;
		return;
	}
}

sub NextGroup {

	$GroupIndex++;
	if ($GroupIndex > $GroupCount) {
		&Prompt("returned to beginning of list");
		$GroupIndex = 0;
	}
	$GroupName = $SortedGroups[$GroupIndex];
}

sub PrevGroup {

	$GroupIndex--;
	if ($GroupIndex < 0) {
		&Prompt("passed beginning; went to end of list");
		$GroupIndex = $GroupCount;
	}
	$GroupName = $SortedGroups[$GroupIndex];
}

sub NextPrefix {

	@GroupSubNames = split(/\./, $GroupName);
	$CurrentPrefix = shift(@GroupSubNames);
	$NewPrefix = $CurrentPrefix;
	while ($CurrentPrefix eq $NewPrefix) {
		&NextGroup;
		@GroupSubNames = split(/\./, $GroupName);
		$NewPrefix = shift(@GroupSubNames);
	}
}

sub PrevPrefix {

	@GroupSubNames = split(/\./, $GroupName);
	$CurrentPrefix = shift(@GroupSubNames);
	$NewPrefix = $CurrentPrefix;
	while ($CurrentPrefix eq $NewPrefix) {
		&PrevGroup;
		@GroupSubNames = split(/\./, $GroupName);
		$NewPrefix = shift(@GroupSubNames);
	}
}

sub Subscribe {

	if ($NewsGroups{SUBSCRIBE . $GroupName} eq "!") {
		$Subscribed++;
		$UnSubscribed--;
		$NewsGroups{SUBSCRIBE . $GroupName} = ":";
	}
	&NextGroup;
}

sub UnSubscribe {

	if ($NewsGroups{SUBSCRIBE . $GroupName} eq ":") {
		$Subscribed--;
		$UnSubscribed++;
		$NewsGroups{SUBSCRIBE . $GroupName} = "!";
	}
	&NextGroup;
}

sub BulkSubscribe {

	&PrevPrefix;
	&NextGroup;
	@GroupSubNames = split(/\./, $GroupName);
	$CurrentPrefix = shift(@GroupSubNames);
	$NewPrefix = $CurrentPrefix;
	while ($CurrentPrefix eq $NewPrefix) {
		&Subscribe;
		@GroupSubNames = split(/\./, $GroupName);
		$NewPrefix = shift(@GroupSubNames);
	}
}

sub BulkUnSubscribe {

	&PrevPrefix;
	&NextGroup;
	@GroupSubNames = split(/\./, $GroupName);
	$CurrentPrefix = shift(@GroupSubNames);
	$NewPrefix = $CurrentPrefix;
	while ($CurrentPrefix eq $NewPrefix) {
		&UnSubscribe;
		@GroupSubNames = split(/\./, $GroupName);
		$NewPrefix = shift(@GroupSubNames);
	}
}

sub FindGroupRegex {

	&CbreakOff;
	&Prompt("enter regular expression to search for");
	&ClearBuf;
	$NewSearchStr = <STDIN>;
	chop($NewSearchStr);
	if (length($NewSearchStr) != 0) {
		$SearchStr = $NewSearchStr;
		$HoldIndex = $GroupIndex;
	}
	&Prompt("search for $SearchStr");
	&NextGroup;
	while ($HoldIndex != $GroupIndex) {
		if ($GroupName =~ $SearchStr) {
			&Prompt("$SearchStr found");
			&CbreakOn;
			return;
		} else {
			&NextGroup;
		}
	}
	&Prompt("$SearchStr not found");
	&CbreakOn;
}

sub FindGroup {

	&CbreakOff;
	&Prompt("enter word to find");
	&ClearBuf;
	$NewSearchStr = <STDIN>;
	chop($NewSearchStr);
	if (length($NewSearchStr) != 0) {
		$SearchStr = $NewSearchStr;
		$HoldIndex = $GroupIndex;
	}
	&Prompt("search for $SearchStr");
	&NextGroup;
	while ($HoldIndex != $GroupIndex) {
		if (index($GroupName,$SearchStr,$[) < $[) {
			&NextGroup;
		} else {
			&Prompt("$SearchStr found");
			&CbreakOn;
			return;
		}
	}
	&Prompt("$SearchStr not found");
	&CbreakOn;
}

sub BrowseGroup {

	&CbreakOff;
	sleep(2);
	&Clear;
	system("vn -n  $GroupName");
	&CbreakOn;
	&InitScreen;
}

#
# Central logic
#

sub SubscriptionLoop {

	for (;;) {
		&ClearBuf;
		$_ = getc(STDIN);

		if ($_ eq "\n") {
			&Prompt("next newsgroup");
			&NextGroup;
		}
		elsif ($_ eq "n") {
			&Prompt("next newsgroup");
			&NextGroup;
		}
		elsif ($_ eq "j") {
			&Prompt("next newsgroup");
			&NextGroup;
		}
		elsif ($_ eq "N") {
			&Prompt("Next major category");
			&NextPrefix;
		}
		elsif ($_ eq "p") {
			&Prompt("previous newsgoup");
			&PrevGroup;
		}
		elsif ($_ eq "k") {
			&Prompt("previous newsgoup");
			&PrevGroup;
		}
		elsif ($_ eq "P") {
			&Prompt("Previous major category");
			&PrevPrefix;
		}
		elsif ($_ eq "s") {
			&Prompt("subscribe");
			&Subscribe;
		}
		elsif ($_ eq "S") {
			&Prompt("Subscribe to category of newsgroups");
			&BulkSubscribe;
		}
		elsif ($_ eq "u") {
			&Prompt("unsubscribe");
			&UnSubscribe;
		}
		elsif ($_ eq "U") {
			&Prompt("Unsubscribe to category of newsgroups");
			&BulkUnSubscribe;
		}
		elsif ($_ eq "F") {
			&Prompt("find group with regular expressions");
			&FindGroupRegex;
		}
		elsif ($_ eq "f") {
			&Prompt("find group");
			&FindGroup;
		}
		elsif ($_ eq "b") {
			&Prompt("browse newsgroup");
			&BrowseGroup;
		}
		elsif ($_ eq "x") {
			&Prompt("undo changes and eXit");
			&AbnormalExit;
		}
		elsif ($_ eq "e") {
			&NormalExit;
		}
		elsif ($_ eq "q") {
			&NormalExit;
		}
		elsif ($_ eq "r") {
			&InitScreen;
			&Prompt("Repaint screen");
		}
		elsif ($_ eq "\f") {
			&InitScreen;
			&Prompt("Repaint screen");
		}
		elsif ($_ eq "?") {
			&NoteScreen;
			&Prompt("notes");
		}
		else {
			&InitScreen;
			&Prompt("\"$_\" is not a known command");
		}
		&ShowGroup;
		&ShowStatus;
		&ShowDescription;
		&ShowTotals;
	}
}	

&OpeningScreen;
&InitData;
&InitScreen;
&SubscriptionLoop;

----------------------------------------------------------------------
Brooks David Smith 		    Internet:	[email-masked]
Computing Services Division	    Uucp:	uwm!brooks
University of Wisconsin -- Milw     Bitnet:	brooks%uwm.edu@INTERBIT
Milwaukee, WI 53211	            Phone:	+1 (414) 229-6413

-- 
Brooks David Smith 		    Internet:	[email-masked]
Computing Services Division	    Uucp:	uwm!brooks
University of Wisconsin -- Milw     Bitnet:	brooks%uwm.edu@INTERBIT
Milwaukee, WI 53211	            Phone:	+1 (414) 229-6413