number_station 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.asciidoc +108 -18
- data/lib/number_station/cli.rb +303 -2
- data/lib/number_station/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d9d58534a90398f402d7fad891415ab81782d96aaa440964703c6d33a1e89f37
|
|
4
|
+
data.tar.gz: 7ca3efa06033aacc95db71c8bd9622f9869f82caa1e535a50f592d72b2f39a5e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 73577af7d0e0e2d6b383398dc3c49dee4d2cfe816d0a6e5d19da5947409108bb79343ddc9fade867209f4dc4973c33d4083ea8098bf691766a2edb81ebd1391b
|
|
7
|
+
data.tar.gz: fff53ecba16b4b3d77674148f2982aa2022cae2e8f5ad0d4995a103562663a4b3bd0aae3337a7be0089c1bfcb152814453f8692456c8ca2af5f932ab5d04198e
|
data/README.asciidoc
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
== number_station ==
|
|
2
|
+
|
|
2
3
|
This gem contains utilities to aid in the running of a number station.
|
|
3
4
|
|
|
4
5
|
image:https://badge.fury.io/rb/number_station.svg["Gem Version", link="https://badge.fury.io/rb/number_station"]
|
|
5
6
|
|
|
6
7
|
=== Installation ===
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
[source,bash]
|
|
10
|
+
----
|
|
11
|
+
gem install number_station
|
|
12
|
+
----
|
|
9
13
|
|
|
10
14
|
=== Usage ===
|
|
11
15
|
|
|
@@ -13,7 +17,10 @@ image:https://badge.fury.io/rb/number_station.svg["Gem Version", link="https://b
|
|
|
13
17
|
|
|
14
18
|
Create the initial configuration:
|
|
15
19
|
|
|
16
|
-
|
|
20
|
+
[source,bash]
|
|
21
|
+
----
|
|
22
|
+
number_station create_config
|
|
23
|
+
----
|
|
17
24
|
|
|
18
25
|
This creates `~/number_station/conf.yaml` and copies template files (`intro_message.txt`, `outro_message.txt`, `repeat_message.txt`) to the same directory.
|
|
19
26
|
|
|
@@ -22,64 +29,147 @@ This creates `~/number_station/conf.yaml` and copies template files (`intro_mess
|
|
|
22
29
|
Manage agents using the `agents` subcommand:
|
|
23
30
|
|
|
24
31
|
*Create an agent:*
|
|
25
|
-
|
|
32
|
+
|
|
33
|
+
[source,bash]
|
|
34
|
+
----
|
|
35
|
+
number_station agents create NAME [--location LOCATION] [--handler HANDLER]
|
|
36
|
+
----
|
|
26
37
|
|
|
27
38
|
*Activate/Deactivate:*
|
|
28
|
-
|
|
29
|
-
|
|
39
|
+
|
|
40
|
+
[source,bash]
|
|
41
|
+
----
|
|
42
|
+
number_station agents activate NAME [--start-date DATE]
|
|
43
|
+
number_station agents deactivate NAME [--end-date DATE]
|
|
44
|
+
----
|
|
30
45
|
|
|
31
46
|
*Update handler codeword:*
|
|
32
|
-
|
|
47
|
+
|
|
48
|
+
[source,bash]
|
|
49
|
+
----
|
|
50
|
+
number_station agents update-handler NAME HANDLER
|
|
51
|
+
----
|
|
33
52
|
|
|
34
53
|
*View agents:*
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
54
|
+
|
|
55
|
+
[source,bash]
|
|
56
|
+
----
|
|
57
|
+
number_station agents list # Active agents only
|
|
58
|
+
number_station agents list-all # All agents with history
|
|
59
|
+
number_station agents stats # Detailed statistics
|
|
60
|
+
----
|
|
38
61
|
|
|
39
62
|
==== One-Time Pads ====
|
|
40
63
|
|
|
41
64
|
*Create pads:*
|
|
42
|
-
|
|
65
|
+
|
|
66
|
+
[source,bash]
|
|
67
|
+
----
|
|
68
|
+
number_station pad create [--name AGENT] [--numpads NUM] [--length LENGTH]
|
|
69
|
+
----
|
|
43
70
|
|
|
44
71
|
If `--name` is provided, creates pad in `~/number_station/pads/AGENT/` directory.
|
|
45
|
-
Defaults: 500 pads, 500 characters
|
|
72
|
+
Defaults: 500 pads, 500 characters. Pad length is automatically rounded up to the nearest multiple of 5.
|
|
46
73
|
|
|
47
74
|
*View pad statistics:*
|
|
48
|
-
|
|
75
|
+
|
|
76
|
+
[source,bash]
|
|
77
|
+
----
|
|
78
|
+
number_station pad stats [--path PATH]
|
|
79
|
+
----
|
|
49
80
|
|
|
50
81
|
Shows pad statistics grouped by agent, including unconsumed pad counts.
|
|
51
82
|
|
|
83
|
+
*Convert pad to AsciiDoc:*
|
|
84
|
+
|
|
85
|
+
[source,bash]
|
|
86
|
+
----
|
|
87
|
+
number_station convert_pad_to_asciidoc --padpath PAD_FILE
|
|
88
|
+
----
|
|
89
|
+
|
|
90
|
+
Converts a pad file to AsciiDoc format with each pad as a chapter. Output saved as `FILENAME.asciidoc`.
|
|
91
|
+
Pads are formatted in groups of 5 characters with page breaks after every 2nd pad.
|
|
92
|
+
|
|
93
|
+
*Convert pad to LaTeX:*
|
|
94
|
+
|
|
95
|
+
[source,bash]
|
|
96
|
+
----
|
|
97
|
+
number_station convert_pad_to_latex --padpath PAD_FILE
|
|
98
|
+
----
|
|
99
|
+
|
|
100
|
+
Converts a pad file to LaTeX format with landscape orientation. Output saved as `FILENAME.tex`.
|
|
101
|
+
Pads are formatted in groups of 5 characters, arranged in a 2x2 grid (4 pads per page), with each pad taking up one quadrant of the page.
|
|
102
|
+
|
|
103
|
+
*Convert to PDF:*
|
|
104
|
+
|
|
105
|
+
[source,bash]
|
|
106
|
+
----
|
|
107
|
+
number_station convert_to_pdf INPUT_FILE
|
|
108
|
+
----
|
|
109
|
+
|
|
110
|
+
Converts an AsciiDoc (`.asciidoc` or `.adoc`) or LaTeX (`.tex`) file to PDF.
|
|
111
|
+
+
|
|
112
|
+
For AsciiDoc files: Uses asciidoctor-pdf. Requires asciidoctor-pdf gem to be installed.
|
|
113
|
+
+
|
|
114
|
+
For LaTeX files: Uses pdflatex. Requires a LaTeX distribution (e.g., texlive) to be installed.
|
|
115
|
+
+
|
|
116
|
+
Output saved as `FILENAME.pdf` in the same directory.
|
|
117
|
+
|
|
52
118
|
==== Message Encryption/Decryption ====
|
|
53
119
|
|
|
54
120
|
*Encrypt:*
|
|
55
|
-
|
|
121
|
+
|
|
122
|
+
[source,bash]
|
|
123
|
+
----
|
|
124
|
+
number_station encrypt [MESSAGE] [--file FILE] [--agent AGENT] [--padpath PADPATH] [--numpad NUMPAD]
|
|
125
|
+
----
|
|
56
126
|
|
|
57
127
|
If `--agent` is provided, searches for oldest pad in agent-specific directory.
|
|
58
128
|
Only active agents can encrypt messages.
|
|
59
129
|
|
|
60
130
|
*Decrypt:*
|
|
61
|
-
|
|
131
|
+
|
|
132
|
+
[source,bash]
|
|
133
|
+
----
|
|
134
|
+
number_station decrypt [MESSAGE] [--file FILE] [--padpath PADPATH] [--numpad NUMPAD]
|
|
135
|
+
----
|
|
62
136
|
|
|
63
137
|
==== Phonetic Conversion ====
|
|
64
138
|
|
|
65
139
|
*Convert to phonetic:*
|
|
66
|
-
|
|
140
|
+
|
|
141
|
+
[source,bash]
|
|
142
|
+
----
|
|
143
|
+
number_station convert_to_phonetic FILE [--intro INTRO_FILE] [--outro OUTRO_FILE] [--repeat REPEAT_FILE]
|
|
144
|
+
----
|
|
67
145
|
|
|
68
146
|
Converts encrypted message to phonetic alphabet. Output saved as `FILENAME_phonetic.txt`.
|
|
69
147
|
Intro, outro, and repeat messages are included as-is (not converted).
|
|
70
148
|
|
|
71
149
|
*Convert to espeak XML:*
|
|
72
|
-
|
|
150
|
+
|
|
151
|
+
[source,bash]
|
|
152
|
+
----
|
|
153
|
+
number_station convert_to_espeak PHONETIC_FILE
|
|
154
|
+
----
|
|
73
155
|
|
|
74
156
|
Generates GLaDOS-style espeak XML file. Output saved as `FILENAME.xml`.
|
|
75
157
|
|
|
76
158
|
*Generate MP3:*
|
|
77
|
-
|
|
159
|
+
|
|
160
|
+
[source,bash]
|
|
161
|
+
----
|
|
162
|
+
number_station convert_to_mp3 XML_FILE
|
|
163
|
+
----
|
|
78
164
|
|
|
79
165
|
Converts XML to MP3 using espeak and ffmpeg. Output saved as `FILENAME.mp3`.
|
|
80
166
|
|
|
81
167
|
*Play audio:*
|
|
82
|
-
|
|
168
|
+
|
|
169
|
+
[source,bash]
|
|
170
|
+
----
|
|
171
|
+
number_station espeak XML_FILE
|
|
172
|
+
----
|
|
83
173
|
|
|
84
174
|
Plays the XML file using espeak with GLaDOS voice settings.
|
|
85
175
|
|
data/lib/number_station/cli.rb
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
require 'thor'
|
|
23
23
|
require 'fileutils'
|
|
24
24
|
require 'date'
|
|
25
|
+
require 'json'
|
|
25
26
|
|
|
26
27
|
module NumberStation
|
|
27
28
|
class Pad < Thor
|
|
@@ -38,13 +39,16 @@ module NumberStation
|
|
|
38
39
|
--name NAME - Agent name to associate with this pad. Creates subdirectory ~/number_station/pads/NAME/ if --path is not provided
|
|
39
40
|
--path PATH - Directory where the pad file will be created (defaults to current directory, or ~/number_station/pads/NAME/ if --name is provided)
|
|
40
41
|
--numpads NUM - Number of pads to generate (defaults to 500)
|
|
41
|
-
--length LENGTH - Length of each pad in characters (defaults to 500)
|
|
42
|
+
--length LENGTH - Length of each pad in characters (defaults to 500, rounded up to nearest multiple of 5)
|
|
42
43
|
|
|
43
44
|
If no parameters are passed it will generate 500 one time pads in the current
|
|
44
45
|
directory of size 500 characters.
|
|
45
46
|
|
|
46
47
|
If --name is provided without --path, the pad will be created in ~/number_station/pads/NAME/
|
|
47
48
|
|
|
49
|
+
Note: Pad length is automatically rounded up to the nearest multiple of 5 to ensure
|
|
50
|
+
proper formatting in 5-character groups.
|
|
51
|
+
|
|
48
52
|
Examples:
|
|
49
53
|
number_station pad create --name Shadow --numpads 1000 --length 1000
|
|
50
54
|
number_station pad create --name Shadow
|
|
@@ -419,7 +423,7 @@ module NumberStation
|
|
|
419
423
|
|
|
420
424
|
desc "activate NAME", "Activate an agent"
|
|
421
425
|
long_desc <<-ACTIVATE_AGENT_LONG_DESC
|
|
422
|
-
Activate an agent by name. Sets active status to true
|
|
426
|
+
Activate an agent by name. Sets active status to true, optionally sets start_date, and resets end_date to null.
|
|
423
427
|
|
|
424
428
|
Parameters:
|
|
425
429
|
NAME - The agent name (required)
|
|
@@ -427,6 +431,8 @@ module NumberStation
|
|
|
427
431
|
Options:
|
|
428
432
|
--start-date DATE - Set the start date (defaults to today if not specified)
|
|
429
433
|
|
|
434
|
+
When activating an agent, the end_date is automatically reset to null (clearing any previous end date).
|
|
435
|
+
|
|
430
436
|
Examples:
|
|
431
437
|
number_station agents activate Shadow
|
|
432
438
|
number_station agents activate Shadow --start-date "2024-01-15"
|
|
@@ -676,6 +682,300 @@ module NumberStation
|
|
|
676
682
|
puts "Generated GLaDOS espeak XML: #{output_path}"
|
|
677
683
|
end
|
|
678
684
|
|
|
685
|
+
desc "convert_pad_to_asciidoc --padpath PADPATH", "Convert a pad file to AsciiDoc format"
|
|
686
|
+
long_desc <<-CONVERT_PAD_TO_ASCIIDOC_LONG_DESC
|
|
687
|
+
Convert a one-time pad file to AsciiDoc format with each pad as a chapter.
|
|
688
|
+
|
|
689
|
+
Parameters:
|
|
690
|
+
--padpath PADPATH - Path to pad file (e.g., ~/number_station/pads/Shadow/Shadow-2026-01-12.json)
|
|
691
|
+
|
|
692
|
+
Each pad in the file is converted to groups of 5 characters, separated by spaces.
|
|
693
|
+
Output saved as FILENAME.asciidoc in the same directory.
|
|
694
|
+
|
|
695
|
+
Example:
|
|
696
|
+
number_station convert_pad_to_asciidoc --padpath ~/number_station/pads/Shadow/Shadow-2026-01-12.json
|
|
697
|
+
CONVERT_PAD_TO_ASCIIDOC_LONG_DESC
|
|
698
|
+
option :padpath, type: :string, required: true
|
|
699
|
+
def convert_pad_to_asciidoc
|
|
700
|
+
ensure_config_loaded
|
|
701
|
+
|
|
702
|
+
pad_path = options[:padpath]
|
|
703
|
+
unless File.exist?(pad_path)
|
|
704
|
+
raise Thor::Error, "Pad file not found: #{pad_path}"
|
|
705
|
+
end
|
|
706
|
+
|
|
707
|
+
# Read pad file
|
|
708
|
+
pad_data = JSON.parse(File.read(pad_path))
|
|
709
|
+
pad_id = pad_data["id"]
|
|
710
|
+
pads_hash = pad_data["pads"]
|
|
711
|
+
|
|
712
|
+
if pads_hash.nil? || pads_hash.empty?
|
|
713
|
+
raise Thor::Error, "No pads found in file: #{pad_path}"
|
|
714
|
+
end
|
|
715
|
+
|
|
716
|
+
# Generate output filename: replace .json extension with .asciidoc
|
|
717
|
+
input_basename = File.basename(pad_path, File.extname(pad_path))
|
|
718
|
+
output_filename = "#{input_basename}.asciidoc"
|
|
719
|
+
output_path = File.join(File.dirname(pad_path), output_filename)
|
|
720
|
+
|
|
721
|
+
# Build AsciiDoc content
|
|
722
|
+
asciidoc_content = []
|
|
723
|
+
asciidoc_content << "= One-Time Pad: #{input_basename}"
|
|
724
|
+
asciidoc_content << ""
|
|
725
|
+
|
|
726
|
+
# Process each pad
|
|
727
|
+
pads_hash.sort_by { |k, v| k.to_i }.each do |pad_num, pad_info|
|
|
728
|
+
hex_key = pad_info["key"]
|
|
729
|
+
|
|
730
|
+
# Convert hex string to groups of 5 characters, separated by spaces
|
|
731
|
+
# Remove any existing formatting and group by 5
|
|
732
|
+
cleaned_hex = hex_key.gsub(/[\s\n\r]/, '')
|
|
733
|
+
grouped_hex = cleaned_hex.chars.each_slice(5).map(&:join).join(' ')
|
|
734
|
+
|
|
735
|
+
# Add chapter heading for this pad
|
|
736
|
+
asciidoc_content << "== Pad #{pad_num}"
|
|
737
|
+
asciidoc_content << ""
|
|
738
|
+
asciidoc_content << "[source]"
|
|
739
|
+
asciidoc_content << "----"
|
|
740
|
+
asciidoc_content << grouped_hex
|
|
741
|
+
asciidoc_content << "----"
|
|
742
|
+
asciidoc_content << ""
|
|
743
|
+
|
|
744
|
+
# Add page break after every 2nd pad (after pad 1, 3, 5, etc.)
|
|
745
|
+
if pad_num.to_i % 2 == 1
|
|
746
|
+
asciidoc_content << "<<<"
|
|
747
|
+
asciidoc_content << ""
|
|
748
|
+
end
|
|
749
|
+
end
|
|
750
|
+
|
|
751
|
+
# Write AsciiDoc file
|
|
752
|
+
File.write(output_path, asciidoc_content.join("\n"))
|
|
753
|
+
puts "Generated AsciiDoc file: #{output_path}"
|
|
754
|
+
end
|
|
755
|
+
|
|
756
|
+
desc "convert_pad_to_latex --padpath PADPATH", "Convert a pad file to LaTeX format"
|
|
757
|
+
long_desc <<-CONVERT_PAD_TO_LATEX_LONG_DESC
|
|
758
|
+
Convert a one-time pad file to LaTeX format with landscape orientation.
|
|
759
|
+
|
|
760
|
+
Parameters:
|
|
761
|
+
--padpath PADPATH - Path to pad file (e.g., ~/number_station/pads/Shadow/Shadow-2026-01-12.json)
|
|
762
|
+
|
|
763
|
+
Each pad in the file is converted to groups of 5 characters, separated by spaces.
|
|
764
|
+
Pads are arranged in a 2x2 grid (4 pads per page), with each pad taking up one quadrant.
|
|
765
|
+
Output saved as FILENAME.tex in the same directory.
|
|
766
|
+
|
|
767
|
+
The LaTeX file uses landscape orientation and can be converted to PDF using convert_to_pdf.
|
|
768
|
+
|
|
769
|
+
Example:
|
|
770
|
+
number_station convert_pad_to_latex --padpath ~/number_station/pads/Shadow/Shadow-2026-01-12.json
|
|
771
|
+
CONVERT_PAD_TO_LATEX_LONG_DESC
|
|
772
|
+
option :padpath, type: :string, required: true
|
|
773
|
+
def convert_pad_to_latex
|
|
774
|
+
ensure_config_loaded
|
|
775
|
+
|
|
776
|
+
pad_path = options[:padpath]
|
|
777
|
+
unless File.exist?(pad_path)
|
|
778
|
+
raise Thor::Error, "Pad file not found: #{pad_path}"
|
|
779
|
+
end
|
|
780
|
+
|
|
781
|
+
# Read pad file
|
|
782
|
+
pad_data = JSON.parse(File.read(pad_path))
|
|
783
|
+
pad_id = pad_data["id"]
|
|
784
|
+
pads_hash = pad_data["pads"]
|
|
785
|
+
|
|
786
|
+
if pads_hash.nil? || pads_hash.empty?
|
|
787
|
+
raise Thor::Error, "No pads found in file: #{pad_path}"
|
|
788
|
+
end
|
|
789
|
+
|
|
790
|
+
# Generate output filename: replace .json extension with .tex
|
|
791
|
+
input_basename = File.basename(pad_path, File.extname(pad_path))
|
|
792
|
+
output_filename = "#{input_basename}.tex"
|
|
793
|
+
output_path = File.join(File.dirname(pad_path), output_filename)
|
|
794
|
+
|
|
795
|
+
# Build LaTeX content
|
|
796
|
+
latex_content = []
|
|
797
|
+
latex_content << "\\documentclass[10pt]{article}"
|
|
798
|
+
latex_content << "\\usepackage[utf8]{inputenc}"
|
|
799
|
+
latex_content << "\\usepackage[landscape,margin=0.5in]{geometry}"
|
|
800
|
+
latex_content << "\\usepackage{fancyhdr}"
|
|
801
|
+
latex_content << "\\usepackage{listings}"
|
|
802
|
+
latex_content << "\\lstset{"
|
|
803
|
+
latex_content << " basicstyle=\\ttfamily\\small,"
|
|
804
|
+
latex_content << " breaklines=true,"
|
|
805
|
+
latex_content << " breakatwhitespace=false,"
|
|
806
|
+
latex_content << " columns=fullflexible,"
|
|
807
|
+
latex_content << " frame=none,"
|
|
808
|
+
latex_content << " showstringspaces=false"
|
|
809
|
+
latex_content << "}"
|
|
810
|
+
latex_content << "\\pagestyle{fancy}"
|
|
811
|
+
latex_content << "\\fancyhf{}"
|
|
812
|
+
latex_content << "\\fancyfoot[C]{One-Time Pad: #{input_basename}}"
|
|
813
|
+
latex_content << "\\renewcommand{\\headrulewidth}{0pt}"
|
|
814
|
+
latex_content << "\\renewcommand{\\footrulewidth}{0pt}"
|
|
815
|
+
latex_content << "\\begin{document}"
|
|
816
|
+
latex_content << ""
|
|
817
|
+
|
|
818
|
+
# Process pads in groups of 4 (one page = 4 quadrants)
|
|
819
|
+
pads_array = pads_hash.sort_by { |k, v| k.to_i }
|
|
820
|
+
pads_array.each_slice(4).with_index do |pad_group, index|
|
|
821
|
+
# Start a new page for each group of 4 pads (except the first)
|
|
822
|
+
if index > 0
|
|
823
|
+
latex_content << "\\newpage"
|
|
824
|
+
latex_content << ""
|
|
825
|
+
end
|
|
826
|
+
|
|
827
|
+
# Create 2x2 grid using minipage environments
|
|
828
|
+
latex_content << "\\noindent"
|
|
829
|
+
latex_content << "\\begin{minipage}[t]{0.48\\textwidth}"
|
|
830
|
+
latex_content << "\\vspace{0pt}"
|
|
831
|
+
|
|
832
|
+
# Top-left quadrant (pad 0)
|
|
833
|
+
if pad_group[0]
|
|
834
|
+
pad_num, pad_info = pad_group[0]
|
|
835
|
+
hex_key = pad_info["key"]
|
|
836
|
+
cleaned_hex = hex_key.gsub(/[\s\n\r]/, '')
|
|
837
|
+
grouped_hex = cleaned_hex.chars.each_slice(5).map(&:join).join(' ')
|
|
838
|
+
latex_content << "\\textbf{Pad #{pad_num}}\\\\"
|
|
839
|
+
latex_content << "\\begin{lstlisting}"
|
|
840
|
+
latex_content << grouped_hex
|
|
841
|
+
latex_content << "\\end{lstlisting}"
|
|
842
|
+
end
|
|
843
|
+
|
|
844
|
+
latex_content << "\\end{minipage}"
|
|
845
|
+
latex_content << "\\hfill"
|
|
846
|
+
latex_content << "\\begin{minipage}[t]{0.48\\textwidth}"
|
|
847
|
+
latex_content << "\\vspace{0pt}"
|
|
848
|
+
|
|
849
|
+
# Top-right quadrant (pad 1)
|
|
850
|
+
if pad_group[1]
|
|
851
|
+
pad_num, pad_info = pad_group[1]
|
|
852
|
+
hex_key = pad_info["key"]
|
|
853
|
+
cleaned_hex = hex_key.gsub(/[\s\n\r]/, '')
|
|
854
|
+
grouped_hex = cleaned_hex.chars.each_slice(5).map(&:join).join(' ')
|
|
855
|
+
latex_content << "\\textbf{Pad #{pad_num}}\\\\"
|
|
856
|
+
latex_content << "\\begin{lstlisting}"
|
|
857
|
+
latex_content << grouped_hex
|
|
858
|
+
latex_content << "\\end{lstlisting}"
|
|
859
|
+
end
|
|
860
|
+
|
|
861
|
+
latex_content << "\\end{minipage}"
|
|
862
|
+
latex_content << ""
|
|
863
|
+
latex_content << "\\\\[1cm]"
|
|
864
|
+
latex_content << ""
|
|
865
|
+
latex_content << "\\noindent"
|
|
866
|
+
latex_content << "\\begin{minipage}[t]{0.48\\textwidth}"
|
|
867
|
+
latex_content << "\\vspace{0pt}"
|
|
868
|
+
|
|
869
|
+
# Bottom-left quadrant (pad 2)
|
|
870
|
+
if pad_group[2]
|
|
871
|
+
pad_num, pad_info = pad_group[2]
|
|
872
|
+
hex_key = pad_info["key"]
|
|
873
|
+
cleaned_hex = hex_key.gsub(/[\s\n\r]/, '')
|
|
874
|
+
grouped_hex = cleaned_hex.chars.each_slice(5).map(&:join).join(' ')
|
|
875
|
+
latex_content << "\\textbf{Pad #{pad_num}}\\\\"
|
|
876
|
+
latex_content << "\\begin{lstlisting}"
|
|
877
|
+
latex_content << grouped_hex
|
|
878
|
+
latex_content << "\\end{lstlisting}"
|
|
879
|
+
end
|
|
880
|
+
|
|
881
|
+
latex_content << "\\end{minipage}"
|
|
882
|
+
latex_content << "\\hfill"
|
|
883
|
+
latex_content << "\\begin{minipage}[t]{0.48\\textwidth}"
|
|
884
|
+
latex_content << "\\vspace{0pt}"
|
|
885
|
+
|
|
886
|
+
# Bottom-right quadrant (pad 3)
|
|
887
|
+
if pad_group[3]
|
|
888
|
+
pad_num, pad_info = pad_group[3]
|
|
889
|
+
hex_key = pad_info["key"]
|
|
890
|
+
cleaned_hex = hex_key.gsub(/[\s\n\r]/, '')
|
|
891
|
+
grouped_hex = cleaned_hex.chars.each_slice(5).map(&:join).join(' ')
|
|
892
|
+
latex_content << "\\textbf{Pad #{pad_num}}\\\\"
|
|
893
|
+
latex_content << "\\begin{lstlisting}"
|
|
894
|
+
latex_content << grouped_hex
|
|
895
|
+
latex_content << "\\end{lstlisting}"
|
|
896
|
+
end
|
|
897
|
+
|
|
898
|
+
latex_content << "\\end{minipage}"
|
|
899
|
+
latex_content << ""
|
|
900
|
+
end
|
|
901
|
+
|
|
902
|
+
latex_content << "\\end{document}"
|
|
903
|
+
|
|
904
|
+
# Write LaTeX file
|
|
905
|
+
File.write(output_path, latex_content.join("\n"))
|
|
906
|
+
puts "Generated LaTeX file: #{output_path}"
|
|
907
|
+
end
|
|
908
|
+
|
|
909
|
+
desc "convert_to_pdf INPUT_FILE", "Convert an AsciiDoc or LaTeX file to PDF"
|
|
910
|
+
long_desc <<-CONVERT_TO_PDF_LONG_DESC
|
|
911
|
+
Convert an AsciiDoc or LaTeX file to PDF format.
|
|
912
|
+
|
|
913
|
+
Parameters:
|
|
914
|
+
INPUT_FILE - Path to AsciiDoc file (e.g., Shadow-2026-01-12.asciidoc) or LaTeX file (e.g., Shadow-2026-01-12.tex)
|
|
915
|
+
|
|
916
|
+
For AsciiDoc files:
|
|
917
|
+
Uses asciidoctor-pdf (checks availability before executing).
|
|
918
|
+
Output saved as FILENAME.pdf in the same directory.
|
|
919
|
+
|
|
920
|
+
For LaTeX files:
|
|
921
|
+
Uses pdflatex (checks availability before executing).
|
|
922
|
+
Output saved as FILENAME.pdf in the same directory.
|
|
923
|
+
|
|
924
|
+
Examples:
|
|
925
|
+
number_station convert_to_pdf Shadow-2026-01-12.asciidoc
|
|
926
|
+
number_station convert_to_pdf Shadow-2026-01-12.tex
|
|
927
|
+
CONVERT_TO_PDF_LONG_DESC
|
|
928
|
+
def convert_to_pdf(input_file)
|
|
929
|
+
ensure_config_loaded
|
|
930
|
+
|
|
931
|
+
unless File.exist?(input_file)
|
|
932
|
+
raise Thor::Error, "File not found: #{input_file}"
|
|
933
|
+
end
|
|
934
|
+
|
|
935
|
+
file_ext = File.extname(input_file).downcase
|
|
936
|
+
input_basename = File.basename(input_file, file_ext)
|
|
937
|
+
output_filename = "#{input_basename}.pdf"
|
|
938
|
+
output_path = File.join(File.dirname(input_file), output_filename)
|
|
939
|
+
|
|
940
|
+
if file_ext == '.tex'
|
|
941
|
+
# LaTeX file - use pdflatex
|
|
942
|
+
unless NumberStation.command?('pdflatex')
|
|
943
|
+
raise Thor::Error, "pdflatex is not available. Please install a LaTeX distribution (e.g., texlive)"
|
|
944
|
+
end
|
|
945
|
+
|
|
946
|
+
# Run pdflatex in the directory containing the .tex file
|
|
947
|
+
file_dir = File.dirname(input_file)
|
|
948
|
+
file_name = File.basename(input_file)
|
|
949
|
+
cmd = "cd #{file_dir} && pdflatex -interaction=nonstopmode #{file_name}"
|
|
950
|
+
NumberStation.log.info "Running: #{cmd}"
|
|
951
|
+
system(cmd)
|
|
952
|
+
|
|
953
|
+
unless $?.success?
|
|
954
|
+
raise Thor::Error, "PDF conversion failed with exit code #{$?.exitstatus}"
|
|
955
|
+
end
|
|
956
|
+
|
|
957
|
+
puts "Generated PDF file: #{output_path}"
|
|
958
|
+
elsif file_ext == '.asciidoc' || file_ext == '.adoc'
|
|
959
|
+
# AsciiDoc file - use asciidoctor-pdf
|
|
960
|
+
unless NumberStation.command?('asciidoctor-pdf')
|
|
961
|
+
raise Thor::Error, "asciidoctor-pdf is not available. Please install it with: gem install asciidoctor-pdf"
|
|
962
|
+
end
|
|
963
|
+
|
|
964
|
+
# Convert using asciidoctor-pdf command
|
|
965
|
+
cmd = "asciidoctor-pdf #{input_file} -o #{output_path}"
|
|
966
|
+
NumberStation.log.info "Running: #{cmd}"
|
|
967
|
+
system(cmd)
|
|
968
|
+
|
|
969
|
+
unless $?.success?
|
|
970
|
+
raise Thor::Error, "PDF conversion failed with exit code #{$?.exitstatus}"
|
|
971
|
+
end
|
|
972
|
+
|
|
973
|
+
puts "Generated PDF file: #{output_path}"
|
|
974
|
+
else
|
|
975
|
+
raise Thor::Error, "Unsupported file type: #{file_ext}. Supported types: .asciidoc, .adoc, .tex"
|
|
976
|
+
end
|
|
977
|
+
end
|
|
978
|
+
|
|
679
979
|
desc "espeak XML_FILE", "Use espeak to read an XML file"
|
|
680
980
|
long_desc <<-ESPEAK_LONG_DESC
|
|
681
981
|
Use the espeak utility to read an XML file with GLaDOS-style voice settings.
|
|
@@ -772,6 +1072,7 @@ module NumberStation
|
|
|
772
1072
|
|
|
773
1073
|
If --agent is provided, the system will search for the oldest pad with unconsumed pads
|
|
774
1074
|
in the agent-specific directory (~/number_station/pads/AGENT/).
|
|
1075
|
+
Note: The agent must be active. Encryption will fail if the agent is inactive.
|
|
775
1076
|
|
|
776
1077
|
If --agent is not provided and --padpath/--numpad are not specified, the system will
|
|
777
1078
|
search for the oldest pad with unconsumed pads in ~/number_station/pads/.
|