Day 15: Project 1 - CLI Tool (Clipflow)
A smart clipboard manager CLI that captures, searches, and organizes everything you copy with type detection and persistent history.
ဒီနေ့တော့ ရှေ့ရက်တွေမှာ လေ့လာခဲ့တာတွေ ပေါင်းပြီးတော့ project တခုစပြီးတော့ ရေးကြည့်ပါမယ်။ project ကိုတော့ clipflow လို့ နာမည်ပေးထားပါတယ်။ ဒါကတော့ clipboard လိုမျိုး data တွေ သိမ်းထားပြီး retrieve လုပ်ချင်ရင် သုံးလို့ရတာမျိုးပါ။ note ထဲမှာ paste လုပ်တာမျိုးမဟုတ်ဘဲ cli ကိုပဲ သုံးပြီးတော့ သိမ်းချင်တဲ့အခါ သုံးလို့ရပါတယ်။ တကယ့် လက်တွေ့မှာ သုံးလို့ရတဲ့အထိ ရေးကြည့်သွားမှာဖြစ်လို့ စလိုက်ကြရအောင်ပါ။
Requirements
ဒါဆိုရင် requirement အနေနဲ့ ဘာတွေ ပါမလဲဆိုတာ တချက်အရင် ကြည့်လိုက်ရအောင်ပါ။ Clipflow ဆိုတဲ့အတိုင်းပဲ clipboard က data တွေကို သိမ်းဆည်းဖို့နဲ့ ပြန်လည်ထုတ်ယူဖို့အတွက် အဓိက ထားပြီး သုံးပါမယ်။ ဒီတော့ ဘယ်လိုလုပ်ဆောင်ချက်တွေ ပါမလဲဆိုတာ တချက်ကြည့်ရအောင်ပါ။
| Description | Command | Alias | Parameter |
|---|---|---|---|
| capture text to clipboard history | --capture | -c | text |
| list recent items (defaults to 10) | --list | -l | count |
| view full content of an item | --view | index | |
| search clipboard history | --search | -s | query |
| delete an item | --delete | -d | index |
| show statistics | --stats | ||
| clear all history | --clear | ||
| show help message | --help | -h | |
| show version information | --version | -v |
အပေါ်မှာ ပြထားတာတွေကတော့ ပါဝင်မယ့် အလုပ်လုပ်ဆောင်မယ့် ပုံစံတွေနဲ့ သူတို့နဲ့ သက်ဆိုင်ရာ command တွေပဲ ဖြစ်ပါတယ်။ အကြမ်းဖျင်းအနေနဲ့ clipflow ထဲကို data ထည့်တာတွေ၊ ထုတ်တာတွေ၊ ရှာတာတွေ၊ ပြန်ကူးတာတွေနဲ့ stats တွေ ကြည့်လို့ရအောင် လုပ်တာတွေ ပါဝင်ပါမယ်။
App Architecture
App architecture ကတော့ အများကြီး ရှုပ်ထွေးအောင် မလုပ်ထားတော့ပါဘူး။ presentation layer (interface) ရှိမယ်။ သူကနေပြီးတော့ user input တွေကို command တွေအဖြစ် လှမ်းခေါ်မယ့် domain layer (manager) ဆိုပြီးရှိမယ်။ ဒါကနေမှ တကယ်လို့ သိမ်းဆည်းဖို့လိုတယ်ဆိုရင် သိမ်းဖို့အတွက် data layer (storage) ဆိုပြီး ရှိပါမယ်။ သူတို့တွေကို ထောက်ပံ့ပေးမယ့် လိုအပ်တဲ့ model တွေ၊ extension တွေ စသဖြင့်လဲ ရှိပါဦးမယ်။

ပထမဆုံး project ဆောက်တာကနေ စပြီးတော့ ဆွေးနွေးသွားပါမယ်။ Dart cli project တခု ဆောက်ဖို့ဆိုရင်တော့ အောက်မှာပေးထားတဲ့ command ကို run လိုက်လို့ရပါတယ်။ CLI tool ကို သုံးတဲ့အခါ data/argument တွေ ထည့်ပေးမှာဆိုတော့ ဒီမှာ type ကိုတော့ cli လို့ ရွေးပေးလိုက်ပါမယ်။
dart create -t cli clipflow
code clipflowဒါဆိုရင်တော့ cli project တခု create လုပ်ပြီးသား ဖြစ်သွားပါလိမ့်မယ်။ ပြီးရင် project ကို vscode မှာ ဖွင့်လိုက်ပါမယ်။ အခုလိုမျိုး ပေါ်လာပါလိမ့်မယ်။ ဒါဆိုရင်တော့ code တွေ စရေးလို့ ရပါပြီ။

Dependencies
အရင်ဆုံး လိုအပ်မယ့် yaml package ကို install လုပ်ပါမယ်။ သူ့ကိုတော့ version number cli ကနေ ပြဖို့ လိုချင်တဲ့အခါ သုံးဖို့ပါ။
dart pub add yamlpubspec.yaml ထဲက dependencies အောက်မှာ yaml: ^3.1.3 (version number ကွာနိုင်) ဆိုပြီး ဝင်သွားမှာပဲ ဖြစ်ပါတယ်။
ပြီးရင်တော့ lib ဆိုတဲ့ folder တခု create လုပ်လိုက်ပါမယ်။ ဒီထဲမှာ လိုအပ်တဲ့ code တွေ အကုန် ရေးသွားမှာပဲ ဖြစ်ပါတယ်။

Constants
ပြီးရင်တော့ constants.dart ဆိုတဲ့ file တခုဆောက်ပြီး အောက်မှာ ပြထားတာလေးတွေ သွားထည့်လိုက်ပါမယ်။ ဒီထဲမှာတော့ cli application ထဲမှာ သုံးမယ့် လိုအပ်တဲ့ constant value တွေ ပါဝင်ပါတယ်။ constant value တွေကိုတော့ k နဲ့စပြီး ရေးလေ့ရှိပါတယ်။ မသုံးလဲ ဘာမှတော့ မဖြစ်ပါဘူး။
// lib/constants.dart
const String kAppName = 'ClipFlow';
const int kMaxHistorySize = 1000;
const int kDefaultListLimit = 10;
const String kStorageDirectoryName = '.clipflow';
const String kHistoryFileName = 'history.json';
Enums
ဒီမှာတော့ enums.dart ဆိုတဲ့ file တခု ဆက်ဆောက်လိုက်ပါမယ်။ ဒီထဲမှာတော့ ဖြစ်နိုင်တဲ့ data type တွေကို ထည့်ပေးထားမှာပါ။ အခုလောလောဆယ်တော့ clipflow က text, code, url, json data တွေကို support ပေးမှာဖြစ်လို့ အခုလိုပဲ ရေးပေးလိုက်ပါမယ်။
// lib/enums.dart
enum ClipType {
clipType('🗒️', 'Text'),
codeType('💻', 'Code'),
urlType('🔗', 'URL'),
jsonType('📊', 'JSON');
final String emoji;
final String label;
const ClipType(this.emoji, this.label);
}
Mixins
ပြီးရင်တော့ mixin တခုကိုလဲ ရေးလိုက်ပါမယ်။ ဒီမှာတော့ search လုပ်တဲ့အခါ တိုက်စစ်ဖို့ logic ပါဝင်ပါတယ်။
// lib/mixins.dart
mixin Searchable {
String get textContent;
bool matches(String query) {
final lowerQuery = query.toLowerCase();
final lowerContent = textContent.toLowerCase();
return lowerContent.contains(lowerQuery);
}
}Models
ပြီးရင်တော့ လိုအပ်မယ့် ClipItem ဆိုတဲ့ model စပြီးတော့ တည်ဆောက်ပါမယ်။ model ထဲမှာ လိုအပ်မယ့် value တွေကို ဖြည့်ပြီးတော့ toJson နဲ့ fromJson ဆိုတာကိုလဲ ကြေငြာထားပါတယ်။ ဒါတွေကတော့ သိမ်းထားမယ့် data တွေကို parse လုပ်ဖို့လိုတဲ့အချိန်မှာ သုံးဖို့ပဲ ဖြစ်ပါတယ်။ toListItem နဲ့ displayFull တို့ကတော့ cli ကနေ access လုပ်တဲ့အခါ preview လိုမျိုး ပြဖို့နဲ့ data အကုန်ပြဖို့ပဲ ဖြစ်ပါတယ်။
// lib/models.dart
import 'package:clipflow/enums.dart';
import 'package:clipflow/extensions.dart';
import 'package:clipflow/mixins.dart';
class ClipItem with Searchable {
final String id;
final DateTime timestamp;
final ClipType type;
@override
final String textContent;
ClipItem({
required this.id,
required this.timestamp,
required this.type,
required this.textContent,
});
Map<String, dynamic> toJson() {
return {
'id': id,
'timestamp': timestamp.toIso8601String(),
'type': switch (type) {
ClipType.textType => 'text',
ClipType.codeType => 'code',
ClipType.urlType => 'url',
ClipType.jsonType => 'json',
},
'textContent': textContent,
};
}
factory ClipItem.fromJson(Map<String, dynamic> json) {
return ClipItem(
id: json['id'],
timestamp: DateTime.parse(json['timestamp']),
type: switch (json['type']) {
'text' => ClipType.textType,
'code' => ClipType.codeType,
'url' => ClipType.urlType,
'json' => ClipType.jsonType,
_ => ClipType.textType,
},
textContent: json['textContent'],
);
}
String toListItem(int index) {
final timeStr = timestamp.timeAgo().padRight(10);
final typeDisplay = '${type.emoji} ${type.label}'.padRight(10);
return '$index. $typeDisplay │ $timeStr | ${textContent.preview} ';
}
void displayFull() {
print('\n${'=' * 70}');
print('${type.emoji} CLIP ITEM #$id');
print('-' * 70);
print('Type: ${type.label}');
print('Created: ${timestamp.toReadable()} (${timestamp.timeAgo()})');
print('-' * 70);
print('CONTENT:');
print(textContent);
print('=' * 70);
}
}
Extensions
ပြီးရင်တော့ ဒီ app မှာ လိုအပ်မယ့် utility တွေကို extension အနေနဲ့ ရေးပါမယ်။ ဒီမှာတော့ DateTime နဲ့ String တို့ကို extension တွေ လုပ်ပြီးတော့ ရေးပါမယ်။
// lib/extensions.dart
import 'package:clipflow/enums.dart';
extension DateTimeExtension on DateTime {
String toReadable() {
const months = [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec',
];
return '${months[month - 1]} $day, $year'
'${hour.toString().padLeft(2, '0')}'
'${minute.toString().padLeft(2, '0')}';
}
String timeAgo() {
final diff = DateTime.now().difference(this);
if (diff.inDays > 0) return '${diff.inDays}d ago';
if (diff.inHours > 0) return '${diff.inHours}h ago';
if (diff.inMinutes > 0) return '${diff.inMinutes}m ago';
return 'just now';
}
}
extension StringExtension on String {
bool get isURL {
try {
final uri = Uri.parse(this);
return uri.hasScheme && {'http', 'https'}.contains(uri.scheme);
} catch (e) {
return false;
}
}
bool get isCode {
final patterns = [
'function ',
'const ',
'var ',
'let ',
'class ',
'import ',
'def ',
'void ',
'=> ',
'public ',
'private ',
'final ',
'String ',
'int ',
];
final matchCount = patterns.where((pattern) => contains(pattern)).length;
return matchCount >= (patterns.length * 0.1);
}
bool get isJSON {
final trimmed = trim();
if (trimmed.isEmpty) return false;
return (trimmed.startsWith('{') && trimmed.endsWith('}')) ||
(trimmed.startsWith('[') && trimmed.endsWith(']'));
}
String truncate(int maxLength) {
if (maxLength <= 0) return '';
final singleLine = replaceAll(RegExp(r'\s+'), ' ').trim();
if (singleLine.length <= maxLength) {
return singleLine;
}
final cutoff = maxLength > 3 ? maxLength - 3 : 0;
return '${singleLine.substring(0, cutoff)}...';
}
String get preview => truncate(70);
ClipType detectType() {
if (isURL) {
return ClipType.urlType;
} else if (isCode) {
return ClipType.codeType;
} else if (isJSON) {
return ClipType.jsonType;
}
return ClipType.textType;
}
}
Typedefs
ပြီးရင်တော့ typedefs.dart ဆိုတဲ့ file တခုကိုလဲ အခုပြထားသလိုပဲ ရေးလိုက်ပါမယ်။ ဒီ ClipStats typedef ထဲမှာတော့ record ကို သုံးပြီးတော့ value တွေ pass ပေးသွားမှာ ဖြစ်ပါတယ်။ data pass ကို သုံးချင်ရုံနဲ့ class တခုဆောက်နေစရာ မလိုဘဲ Record ကို typedef အနေနဲ့ ကြေငြာပြီး အလွယ်တကူ သုံးလို့ရသွားပါလိမ့်မယ်။
// lib/typedefs.dart
typedef ClipStats = ({
int totalItems,
int textItems,
int codeItems,
int urlItems,
int jsonItems,
});
Exceptions
ပြီးရင်တော့ app ရေးတဲ့အခါ ဖြစ်နိုင်တဲ့ expected error တွေကို exceptions တွေ အနေနဲ့ ရေးပါမယ်။
// lib/extensions.dart
class ClipflowException implements Exception {
final String message;
ClipflowException(this.message);
@override
String toString() {
return '❗ Clipflow Error: $message';
}
}
class InvalidIndexException extends ClipflowException {
final int providedIndex;
final int length;
InvalidIndexException(this.providedIndex, this.length)
: super('Invalid index: $providedIndex, Valid range: 0-${length - 1}');
}
class ItemNotFoundException extends ClipflowException {
ItemNotFoundException(String id) : super('Item not found $id');
}
Manager
ပြီးရင်တော့ အဓိက brain လို့ ပြောလို့ရတဲ့ manager ကို ရေးပါမယ်။ သူ့ထဲမှာ လုပ်ဆောင်မှု တခုစီတိုင်းအတွက် လုပ်ရမှာတွေကို စုစည်းထားတဲ့ သဘောပါ။ business/domain စသဖြင့်လဲ ခေါ်လို့ရပါတယ်။
သူ့ထဲမှာ ClipflowStorage ဆိုတဲ့ dependency တခုရှိတာကိုလဲ တွေ့ရမှာပါ။ ဒါကိုတော့ constructor ကနေတဆင့် pass ပေးသွားပါမယ်။ နောက်ကျရင် abstract class တွေအနေနဲ့ ရေးပြီးတော့ storage ကြိုက်တာ ပြောင်းလို့ရအောင် ရေးလို့လဲ ရပါတယ်။
// lib/clipflow_manager.dart
import 'dart:io';
import 'package:clipflow/clipflow_storage.dart';
import 'package:clipflow/constants.dart';
import 'package:clipflow/enums.dart';
import 'package:clipflow/exceptions.dart';
import 'package:clipflow/extensions.dart';
import 'package:clipflow/models.dart';
import 'package:clipflow/typedefs.dart';
import 'package:yaml/yaml.dart';
class ClipflowManager {
final ClipflowStorage _storage;
ClipflowManager(this._storage);
void capture(String content) {
final item = ClipItem(
id: DateTime.now().microsecondsSinceEpoch.toString(),
timestamp: DateTime.now(),
type: content.detectType(),
textContent: content,
);
_storage.save(item);
}
void list({int limit = kDefaultListLimit}) {
final items = _storage.items.take(limit).toList();
if (items.isEmpty) {
print('\n No items in history yet!');
print('');
print('Try clipflow --capture "your text here"');
return;
}
final actualLimit = items.length < limit ? items.length : limit;
print('\nClipboard history (Last $actualLimit items)');
print('=' * 100);
for (var i = 0; i < items.length; i++) {
print(items[i].toListItem(i));
}
print('=' * 100);
print('');
}
ClipItem? getItemAt(int index) {
if (index < 0 || index >= _storage.items.length) {
return null;
}
return _storage.items[index];
}
List<ClipItem> search(String query) {
return _storage.items.where((item) => item.hasMatches(query)).toList();
}
Future<void> delete(int index) async {
final item = getItemAt(index);
if (item == null) {
throw InvalidIndexException(index, _storage.items.length);
}
await _storage.deleteItem(item.id);
print('Deleted: ${item.textContent.preview}');
}
void showStats() {
final stats = _getStats();
print('\n📊 CLIPFLOW STATISTICS');
print('═' * 70);
print('Total Items: ${stats.totalItems}');
print('├─ 📝 Text: ${stats.textItems}');
print('├─ 💻 Code: ${stats.codeItems}');
print('├─ 🔗 URLs: ${stats.urlItems}');
print('└─ 📊 JSON: ${stats.jsonItems}');
print('═' * 70);
print('');
}
ClipStats _getStats() {
final items = _storage.items;
if (items.isEmpty) {
return (
totalItems: 0,
textItems: 0,
codeItems: 0,
urlItems: 0,
jsonItems: 0,
);
}
return (
totalItems: items.length,
textItems: items.where((i) => i.type == ClipType.textType).length,
codeItems: items.where((i) => i.type == ClipType.codeType).length,
urlItems: items.where((i) => i.type == ClipType.urlType).length,
jsonItems: items.where((i) => i.type == ClipType.jsonType).length,
);
}
Future<String> getAppVersion() async {
final pubspecFile = File('pubspec.yaml');
final content = await pubspecFile.readAsString();
final yaml = loadYaml(content);
if (yaml is YamlMap) {
final version = yaml['version'];
if (version is String && version.isNotEmpty) {
return version;
}
}
return '0.0.0';
}
}
CLI Interface
နောက်ဆုံးကျန်တာကတော့ command တွေကို cli ကနေပြီးတော့ လက်ခံပြီး အလုပ်လုပ်သွားပေးမယ့် bin folder ထဲက clipflow.dart ဆိုတာကို အခုလိုမျိုး update လုပ်လိုက်ပါမယ်။
// bin/clipflow.dart
import 'dart:io';
import 'dart:async';
import 'dart:convert';
import 'package:args/args.dart';
import 'package:clipflow/clipflow_manager.dart';
import 'package:clipflow/clipflow_storage.dart';
import 'package:clipflow/constants.dart';
import 'package:clipflow/exceptions.dart';
ArgParser buildParser() {
return ArgParser()
..addFlag(
'capture',
abbr: 'c',
help: 'Capture text to clipboard history (interactive)',
negatable: false,
)
..addOption(
'list',
abbr: 'l',
help: 'List recent items',
valueHelp: 'count',
defaultsTo: '$kDefaultListLimit',
)
..addOption(
'view',
help: 'View full content of an item',
valueHelp: 'index',
)
..addOption(
'search',
abbr: 's',
help: 'Search clipboard history',
valueHelp: 'query',
)
..addOption('delete', abbr: 'd', help: 'Delete an item', valueHelp: 'index')
// Flags
..addFlag('stats', help: 'Show statistics', negatable: false)
..addFlag(
'clear',
help: 'Clear all history (requires confirmation)',
negatable: false,
)
..addFlag(
'help',
abbr: 'h',
help: 'Show this help message',
negatable: false,
)
..addFlag(
'version',
abbr: 'v',
help: 'Show version information',
negatable: false,
);
}
void printUsage(ArgParser argParser) {
print('Usage: clipflow <flags> [arguments]');
print(argParser.usage);
}
Future<void> main(List<String> arguments) async {
final ArgParser argParser = buildParser();
try {
final ArgResults results = argParser.parse(arguments);
final storage = ClipflowStorage();
final manager = ClipflowManager(storage);
if (results.flag('help')) {
printUsage(argParser);
return;
}
if (results.flag('version')) {
final resolvedVersion = await manager.getAppVersion();
print('$kAppName version: $resolvedVersion');
return;
}
await storage.load();
bool commandExecuted = false;
// Capture
if (results.flag('capture')) {
print('📝 Enter text to capture (press Ctrl+D when done):');
final content = await stdin.transform(utf8.decoder).join();
if (content.trim().isEmpty) {
print('❌ No text entered.');
} else {
manager.capture(content.trim());
print('✅ Captured!');
}
commandExecuted = true;
}
// List
if (results.wasParsed('list')) {
final limit = int.tryParse(results.option('list')!) ?? kDefaultListLimit;
manager.list(limit: limit);
commandExecuted = true;
}
// View
if (results.wasParsed('view')) {
final index = int.tryParse(results.option('view')!);
if (index == null) {
print('❌ Invalid index. Must be a number.');
} else {
final item = manager.getItemAt(index);
if (item != null) {
item.displayFull();
} else {
throw InvalidIndexException(index, storage.items.length);
}
}
commandExecuted = true;
}
// Search
if (results.wasParsed('search')) {
final query = results.option('search')!;
final searchResults = manager.search(query);
if (searchResults.isEmpty) {
print('\n🔍 No results found for: "$query"');
print('');
} else {
print('\n🔍 Found ${searchResults.length} result(s) for: "$query"');
print('─' * 70);
for (var i = 0; i < searchResults.length; i++) {
print(searchResults[i].toListItem(i));
}
print('');
}
commandExecuted = true;
}
// Delete
if (results.wasParsed('delete')) {
final index = int.tryParse(results.option('delete')!);
if (index == null) {
print('❌ Invalid index. Must be a number.');
} else {
await manager.delete(index);
}
commandExecuted = true;
}
// Stats
if (results.flag('stats')) {
manager.showStats();
commandExecuted = true;
}
// Clear
if (results.flag('clear')) {
stdout.write('⚠️ Clear all history? This cannot be undone! (y/n): ');
final confirm = stdin.readLineSync();
if (confirm?.toLowerCase() == 'y') {
await storage.clear();
print('🗑️ All history cleared!');
} else {
print('❌ Cancelled');
}
commandExecuted = true;
}
// If no command was executed, show help
if (!commandExecuted) {
printUsage(argParser);
}
} on FormatException catch (e) {
print('\n❌ ${e.message}');
print('');
printUsage(argParser);
exit(1);
} on ClipflowException catch (e) {
print('\n$e\n');
exit(1);
} catch (e) {
print('\n❌ Unexpected error: $e\n');
exit(1);
}
}
အပေါ်ကဟာတွေ အကုန် ရေးပြီးပြီဆိုရင်တော့ စပြီးတော့ cli ကနေ စမ်းကြည့်လို့ရပါပြီ။

ဒါကတော့ command ဘာမှ မထည့်ပေးတဲ့အခါ help အနေနဲ့ပြပေးတာပဲ ဖြစ်ပါတယ်။
Capture (-c or --capture)
ဒါကတော့ data တွေကို clipboard ထဲမှာ မှတ်ထားချင်တဲ့အခါ capture နဲ့ သုံးပါတယ်။ ဒီမှာ paste လုပ်လိုက်တာကို မှတ်သွားပေးမှာ ဖြစ်ပါတယ်။

List (-l or --list)
ဒါကတော့ သိမ်းထားတဲ့ data တွေကို ကြည့်ချင်တဲ့အခါ သုံးလို့ရပါတယ်။


View (--view)
ဒါကတော့ တခုချင်းစီ detail ကြည့်ချင်တဲ့အခါ သုံးလို့ရပါတယ်။ သူ့ကိုတော့ index parameter ထည့်ပေးပါတယ်။

Search (-s or --search)
ဒါကတော့ keyword ပေါ်မူတည်ပြီး သိမ်းထားတဲ့ထဲမှာ ရှိလားလို့ ရှာချင်ရင် သုံးလို့ရပါတယ်။

Delete (-d or --delete)
ဒါကတော့ သိမ်းထားတဲ့ data တွေကို ဖျက်ချင်တဲ့အခါ သုံးပါတယ်။ ဒါကတော့ တခုချင်းစီ ဖျက်ချင်တဲ့အခါ သုံးလို့ရပါတယ်။ index ကို parameter အနေနဲ့ ထည့်ပေးရပါတယ်။

Clear (--clear)
ဒါကတော့ သိမ်းထားတာတွေအကုန် ဖျက်ပစ်ချင်တဲ့အခါ အသုံးပြုပါတယ်။

Stats (--stats)
ဒါကတော့ သိမ်းထားတာတွေ ဘယ်နှစ်ခုစီရှိလဲ ကြည့်ကြည့်တဲ့အခါ သုံးပါတယ်။

Version (-v)
ဒါကတော့ cli tool ရဲ့ version ကို ပြချင်တဲ့အခါ သုံးလို့ရပါတယ်။

Build Process
တကယ်လို့ ဒီ program ကို အသုံးပြုချင်တိုင်းမှာ dart run bin/clipflow.dart ကို run နေရမယ်ဆို သိပ်ပြီးတော့ အဆင်မပြေပါဘူး။ ဒီတော့ cli ထဲမှာ clipflow ဆိုပြီး ရေးလိုက်တာနဲ့ ရအောင်လဲ လုပ်လို့ရပါတယ်။ ဒါကိုတော့ complie လုပ်တဲ့အဆင့်လို့ ပြောလို့ရပါတယ်။
အသေးစိတ် ဘယ်လို option တွေနဲ့ ဘာတွေလုပ်ရမလဲဆိုတာကို ဒီမှာပေးထားပါတယ်။ သိလိုတာတွေ စမ်းကြည့်ပြီး လုပ်လို့ရပါတယ်။

ဒါဆိုရင်တော့ complie အရင်လိုက်လိုက်ပါမယ်။

Compile လုပ်ပြီးပြီဆိုရင်တော့ clipflow ဆိုတဲ့ binary file ကို တွေ့ရမှာပါ။

ပြီးရင်တော့ cli ကနေ သုံးလို့ရအောင် ရွှေ့လိုက်ပါမယ်။
mv clipflow /usr/local/binဒါဆိုရင်တော့ အခုလိုတွေ သုံးလို့ရသွားမှာပဲ ဖြစ်ပါတယ်။

ဒီနေ့ Day 15 အတွက်ကတော့ ဒီလောက်ပဲ ဖြစ်ပါတယ်။ အဆုံးထိ ဖတ်ပေးတဲ့အတွက် အများကြီး ကျေးဇူးတင်ပါတယ်။ နားမလည်တာတွေ အဆင်မပြေတာတွေ ရှိခဲ့ရင်လဲ အောက်မှာပေးထားတဲ့ discord server ထဲမှာ လာရောက်ဆွေးနွေးနိုင်ပါတယ်။ နောက်နေ့တွေမှာလဲ ဆက်လက်ပြီး sharing လုပ်သွားပေးသွားမှာ ဖြစ်တဲ့အတွက် subscribe လုပ်ထားဖို့ ဖိတ်ခေါ်ပါတယ်။
- Youtube: https://www.youtube.com/@arkarmintun
- Newsletter: https://arkar.dev/
- Discord: https://discord.gg/3xUJ6k6dkH
