zhuzh 0.1.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 +7 -0
- data/README.md +76 -0
- data/bin/tapioca +27 -0
- data/bin/zz +22 -0
- data/lib/zhuzh/cli.rb +68 -0
- data/lib/zhuzh/config.rb +28 -0
- data/lib/zhuzh/note.rb +95 -0
- data/lib/zhuzh/version.rb +10 -0
- data/lib/zhuzh.rb +23 -0
- metadata +162 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: a8023b517bd18b09affd30ac54d42b5245021c0cee130f9de6490f758e64b378
|
4
|
+
data.tar.gz: 8551adc0c2205d4ff3640afe090239740158fea6c2f5876e38313b78e786d185
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 9a7875c2e0855f3e00f6a8a607d9635c8e2342dabeeeb90334b8a91448f193ee3c5ab7d890488ce8339e9c680a3d3d9dad1745bc7601f1b259f1805eeadc96eb
|
7
|
+
data.tar.gz: 63ba8328c156ac24b395440e5b603eaf458da623aaca25900f70f86763221727b33dbf0722b62829a659bd054e084a37a3e07db2aa3267115693081b768fc1a1
|
data/README.md
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
# Zhuzh
|
2
|
+
|
3
|
+
A tool for easily creating and managing markdown notes.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
```bash
|
8
|
+
gem install zhuzh
|
9
|
+
```
|
10
|
+
|
11
|
+
## Usage
|
12
|
+
|
13
|
+
### Basic Usage
|
14
|
+
|
15
|
+
Open your editor:
|
16
|
+
```bash
|
17
|
+
zz
|
18
|
+
```
|
19
|
+
|
20
|
+
When you save and close the editor, the note will be automatically saved.
|
21
|
+
|
22
|
+
### Add a note directly:
|
23
|
+
|
24
|
+
```bash
|
25
|
+
zz "This is my super important note I need to write it quick before I forget"
|
26
|
+
```
|
27
|
+
|
28
|
+
### Pipe/redirect content to zz:
|
29
|
+
|
30
|
+
```bash
|
31
|
+
cat blog_post.txt | zz
|
32
|
+
```
|
33
|
+
|
34
|
+
or
|
35
|
+
|
36
|
+
```bash
|
37
|
+
zz < blog_post.txt
|
38
|
+
```
|
39
|
+
|
40
|
+
### Configuration
|
41
|
+
|
42
|
+
By default, notes are stored in `$HOME/Zarchive`. You can change this by setting the `ZZ_DIR` environment variable:
|
43
|
+
|
44
|
+
```bash
|
45
|
+
export ZZ_DIR="$HOME/my_notes"
|
46
|
+
```
|
47
|
+
|
48
|
+
### Note Organization
|
49
|
+
|
50
|
+
Notes are organized based on their content:
|
51
|
+
- The first word becomes the subdirectory name
|
52
|
+
- Words 2-5 are used in the filename, followed by a timestamp
|
53
|
+
- For example, if you start a note with "Project ideas for tomorrow", it will be saved as `$ZZ_DIR/project/ideas_for_tomorrow_TIMESTAMP.md`
|
54
|
+
|
55
|
+
## Neovim Integration
|
56
|
+
|
57
|
+
Add this to your Neovim configuration:
|
58
|
+
|
59
|
+
```vim
|
60
|
+
" Save current buffer with Zhuzh
|
61
|
+
function! SaveToZhuzh()
|
62
|
+
let output = system('zz', join(getline(1, '$'), "\n"))
|
63
|
+
echo "Saved to: " . trim(output)
|
64
|
+
endfunction
|
65
|
+
command! Zz call SaveToZhuzh()
|
66
|
+
```
|
67
|
+
|
68
|
+
Then you can use `:Zz` to save the current buffer as a Zhuzh note.
|
69
|
+
|
70
|
+
## Development
|
71
|
+
|
72
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests.
|
73
|
+
|
74
|
+
## License
|
75
|
+
|
76
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/bin/tapioca
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
#
|
5
|
+
# This file was generated by Bundler.
|
6
|
+
#
|
7
|
+
# The application 'tapioca' is installed as part of a gem, and
|
8
|
+
# this file is here to facilitate running it.
|
9
|
+
#
|
10
|
+
|
11
|
+
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
|
12
|
+
|
13
|
+
bundle_binstub = File.expand_path('bundle', __dir__)
|
14
|
+
|
15
|
+
if File.file?(bundle_binstub)
|
16
|
+
if File.read(bundle_binstub, 300).include?('This file was generated by Bundler')
|
17
|
+
load(bundle_binstub)
|
18
|
+
else
|
19
|
+
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
|
20
|
+
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
require 'rubygems'
|
25
|
+
require 'bundler/setup'
|
26
|
+
|
27
|
+
load Gem.bin_path('tapioca', 'tapioca')
|
data/bin/zz
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'zhuzh'
|
5
|
+
|
6
|
+
# Handle command line arguments directly
|
7
|
+
if ARGV.size.positive?
|
8
|
+
# Add explicit newline at the end of command line input
|
9
|
+
text = "#{ARGV.join(' ')}\n"
|
10
|
+
config = Zhuzh::Config.new
|
11
|
+
note = Zhuzh::Note.new(text, config)
|
12
|
+
|
13
|
+
if note.save
|
14
|
+
puts note.filepath
|
15
|
+
exit 0
|
16
|
+
else
|
17
|
+
exit 1
|
18
|
+
end
|
19
|
+
else
|
20
|
+
# No arguments provided, use the CLI handler for stdin/editor
|
21
|
+
exit Zhuzh::CLI.start(ARGV) || 0
|
22
|
+
end
|
data/lib/zhuzh/cli.rb
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# typed: strict
|
3
|
+
|
4
|
+
require 'sorbet-runtime'
|
5
|
+
|
6
|
+
module Zhuzh
|
7
|
+
# Command line interface for Zhuzh
|
8
|
+
class CLI
|
9
|
+
extend T::Sig
|
10
|
+
|
11
|
+
sig { params(args: T::Array[String]).returns(Integer) }
|
12
|
+
def self.start(args)
|
13
|
+
new.run(args)
|
14
|
+
end
|
15
|
+
|
16
|
+
sig { params(args: T::Array[String]).returns(Integer) }
|
17
|
+
def run(args)
|
18
|
+
content = if args.empty?
|
19
|
+
if $stdin.tty?
|
20
|
+
# No text, open editor
|
21
|
+
open_editor
|
22
|
+
else
|
23
|
+
# Text piped from stdin
|
24
|
+
$stdin.read
|
25
|
+
end
|
26
|
+
else
|
27
|
+
# Combine all arguments as note text
|
28
|
+
args.join(' ')
|
29
|
+
end
|
30
|
+
|
31
|
+
process_content(content) ? 0 : 1
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
sig { params(content: T.nilable(String)).returns(T::Boolean) }
|
37
|
+
def process_content(content)
|
38
|
+
return false if content.nil? || content.strip.empty?
|
39
|
+
|
40
|
+
config = Config.new
|
41
|
+
note = Note.new(content, config)
|
42
|
+
|
43
|
+
if note.save
|
44
|
+
puts note.filepath
|
45
|
+
true
|
46
|
+
else
|
47
|
+
false
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
sig { returns(T.nilable(String)) }
|
52
|
+
def open_editor
|
53
|
+
editor = ENV['EDITOR'] || 'vim'
|
54
|
+
temp_file = "/tmp/zz_#{Time.now.to_i}.md"
|
55
|
+
|
56
|
+
# Open editor and wait for it to close
|
57
|
+
system("#{editor} #{temp_file}")
|
58
|
+
|
59
|
+
if File.exist?(temp_file)
|
60
|
+
content = File.read(temp_file)
|
61
|
+
File.unlink(temp_file)
|
62
|
+
return content
|
63
|
+
end
|
64
|
+
|
65
|
+
nil
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
data/lib/zhuzh/config.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# typed: strict
|
3
|
+
|
4
|
+
require 'sorbet-runtime'
|
5
|
+
require 'fileutils'
|
6
|
+
|
7
|
+
module Zhuzh
|
8
|
+
# Manages configuration and directory settings
|
9
|
+
class Config
|
10
|
+
extend T::Sig
|
11
|
+
|
12
|
+
sig { returns(String) }
|
13
|
+
attr_reader :zz_dir
|
14
|
+
|
15
|
+
sig { void }
|
16
|
+
def initialize
|
17
|
+
@zz_dir = T.let(determine_zz_dir, String)
|
18
|
+
Zhuzh::Utils.ensure_directory_exists(@zz_dir)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
sig { returns(String) }
|
24
|
+
def determine_zz_dir
|
25
|
+
ENV['ZZ_DIR'] || File.join(Dir.home, 'Zarchive')
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/zhuzh/note.rb
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# typed: strict
|
3
|
+
|
4
|
+
require 'sorbet-runtime'
|
5
|
+
require 'fileutils'
|
6
|
+
require 'time'
|
7
|
+
|
8
|
+
module Zhuzh
|
9
|
+
# Handles creation and storage of notes
|
10
|
+
class Note
|
11
|
+
extend T::Sig
|
12
|
+
|
13
|
+
sig { returns(String) }
|
14
|
+
attr_reader :content, :filepath
|
15
|
+
|
16
|
+
sig { params(content: String, config: Zhuzh::Config).void }
|
17
|
+
def initialize(content, config)
|
18
|
+
# Add trailing newline if not present for command line input
|
19
|
+
@content = T.let(content.end_with?("\n") ? content : "#{content}\n", String)
|
20
|
+
@config = T.let(config, Zhuzh::Config)
|
21
|
+
@filepath = T.let(generate_filepath, String)
|
22
|
+
end
|
23
|
+
|
24
|
+
sig { returns(T::Boolean) }
|
25
|
+
def save
|
26
|
+
Zhuzh::Utils.ensure_directory_exists(File.dirname(@filepath))
|
27
|
+
File.write(@filepath, @content)
|
28
|
+
true
|
29
|
+
rescue StandardError => e
|
30
|
+
warn "Error saving note: #{e.message}"
|
31
|
+
false
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
sig { returns(String) }
|
37
|
+
def generate_filepath
|
38
|
+
text = strip_frontmatter(@content)
|
39
|
+
words = text.strip.split(/\s+/)
|
40
|
+
|
41
|
+
dir_name = extract_directory_name(words)
|
42
|
+
file_base = extract_file_base(words)
|
43
|
+
timestamp = Time.now.strftime('%Y-%m-%d-%H-%M-%S')
|
44
|
+
base_path = File.join(@config.zz_dir, dir_name)
|
45
|
+
|
46
|
+
create_unique_filepath(base_path, file_base, timestamp)
|
47
|
+
end
|
48
|
+
|
49
|
+
sig { params(words: T::Array[String]).returns(String) }
|
50
|
+
def extract_directory_name(words)
|
51
|
+
words.empty? ? 'blank' : normalize_path_component(T.must(words[0]))
|
52
|
+
end
|
53
|
+
|
54
|
+
sig { params(words: T::Array[String]).returns(String) }
|
55
|
+
def extract_file_base(words)
|
56
|
+
file_words = words[1..4]&.compact || []
|
57
|
+
if file_words.empty?
|
58
|
+
'blank'
|
59
|
+
else
|
60
|
+
file_words.map { |w| normalize_path_component(w) }.join('_')
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
sig { params(base_path: String, file_base: String, timestamp: String).returns(String) }
|
65
|
+
def create_unique_filepath(base_path, file_base, timestamp)
|
66
|
+
file_path = File.join(base_path, "#{file_base}_#{timestamp}.md")
|
67
|
+
|
68
|
+
counter = 1
|
69
|
+
while File.exist?(file_path)
|
70
|
+
file_path = File.join(base_path, "#{file_base}_#{counter}_#{timestamp}.md")
|
71
|
+
counter += 1
|
72
|
+
end
|
73
|
+
|
74
|
+
file_path
|
75
|
+
end
|
76
|
+
|
77
|
+
sig { params(text: String).returns(String) }
|
78
|
+
def strip_frontmatter(text)
|
79
|
+
if text.start_with?("---\n")
|
80
|
+
# Ignore everything between the first and second "---"
|
81
|
+
parts = text.split("---\n", 3)
|
82
|
+
return parts[2] || '' if parts.size >= 3
|
83
|
+
end
|
84
|
+
text
|
85
|
+
end
|
86
|
+
|
87
|
+
sig { params(component: String).returns(String) }
|
88
|
+
def normalize_path_component(component)
|
89
|
+
# Convert to lowercase, remove non-alphanumeric chars, replace spaces with underscore
|
90
|
+
normalized = component.downcase.gsub(/[^a-z0-9\s]/, '').gsub(/\s+/, '_')
|
91
|
+
# Truncate to avoid filesystem issues
|
92
|
+
T.must(normalized[0..63])
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
data/lib/zhuzh.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# typed: strict
|
3
|
+
|
4
|
+
require 'sorbet-runtime'
|
5
|
+
require_relative 'zhuzh/version'
|
6
|
+
require_relative 'zhuzh/config'
|
7
|
+
require_relative 'zhuzh/note'
|
8
|
+
require_relative 'zhuzh/cli'
|
9
|
+
|
10
|
+
module Zhuzh
|
11
|
+
class Error < StandardError; end
|
12
|
+
|
13
|
+
# Common utility methods for Zhuhz
|
14
|
+
module Utils
|
15
|
+
extend T::Sig
|
16
|
+
|
17
|
+
# Creates a directory and its parent directories if they don't exist
|
18
|
+
sig { params(dir: String).void }
|
19
|
+
def self.ensure_directory_exists(dir)
|
20
|
+
FileUtils.mkdir_p(dir)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
metadata
ADDED
@@ -0,0 +1,162 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: zhuzh
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Fuzz Leonard
|
8
|
+
bindir: bin
|
9
|
+
cert_chain: []
|
10
|
+
date: 2025-04-14 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: sorbet-runtime
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - "~>"
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '0.5'
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - "~>"
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '0.5'
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: minitest
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - "~>"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '5.0'
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '5.0'
|
40
|
+
- !ruby/object:Gem::Dependency
|
41
|
+
name: rake
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '13.0'
|
47
|
+
type: :development
|
48
|
+
prerelease: false
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - "~>"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '13.0'
|
54
|
+
- !ruby/object:Gem::Dependency
|
55
|
+
name: rubocop
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - "~>"
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '1.21'
|
61
|
+
type: :development
|
62
|
+
prerelease: false
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - "~>"
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '1.21'
|
68
|
+
- !ruby/object:Gem::Dependency
|
69
|
+
name: rubocop-minitest
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - "~>"
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0.38'
|
75
|
+
type: :development
|
76
|
+
prerelease: false
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - "~>"
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0.38'
|
82
|
+
- !ruby/object:Gem::Dependency
|
83
|
+
name: rubocop-rake
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - "~>"
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '0.7'
|
89
|
+
type: :development
|
90
|
+
prerelease: false
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - "~>"
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '0.7'
|
96
|
+
- !ruby/object:Gem::Dependency
|
97
|
+
name: sorbet
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - "~>"
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '0.5'
|
103
|
+
type: :development
|
104
|
+
prerelease: false
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - "~>"
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0.5'
|
110
|
+
- !ruby/object:Gem::Dependency
|
111
|
+
name: tapioca
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - "~>"
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '0.16'
|
117
|
+
type: :development
|
118
|
+
prerelease: false
|
119
|
+
version_requirements: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - "~>"
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: '0.16'
|
124
|
+
description: A tool for easily creating and managing markdown notes
|
125
|
+
email:
|
126
|
+
- ink@fuzz.ink
|
127
|
+
executables:
|
128
|
+
- zz
|
129
|
+
extensions: []
|
130
|
+
extra_rdoc_files: []
|
131
|
+
files:
|
132
|
+
- README.md
|
133
|
+
- bin/tapioca
|
134
|
+
- bin/zz
|
135
|
+
- lib/zhuzh.rb
|
136
|
+
- lib/zhuzh/cli.rb
|
137
|
+
- lib/zhuzh/config.rb
|
138
|
+
- lib/zhuzh/note.rb
|
139
|
+
- lib/zhuzh/version.rb
|
140
|
+
homepage: https://github.com/fuzz/zhuhz
|
141
|
+
licenses:
|
142
|
+
- MIT
|
143
|
+
metadata:
|
144
|
+
rubygems_mfa_required: 'true'
|
145
|
+
rdoc_options: []
|
146
|
+
require_paths:
|
147
|
+
- lib
|
148
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - ">="
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: 2.7.0
|
153
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
154
|
+
requirements:
|
155
|
+
- - ">="
|
156
|
+
- !ruby/object:Gem::Version
|
157
|
+
version: '0'
|
158
|
+
requirements: []
|
159
|
+
rubygems_version: 3.6.3
|
160
|
+
specification_version: 4
|
161
|
+
summary: Simple markdown note management
|
162
|
+
test_files: []
|