#!/usr/bin/env perl # Copyright (c) 2026 snake_case_nemo # SPDX-License-Identifier: BSD-2-Clause # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # 1. Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. use strict; use warnings; # Options passed to the constructor are only used if they are not set in the # config file. # root_dir defaults to $HOME/gopher and the setting is commented out in the # example config that is automatically created. my $geph = Geph::Map->new( root_dir => "$ENV{HOME}/gopher", logo_file => "logo", logo_color => "red", header_file => "header", static_file => "static", phlog_dir_names => "phlog,ansi_phlog", latest_files => 5 ); my $entries_hash = $geph->get_entry_list; $geph->create_phlog_maps($entries_hash); $geph->delete_main_gophermap; $geph->write_logo; $geph->write_header; $geph->cow('think', 'bunny', $ARGV[0]) if $ARGV[0]; $geph->create_phlog_list($entries_hash); $geph->write_static_file; $geph->__chmod; ################################################################################ =head1 NAME Geph::Map - create gophermaps for my phlog =head1 SYNOPSIS use Geph::Map; # Options passed to the constructor are only used if they are not set in the # config file. # root_dir defaults to $HOME/gopher and the setting is commented out in the # example config that is automatically created. my $geph = Geph::Map->new( root_dir => "$ENV{HOME}/gopher", logo_file => "logo", logo_color => "red", header_file => "header", static_file => "static" ); my $entries_hash = $geph->get_entry_list; $geph->create_phlog_maps($entries_hash); $geph->delete_main_gophermap; $geph->write_logo; $geph->write_header; $geph->create_phlog_list($entries_hash); $geph->write_static_file; =cut =head1 DESCRIPTION This is what I use. Creates main gophermap from static files containing gopher formatted text. Dynamically creates a list of latest entries of a phlog from text files. =cut package Geph::Map; use strict; use warnings; use Time::Piece; use File::Basename; use File::Path qw(make_path); use feature 'say'; use Data::Dumper; use constant DEFAULT_CONFIG_LOCATION => "$ENV{HOME}/.config/geph-map/config"; use constant POSSIBLE_CONFIG_LOCATIONS => [( "$ENV{HOME}/.config/geph-map/config", "$ENV{HOME}/.geph-map/config", "$ENV{HOME}/.geph-map.conf", )]; use constant COLOR => { RED => "\e[31m", GREEN => "\e[32m", YELLOW => "\e[33m", BLUE => "\e[34m", MAGENTA => "\e[35m", CYAN => "\e[36m", BOLD => "\e[1m", RESET => "\e[0m" }; =head1 FUNCTIONS =cut sub new { my ($class, %args) = @_; # Get param or set default. my $root_dir = delete $args{ root_dir } || "$ENV{HOME}/gopher"; my $phlog_dir_names = delete $args{ phlog_dir_names } || "phlog"; my $latest_files = delete $args{ latest_files } || 3; my $examine_lines = delete $args{ examine_lines } || 5; my $timestamp_fmt_in = delete $args{ timestamp_fmt_in } || "%a %b %d %H:%M:%S %Z %Y"; my $timestamp_fmt_out = delete $args{ timestamp_fmt_out } || "[%d-%b-%Y]"; my $timestamp_align = delete $args{ timestamp_align } || "before"; my $headline = delete $args{ headline } || "===="; my $header_file = delete $args{ header_file } || ""; my $logo_file = delete $args{ logo_file } || ""; my $static_file = delete $args{ static_file } || ""; my $logo_color = delete $args{ logo_color } || ""; my $txt_file_extension = delete $args{ txt_file_extension } || "txt"; # Get config location or use standard. my $try_config_location = &{ sub { do { return $_ if -e $_; } for @{ &POSSIBLE_CONFIG_LOCATIONS } } }; my $_config_location = ($try_config_location) ? $try_config_location : DEFAULT_CONFIG_LOCATION; # Make config directory if it does not exist. my $_config_dir = dirname $_config_location; make_path($_config_dir) if ! -d $_config_dir; # default config my $_config = { root_dir => $root_dir, gm_file => "gophermap", etc_dots => "...", phlog_dir_names => $phlog_dir_names, latest_files => $latest_files, examine_lines => $examine_lines, timestamp_fmt_in => $timestamp_fmt_in, timestamp_fmt_out => $timestamp_fmt_out, timestamp_align => $timestamp_align, headline => $headline, header_file => $header_file, logo_file => $logo_file, static_file => $static_file, logo_color => $logo_color, txt_file_extension => $txt_file_extension }; my $self = { config => $_config, config_dir => $_config_dir, config_location => $_config_location, }; bless $self, $class; $self->_parse_config; return $self; } =head2 cow() $obj->cow('say', 'default', 'Moo!'); $obj->cow('think', 'daemon', 'Moo?'); Insert text with cowsay or cowthink. =cut sub cow { my $self = shift; my $type = shift; my $cowfile = shift || "small"; my $str = shift || "Moo"; my $exec; if ($type eq "say") { $exec = "cowsay"; } elsif ($type eq "think") { $exec = "cowthink"; } else { return; } my $rd = $self->{config}->{root_dir}; my $main_map = "$rd/gophermap"; open GM, ">>", $main_map or die $!; print GM $_ for (`bash -c 'echo "$str" | $exec -f $cowfile'`); print GM $_ for (`date`); say GM ""; close GM; } =head2 __chmod() Fix access rights. =cut sub __chmod { my $self = shift; chmod 0644, "$self->{config}->{root_dir}/gophermap"; } =head2 create_phlog_list TODO DESCRIPTION =cut sub create_phlog_list { my ($self, $entries) = @_; my $rd = $self->{config}->{root_dir}; my $main_map = "$rd/gophermap"; my $c; my $latest_files = $self->{config}->{latest_files}; my $f_ext = $self->{config}->{txt_file_extension}; open GM, ">>", $main_map or die $!; for my $dir ( keys %{ $entries } ) { my @entries = @{ $entries->{$dir} }; print GM " " . $self->{config}->{headline} . " "; print GM $dir . " "; print GM $self->{config}->{headline} . "\n"; $c = 0; while ($c < $latest_files) { last if ! $entries[$c]; $entries[$c] =~s /(.*)\t(.*.txt)$/$1\t$dir\/$2/; say GM $entries[$c++]; } # print MAP to phlog dir say GM "1...\t$dir\n"; } close GM; } =head2 delete_main_gophermap Delete main gophermap. You probably want to do this. Other methods only append to main gophermap. =cut sub delete_main_gophermap { my $self = shift; my $rd = $self->{config}->{root_dir}; my $main_map = "$rd/gophermap"; return if ! -e $main_map; unlink $main_map; } sub _write_static_file { my ($self, $option) = @_; my $rd = $self->{config}->{root_dir}; my $file = "$rd/$self->{config}->{$option}"; return if ! -e $file; my $main_map = "$rd/gophermap"; open F, "<", $file or die $!; open GM, ">>", $main_map or die $!; print GM $_ while ; close F; close GM; } =head2 write_logo() Prints the logo from log file if said file exists. Prints the logo in color if C is set. TODO specifiy details for configuration =cut sub write_logo { my $self = shift; my $rd = $self->{config}->{root_dir}; my $lf = "$rd/$self->{config}->{logo_file}"; return if ! -e $lf; # Logo Color my $lg = $self->{config}->{logo_color}; $lg = ($lg) ? uc($lg) : ""; my $main_map = "$rd/gophermap"; open LF, "<", $lf or die $!; open GM, ">>", $main_map or die $!; say GM COLOR->{$lg} if COLOR->{$lg}; print GM $_ while ; say GM COLOR->{RESET} if COLOR->{$lg}; close LF; close GM; } =head2 write_header() Print the header file if it exists. Uses the configuration option C. =cut sub write_header { my $self = shift; my $rd = $self->{config}->{root_dir}; $self->_write_static_file("header_file"); } =head2 write_static_file Write file from option C to main gophermap. =cut sub write_static_file { my $self = shift; my $rd = $self->{config}->{root_dir}; $self->_write_static_file("static_file"); } =head2 create_phlog_maps() Creates a gophermap file under each directory of C. =cut sub create_phlog_maps { my ($self, $entries) = @_; for my $dir ( keys %{ $entries } ) { my $gophermap = "$self->{config}->{root_dir}/$dir/gophermap"; open GM, ">", $gophermap or die $!; say GM $_ for @{ $entries->{$dir} }; close GM; chmod 0644, $gophermap; } } =head2 get_entry_list() Creates a list for entries in the phlog directories. =cut sub get_entry_list { my $self = shift; my $entries_hash = {}; my @result; my @sorted; my @files; my $f_ext = $self->{config}->{txt_file_extension}; for my $dir ((split /,/, $self->{ config }->{ phlog_dir_names })) { my $phlog_dir = "$self->{ config }->{ root_dir }/$dir"; @result = (); @files = (); @sorted = (); @files = grep { /.*\.$f_ext$/ } glob("$phlog_dir/*"); for (@files) { my ($timestamp, $date_string, $title, $filename) = $self->_get_file_info($_); $title =~ s/\.txt$//; $title =~ s/_/ /g; push @result, { timestamp => $timestamp, line => "0$date_string $title\t$filename" }; chmod 0644, $_; } # return result sorted push @sorted, $_->{line} for ( sort { $b->{timestamp} <=> $a->{timestamp} } @result) ; $entries_hash->{ $dir } = [ @sorted ]; } return $entries_hash; } # TODO DESCRIPTION sub _get_file_info { my $self = shift; my $f_path = shift or die; my $filename = basename($f_path); my $mtime = (stat $_)[9]; my $t = gmtime($mtime); my $timestamp; my $tp; # my $date_string = $t->strftime("%a %b %d %H:%M:%S %Z %Y"); my $date_string = $t->strftime("'$self->{config}->{timestamp_fmt_in}'"); open my $fh, "<", $_ or die $!; my @first_four_lines = (<$fh>)[0..3]; close $fh; my $title = (map { $_=~s/^!//;+$_; } grep { /^!.*/ } @first_four_lines)[0] || $filename; chomp($title); $date_string = (grep { /\d{2}:\d{2}:?\d{2}/ } @first_four_lines)[0] || $date_string; chomp($date_string); # UTC can make problems $date_string =~ s/\s+UTC//; my $format_in_without_utc = $self->{config}->{timestamp_fmt_in}; $format_in_without_utc =~ s/%Z\s//g; eval { $tp = Time::Piece->strptime( $date_string, $format_in_without_utc); 1; }; if ($@) { $timestamp = $mtime; } else { $timestamp = $tp->epoch; } $t = gmtime($timestamp); $date_string = $t->strftime($self->{config}->{timestamp_fmt_out}); return ($timestamp, $date_string, $title, $filename); } # TODO DESCRIPTION sub _parse_config { my ($self) = shift; if (! -e $self->{config_location}) { $self->_gen_example_config; } open CONFIG, "<", $self->{config_location} or die $!; my @config_file = ; close CONFIG; my $val; for my $k (keys %{ $self->{config} }) { $val = $self->_get_option(\@config_file, $k); $val =~ s/"//g if $val; # keep default if $val is undef $self->{config}->{ $k } = $val if $val; } } # TODO DESCRIPTION sub _get_option { my ($self, $config_ref, $key) = @_; my $val = (grep { /^$key\s=\s.*$/ } @{ $config_ref })[0] || undef; return undef if !$val; $val = (split(/ = /, $val))[1]; # trim leading, trailing spaces, newlines $val =~ s/^\s*(.*)\s*$/$1/; return $val; } # TODO DESCRIPTION sub _gen_example_config { my $self = shift; my @example_config = <", $self->{config_location} or die $!; print EX_CFG $_ for @example_config; close EX_CFG; } =head1 CONFIGURATION FILE Can be one of the folling: $HOME/.config/geph-map/config $HOME/.geph-map/config $HOME/.geph-map.conf Default is $HOME/.config/geph-map/config =head1 AUTHOR snake_case_nemo Escnemo@sdf.orgE =cut 1;