tasku 0.4.1 → 0.4.2
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/CHANGELOG.md +11 -0
- data/lib/tasku/cli.rb +80 -4
- data/lib/tasku/config.rb +34 -0
- data/lib/tasku/tui/mado_list.rb +25 -7
- data/lib/tasku/version.rb +1 -1
- metadata +3 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d18e6bd1dfa0e69dc685364eb89aaf5a77ea20536179bb635fe5cabf419b7296
|
|
4
|
+
data.tar.gz: 2d37502dcef646ec9f7d5b8805ee266759e3414e6768abfc3e0393b0193c5a1e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4d90b603cc137e7a1a296d808bfae2287d7bb3a111942a5ead559bb67684bf1a24c30a0b5a03afdced036b60eaebc2a50c4293aead2af7eb6e6caac21a39b49a
|
|
7
|
+
data.tar.gz: fb33681495b68a02ed4a5b82dc1b9cb04da5409d48bb90a5e6cc70128e8b8e8417b839b4b74d4e86f3ececa9bbf8d89b96f7705b14f20cd2326ce1615cd70777
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to Tasku are documented here.
|
|
4
4
|
|
|
5
|
+
## [0.4.2] - 2026-10-29
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **Auto branch checkout** — when a task is set to `in_progress` via `tasku edit ID --status in_progress`, Tasku automatically creates and checks out a git branch named `CODE-ID-name-slug` in the linked repo. If the branch already exists it switches to it instead. Configure with `tasku git link CODE PATH`, remove with `tasku git unlink CODE`, and inspect with `tasku git list`.
|
|
9
|
+
- **Mado: confirm before delete** — pressing `d` in the interactive task list now shows a confirmation prompt; press `y` to confirm, any other key cancels
|
|
10
|
+
|
|
11
|
+
### Security
|
|
12
|
+
- All `system()` and `exec()` calls in `MadoList` now use the array form (via
|
|
13
|
+
`Shellwords.shellsplit`) instead of a shell string, preventing shell
|
|
14
|
+
interpretation of metacharacters in command arguments
|
|
15
|
+
|
|
5
16
|
## [0.4.1] - 2026-09-29
|
|
6
17
|
|
|
7
18
|
### Fixed
|
data/lib/tasku/cli.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "fileutils"
|
|
4
4
|
require "json"
|
|
5
5
|
require "net/http"
|
|
6
|
+
require "shellwords"
|
|
6
7
|
require "uri"
|
|
7
8
|
|
|
8
9
|
module Tasku
|
|
@@ -69,6 +70,48 @@ module Tasku
|
|
|
69
70
|
end
|
|
70
71
|
end
|
|
71
72
|
|
|
73
|
+
class GitApp < Thor
|
|
74
|
+
desc "link CODE PATH", "Link a project code to a git repo path (enables auto branch checkout)"
|
|
75
|
+
def link(code, path)
|
|
76
|
+
pastel = Pastel.new
|
|
77
|
+
expanded = File.expand_path(path)
|
|
78
|
+
unless Dir.exist?(expanded)
|
|
79
|
+
abort pastel.red(" Path does not exist: #{expanded}")
|
|
80
|
+
end
|
|
81
|
+
unless Dir.exist?(File.join(expanded, ".git"))
|
|
82
|
+
abort pastel.red(" Not a git repository: #{expanded}")
|
|
83
|
+
end
|
|
84
|
+
Tasku::Config.set_git_path(code, expanded)
|
|
85
|
+
puts pastel.green(" ✓ #{code.upcase} linked to #{expanded}")
|
|
86
|
+
puts pastel.dim(" Setting a task to in_progress will now auto-checkout a branch.")
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
desc "unlink CODE", "Remove a project code's git repo link"
|
|
90
|
+
def unlink(code)
|
|
91
|
+
pastel = Pastel.new
|
|
92
|
+
unless Tasku::Config.git_path_for(code)
|
|
93
|
+
abort pastel.yellow(" #{code.upcase} is not linked to any repo.")
|
|
94
|
+
end
|
|
95
|
+
Tasku::Config.unset_git_path(code)
|
|
96
|
+
puts pastel.green(" ✓ #{code.upcase} unlinked.")
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
desc "list", "List all project code → repo path links"
|
|
100
|
+
def list
|
|
101
|
+
pastel = Pastel.new
|
|
102
|
+
projects = Tasku::Config.all_git_projects
|
|
103
|
+
if projects.empty?
|
|
104
|
+
puts pastel.dim(" No git links configured. Use `tasku git link CODE PATH` to add one.")
|
|
105
|
+
return
|
|
106
|
+
end
|
|
107
|
+
puts ""
|
|
108
|
+
projects.each do |code, path|
|
|
109
|
+
puts " #{pastel.bold(code.ljust(8))} #{path}"
|
|
110
|
+
end
|
|
111
|
+
puts ""
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
72
115
|
class CloudApp < Thor
|
|
73
116
|
desc "login TOKEN", "Authenticate with Tasku Cloud using an API token"
|
|
74
117
|
option :url, type: :string, desc: "Tasku Cloud URL (default: #{Tasku::Config::CLOUD_URL_DEFAULT})"
|
|
@@ -196,9 +239,10 @@ module Tasku
|
|
|
196
239
|
abort pastel.red(" Sync failed (HTTP #{res.code}).")
|
|
197
240
|
end
|
|
198
241
|
|
|
199
|
-
body
|
|
200
|
-
server_tasks
|
|
201
|
-
|
|
242
|
+
body = JSON.parse(res.body)
|
|
243
|
+
server_tasks = body["tasks"] || []
|
|
244
|
+
server_projects = body["projects"] || []
|
|
245
|
+
synced_at = body["synced_at"]
|
|
202
246
|
|
|
203
247
|
created = updated = conflicts = 0
|
|
204
248
|
|
|
@@ -318,6 +362,9 @@ module Tasku
|
|
|
318
362
|
desc "config SUBCOMMAND", "Manage user preferences"
|
|
319
363
|
subcommand "config", ConfigApp
|
|
320
364
|
|
|
365
|
+
desc "git SUBCOMMAND", "Manage git repo links for auto branch checkout"
|
|
366
|
+
subcommand "git", GitApp
|
|
367
|
+
|
|
321
368
|
desc "cloud SUBCOMMAND", "Sync tasks with Tasku Cloud"
|
|
322
369
|
subcommand "cloud", CloudApp
|
|
323
370
|
|
|
@@ -561,6 +608,7 @@ module Tasku
|
|
|
561
608
|
|
|
562
609
|
task.update(attrs)
|
|
563
610
|
terminal.render_updated(task)
|
|
611
|
+
auto_checkout_branch(task, attrs) if attrs[:status] == "in_progress"
|
|
564
612
|
rescue Sequel::ValidationFailed => e
|
|
565
613
|
abort pastel.red("Validation error: #{e.message}")
|
|
566
614
|
end
|
|
@@ -696,7 +744,7 @@ module Tasku
|
|
|
696
744
|
category = nil if category&.empty?
|
|
697
745
|
|
|
698
746
|
priority = prompt.select("Priority?", %w[none low medium high urgent], default: 1)
|
|
699
|
-
status = prompt.select("Status?",
|
|
747
|
+
status = prompt.select("Status?", Task::VALID_STATUSES, default: 2)
|
|
700
748
|
|
|
701
749
|
start_day = prompt.ask("Start date (YYYY-MM-DD, optional):", default: "")
|
|
702
750
|
start_day = nil if start_day&.empty?
|
|
@@ -884,6 +932,34 @@ module Tasku
|
|
|
884
932
|
"\e[1;38;2;#{r};#{g};#{b}m#{text}\e[0m"
|
|
885
933
|
end
|
|
886
934
|
|
|
935
|
+
def auto_checkout_branch(task, attrs)
|
|
936
|
+
return unless task.code && !task.code.empty?
|
|
937
|
+
|
|
938
|
+
repo_path = Tasku::Config.git_path_for(task.code)
|
|
939
|
+
return unless repo_path
|
|
940
|
+
|
|
941
|
+
slug = task.name.downcase.gsub(/[^a-z0-9]+/, "-").gsub(/\A-|-\z/, "")
|
|
942
|
+
branch = "#{task.code}-#{task.id}-#{slug}"
|
|
943
|
+
|
|
944
|
+
# Check if the branch already exists locally.
|
|
945
|
+
existing = `git -C #{repo_path.shellescape} branch --list #{branch.shellescape} 2>/dev/null`.strip
|
|
946
|
+
if existing.empty?
|
|
947
|
+
result = system("git", "-C", repo_path, "checkout", "-b", branch, out: :close, err: :close)
|
|
948
|
+
if result
|
|
949
|
+
puts pastel.dim(" git: created and switched to branch #{branch}")
|
|
950
|
+
else
|
|
951
|
+
puts pastel.yellow(" git: could not create branch #{branch} — is the working tree clean?")
|
|
952
|
+
end
|
|
953
|
+
else
|
|
954
|
+
result = system("git", "-C", repo_path, "checkout", branch, out: :close, err: :close)
|
|
955
|
+
if result
|
|
956
|
+
puts pastel.dim(" git: switched to existing branch #{branch}")
|
|
957
|
+
else
|
|
958
|
+
puts pastel.yellow(" git: could not switch to branch #{branch}")
|
|
959
|
+
end
|
|
960
|
+
end
|
|
961
|
+
end
|
|
962
|
+
|
|
887
963
|
def colour_cell(col, padded_val, row)
|
|
888
964
|
case col.to_s
|
|
889
965
|
when "priority"
|
data/lib/tasku/config.rb
CHANGED
|
@@ -104,6 +104,40 @@ module Tasku
|
|
|
104
104
|
!cloud_token.nil?
|
|
105
105
|
end
|
|
106
106
|
|
|
107
|
+
# ── Git project paths ──────────────────────────────────────────────────
|
|
108
|
+
# Maps a 3-char project code (e.g. "TAS") to an absolute repo path.
|
|
109
|
+
# Stored under the "git_projects" key in config.json.
|
|
110
|
+
|
|
111
|
+
def self.git_path_for(code)
|
|
112
|
+
git_projects[code.to_s.upcase]
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def self.set_git_path(code, path)
|
|
116
|
+
current = all
|
|
117
|
+
current["git_projects"] ||= {}
|
|
118
|
+
current["git_projects"][code.to_s.upcase] = File.expand_path(path)
|
|
119
|
+
FileUtils.mkdir_p(File.dirname(CONFIG_PATH))
|
|
120
|
+
File.write(CONFIG_PATH, JSON.pretty_generate(current))
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def self.unset_git_path(code)
|
|
124
|
+
current = all
|
|
125
|
+
return unless current["git_projects"]
|
|
126
|
+
|
|
127
|
+
current["git_projects"].delete(code.to_s.upcase)
|
|
128
|
+
current.delete("git_projects") if current["git_projects"].empty?
|
|
129
|
+
File.write(CONFIG_PATH, JSON.pretty_generate(current))
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def self.all_git_projects
|
|
133
|
+
git_projects
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def self.git_projects
|
|
137
|
+
all["git_projects"] || {}
|
|
138
|
+
end
|
|
139
|
+
private_class_method :git_projects
|
|
140
|
+
|
|
107
141
|
def self.get(key)
|
|
108
142
|
all[key] || VALID_KEYS.dig(key, :default)
|
|
109
143
|
end
|
data/lib/tasku/tui/mado_list.rb
CHANGED
|
@@ -13,6 +13,7 @@ module Tasku
|
|
|
13
13
|
# button handlers can inject it into commands like `tasku edit <id>`.
|
|
14
14
|
class MadoList
|
|
15
15
|
HINT = " ↑↓ navigate a add e/↵ edit d delete s stats q quit"
|
|
16
|
+
HINT_CONFIRM = " Delete task? Press y to confirm, any other key to cancel"
|
|
16
17
|
|
|
17
18
|
def initialize(tasks, colour_map: {}, bar: {}, cols_cfg: {}, project: nil, list_cmd: nil)
|
|
18
19
|
@tasks = tasks
|
|
@@ -156,7 +157,7 @@ module Tasku
|
|
|
156
157
|
run_command("tasku edit #{@tasks[@cursor].id} -i")
|
|
157
158
|
when :delete
|
|
158
159
|
return if @tasks.empty?
|
|
159
|
-
|
|
160
|
+
confirm_delete
|
|
160
161
|
when :stats
|
|
161
162
|
run_and_show("tasku stats")
|
|
162
163
|
when :inline_exec
|
|
@@ -175,7 +176,7 @@ module Tasku
|
|
|
175
176
|
restore_terminal
|
|
176
177
|
$stdin.cooked!
|
|
177
178
|
MadoLog.log("inline_exec tasku list → exec #{cmd.inspect}")
|
|
178
|
-
exec(cmd)
|
|
179
|
+
exec(*Shellwords.shellsplit(cmd))
|
|
179
180
|
else
|
|
180
181
|
run_command(cmd)
|
|
181
182
|
end
|
|
@@ -189,9 +190,9 @@ module Tasku
|
|
|
189
190
|
@running = false
|
|
190
191
|
restore_terminal
|
|
191
192
|
$stdin.cooked!
|
|
192
|
-
system(cmd)
|
|
193
|
+
system(*Shellwords.shellsplit(cmd))
|
|
193
194
|
MadoLog.log("run_command finished cmd=#{cmd.inspect} → exec #{@list_cmd}")
|
|
194
|
-
exec(@list_cmd)
|
|
195
|
+
exec(*Shellwords.shellsplit(@list_cmd))
|
|
195
196
|
end
|
|
196
197
|
|
|
197
198
|
# Run a non-interactive display command, wait for any keypress via
|
|
@@ -202,12 +203,12 @@ module Tasku
|
|
|
202
203
|
@running = false
|
|
203
204
|
restore_terminal
|
|
204
205
|
$stdin.cooked!
|
|
205
|
-
system(cmd)
|
|
206
|
+
system(*Shellwords.shellsplit(cmd))
|
|
206
207
|
$stdout.print "\r\n\e[2m Press any key to return to list…\e[0m\r\n"
|
|
207
208
|
$stdout.flush
|
|
208
209
|
$stdin.getch
|
|
209
210
|
MadoLog.log("run_and_show keypress received → exec #{@list_cmd}")
|
|
210
|
-
exec(@list_cmd)
|
|
211
|
+
exec(*Shellwords.shellsplit(@list_cmd))
|
|
211
212
|
end
|
|
212
213
|
|
|
213
214
|
# Show a SQL prompt, run the query, wait for keypress, then re-exec list.
|
|
@@ -227,7 +228,24 @@ module Tasku
|
|
|
227
228
|
$stdin.getch
|
|
228
229
|
end
|
|
229
230
|
MadoLog.log("run_sql done → exec #{@list_cmd}")
|
|
230
|
-
exec(@list_cmd)
|
|
231
|
+
exec(*Shellwords.shellsplit(@list_cmd))
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def confirm_delete
|
|
235
|
+
task = @tasks[@cursor]
|
|
236
|
+
name_preview = task.name.length > 40 ? "#{task.name[0, 39]}…" : task.name
|
|
237
|
+
border = @pastel.dim(" " + "─" * (@row_width + 4))
|
|
238
|
+
$stdout.print "\r\n#{border}\r\n"
|
|
239
|
+
$stdout.print " #{@pastel.yellow("Delete: ")}#{name_preview}\r\n"
|
|
240
|
+
$stdout.print "#{border}\r\n"
|
|
241
|
+
$stdout.print "#{@pastel.dim(HINT_CONFIRM)}\r\n"
|
|
242
|
+
$stdout.flush
|
|
243
|
+
byte = $stdin.getbyte
|
|
244
|
+
if byte == 0x79 || byte == 0x59 # y / Y
|
|
245
|
+
run_command("tasku delete #{task.id} -f")
|
|
246
|
+
else
|
|
247
|
+
full_render
|
|
248
|
+
end
|
|
231
249
|
end
|
|
232
250
|
|
|
233
251
|
def restore_terminal
|
data/lib/tasku/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: tasku
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.4.
|
|
4
|
+
version: 0.4.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tom Dringer
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: exe
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: thor
|
|
@@ -82,7 +81,6 @@ dependencies:
|
|
|
82
81
|
version: '1.6'
|
|
83
82
|
description: Tasku is a terminal-based task manager with colour-coded priorities,
|
|
84
83
|
status tracking, SQLite persistence, and a beautiful CLI interface.
|
|
85
|
-
email:
|
|
86
84
|
executables:
|
|
87
85
|
- tasku
|
|
88
86
|
extensions: []
|
|
@@ -107,7 +105,6 @@ metadata:
|
|
|
107
105
|
homepage_uri: https://github.com/tomdringer/tasku
|
|
108
106
|
source_code_uri: https://github.com/tomdringer/tasku
|
|
109
107
|
changelog_uri: https://github.com/tomdringer/tasku/blob/main/CHANGELOG.md
|
|
110
|
-
post_install_message:
|
|
111
108
|
rdoc_options: []
|
|
112
109
|
require_paths:
|
|
113
110
|
- lib
|
|
@@ -122,8 +119,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
122
119
|
- !ruby/object:Gem::Version
|
|
123
120
|
version: '0'
|
|
124
121
|
requirements: []
|
|
125
|
-
rubygems_version:
|
|
126
|
-
signing_key:
|
|
122
|
+
rubygems_version: 4.0.20
|
|
127
123
|
specification_version: 4
|
|
128
124
|
summary: タスクリスト — a beautiful terminal task manager
|
|
129
125
|
test_files: []
|